Skip to content

Commit

Permalink
Display provider credentials for CAPZ (#4175)
Browse files Browse the repository at this point in the history
* Add types and client functions for AzureClusterIdentity resources

* Add permission hook for provider credentials

* Refactor fetching provider credentials logic into utility function

* Use provider-specific components for provider-specific logic only

* Display provider credentials for CAPZ

* Add test cases for provider credentials on CAPZ

* Fix loading behaviour

* Bump circleCI resource class

* Revert "Bump circleCI resource class"

This reverts commit 2dd0ecf.
  • Loading branch information
kuosandys authored Mar 22, 2023
1 parent 71cf810 commit d9e1df3
Show file tree
Hide file tree
Showing 19 changed files with 1,200 additions and 434 deletions.
4 changes: 4 additions & 0 deletions scripts/generate/mapi-resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
crdURL: https://raw.githubusercontent.com/kubernetes-sigs/cluster-api-provider-azure/main/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
verbs:
- get
- name: AzureClusterIdentity
crdURL: https://raw.githubusercontent.com/kubernetes-sigs/cluster-api-provider-azure/main/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusteridentities.yaml
verbs:
- get
- name: AzureMachineTemplate
crdURL: https://raw.githubusercontent.com/kubernetes-sigs/cluster-api-provider-azure/main/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml
verbs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const ClusterDetailOverview: React.FC<React.PropsWithChildren<{}>> = () => {
/>
<ClusterDetailWidgetKubernetesAPI cluster={cluster} basis='100%' />
<ClusterDetailWidgetProvider
cluster={cluster}
providerCluster={providerCluster}
basis='100%'
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,97 @@
import { useAuthProvider } from 'Auth/MAPI/MapiAuthProvider';
import { Grid } from 'grommet';
import { ProviderCluster } from 'MAPI/types';
import { Cluster, ProviderCluster, ProviderCredential } from 'MAPI/types';
import { extractErrorMessage } from 'MAPI/utils';
import { GenericResponseError } from 'model/clients/GenericResponseError';
import * as capav1beta1 from 'model/services/mapi/capav1beta1';
import * as capgv1beta1 from 'model/services/mapi/capgv1beta1';
import * as capzv1beta1 from 'model/services/mapi/capzv1beta1';
import * as infrav1alpha2 from 'model/services/mapi/infrastructurev1alpha2';
import * as infrav1alpha3 from 'model/services/mapi/infrastructurev1alpha3';
import React from 'react';
import * as legacyCredentials from 'model/services/mapi/legacy/credentials';
import { selectOrganizations } from 'model/stores/organization/selectors';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router';
import useSWR from 'swr';
import ClusterDetailWidget from 'UI/Display/MAPI/clusters/ClusterDetail/ClusterDetailWidget';
import ErrorReporter from 'utils/errors/ErrorReporter';
import { FlashMessage, messageTTL, messageType } from 'utils/flashMessage';
import { useHttpClientFactory } from 'utils/hooks/useHttpClientFactory';

import { usePermissionsForProviderCredentials } from '../permissions/usePermissionsForProviderCredentials';
import ClusterDetailWidgetProviderAWS from './ClusterDetailWidgetProviderAWS';
import ClusterDetailWidgetProviderAzure from './ClusterDetailWidgetProviderAzure';
import ClusterDetailWidgetProviderCAPA from './ClusterDetailWidgetProviderCAPA';
import ClusterDetailWidgetProviderCAPG from './ClusterDetailWidgetProviderCAPG';
import ClusterDetailWidgetProviderLoader from './ClusterDetailWidgetProviderLoader';
import { fetchProviderCredential, fetchProviderCredentialKey } from './utils';

interface IClusterDetailWidgetProviderProps
extends Omit<
React.ComponentPropsWithoutRef<typeof ClusterDetailWidget>,
'title'
> {
cluster?: Cluster;
providerCluster?: ProviderCluster;
}

const ClusterDetailWidgetProvider: React.FC<
React.PropsWithChildren<IClusterDetailWidgetProviderProps>
> = ({ providerCluster, ...props }) => {
const isLoading = typeof providerCluster === 'undefined';
> = ({ cluster, providerCluster, ...props }) => {
const { orgId } = useParams<{ clusterId: string; orgId: string }>();
const organizations = useSelector(selectOrganizations());
const selectedOrg = orgId ? organizations[orgId] : undefined;
const selectedOrgID = selectedOrg?.name ?? selectedOrg?.id;

const { kind, apiVersion } = providerCluster || {};
const clientFactory = useHttpClientFactory();
const auth = useAuthProvider();

const provider = window.config.info.general.provider;

const { canList, canGet } = usePermissionsForProviderCredentials(
provider,
selectedOrg?.namespace ?? ''
);

const providerCredentialKey =
canList && canGet
? fetchProviderCredentialKey(cluster, providerCluster, selectedOrgID)
: undefined;

const {
data: providerCredential,
error: providerCredentialError,
isLoading: providerCredentialIsLoading,
} = useSWR<ProviderCredential, GenericResponseError>(
providerCredentialKey,
() =>
fetchProviderCredential(
clientFactory,
auth,
cluster!,
providerCluster,
selectedOrgID!
)
);

useEffect(() => {
if (providerCredentialError) {
new FlashMessage(
`Could not fetch provider-specific credentials`,
messageType.ERROR,
messageTTL.LONG,
extractErrorMessage(providerCredentialError)
);

ErrorReporter.getInstance().notify(providerCredentialError);
}
}, [providerCredentialError, orgId]);

const infrastructureRef = cluster?.spec?.infrastructureRef;
const { kind } = infrastructureRef || {};

const isLoading =
cluster === undefined ||
providerCluster === undefined ||
providerCredentialIsLoading;

return (
<ClusterDetailWidget title='Provider' inline={true} {...props}>
Expand All @@ -39,10 +103,12 @@ const ClusterDetailWidgetProvider: React.FC<
>
{isLoading ? (
<ClusterDetailWidgetProviderLoader />
) : kind === capav1beta1.AWSCluster &&
apiVersion === capav1beta1.ApiVersion ? (
<ClusterDetailWidgetProviderCAPA
) : kind === capav1beta1.AWSCluster ? (
<ClusterDetailWidgetProviderAWS
providerCluster={providerCluster as capav1beta1.IAWSCluster}
providerCredential={
providerCredential as capav1beta1.IAWSClusterRoleIdentity
}
/>
) : kind === capgv1beta1.GCPCluster ? (
<ClusterDetailWidgetProviderCAPG
Expand All @@ -51,13 +117,11 @@ const ClusterDetailWidgetProvider: React.FC<
) : kind === capzv1beta1.AzureCluster ? (
<ClusterDetailWidgetProviderAzure
providerCluster={providerCluster as capzv1beta1.IAzureCluster}
/>
) : (kind === infrav1alpha2.AWSCluster &&
apiVersion === infrav1alpha2.ApiVersion) ||
(kind === infrav1alpha3.AWSCluster &&
apiVersion === infrav1alpha3.ApiVersion) ? (
<ClusterDetailWidgetProviderAWS
providerCluster={providerCluster as infrav1alpha3.IAWSCluster}
providerCredential={
providerCredential as
| capzv1beta1.IAzureClusterIdentity
| legacyCredentials.ICredential
}
/>
) : null}
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import { useAuthProvider } from 'Auth/MAPI/MapiAuthProvider';
import { Text } from 'grommet';
import { normalizeColor } from 'grommet/utils';
import { extractErrorMessage, getProviderClusterLocation } from 'MAPI/utils';
import { GenericResponseError } from 'model/clients/GenericResponseError';
import { getProviderClusterLocation } from 'MAPI/utils';
import * as capav1beta1 from 'model/services/mapi/capav1beta1';
import * as infrav1alpha2 from 'model/services/mapi/infrastructurev1alpha2';
import * as infrav1alpha3 from 'model/services/mapi/infrastructurev1alpha3';
import * as legacyCredentials from 'model/services/mapi/legacy/credentials';
import { selectOrganizations } from 'model/stores/organization/selectors';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router';
import React from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import OptionalValue from 'UI/Display/OptionalValue/OptionalValue';
import ErrorReporter from 'utils/errors/ErrorReporter';
import { FlashMessage, messageTTL, messageType } from 'utils/flashMessage';
import { useHttpClient } from 'utils/hooks/useHttpClient';

import { usePermissionsForOrgCredentials } from '../permissions/usePermissionsForOrgCredentials';
import { getCredentialsAccountID } from './utils';
import { getAWSCredentialAccountID } from './utils';

const ValueWrapper = styled.div`
display: inline-block;
Expand All @@ -29,69 +21,18 @@ const StyledLink = styled.a`
`;

interface IClusterDetailWidgetProviderAWSProps {
providerCluster: infrav1alpha3.IAWSCluster;
providerCluster:
| capav1beta1.IAWSCluster
| infrav1alpha2.IAWSCluster
| infrav1alpha3.IAWSCluster;
providerCredential?:
| capav1beta1.IAWSClusterRoleIdentity
| legacyCredentials.ICredential;
}

const ClusterDetailWidgetProviderAWS: React.FC<
React.PropsWithChildren<IClusterDetailWidgetProviderAWSProps>
> = ({ providerCluster }) => {
const { orgId } = useParams<{ clusterId: string; orgId: string }>();
const organizations = useSelector(selectOrganizations());
const selectedOrg = orgId ? organizations[orgId] : undefined;
const selectedOrgID = selectedOrg?.name ?? selectedOrg?.id;

const credentialListClient = useHttpClient();
const auth = useAuthProvider();

const provider = window.config.info.general.provider;

const { canList } = usePermissionsForOrgCredentials(
provider,
selectedOrg?.namespace ?? ''
);

const credentialListKey =
canList && selectedOrgID
? legacyCredentials.getCredentialListKey(selectedOrgID)
: undefined;

const {
data: credentialList,
error: credentialListError,
isLoading: credentialListIsLoading,
} = useSWR<legacyCredentials.ICredentialList, GenericResponseError>(
credentialListKey,
() =>
legacyCredentials.getCredentialList(
credentialListClient,
auth,
selectedOrgID!
)
);

useEffect(() => {
if (credentialListError) {
new FlashMessage(
`Could not fetch provider-specific credentials for organization ${orgId}`,
messageType.ERROR,
messageTTL.LONG,
extractErrorMessage(credentialListError)
);

ErrorReporter.getInstance().notify(credentialListError);
}
}, [credentialListError, orgId]);

const credentials = credentialListIsLoading
? undefined
: credentialList?.items;

const accountID = credentialListIsLoading
? undefined
: credentials
? getCredentialsAccountID(credentials)
: '';

> = ({ providerCluster, providerCredential }) => {
return (
<>
<Text>AWS region</Text>
Expand All @@ -100,7 +41,7 @@ const ClusterDetailWidgetProviderAWS: React.FC<
</ValueWrapper>

<Text>Account ID</Text>
<OptionalValue value={accountID}>
<OptionalValue value={getAWSCredentialAccountID(providerCredential)}>
{(value) => (
<StyledLink
href={`https://${value}.signin.aws.amazon.com/console`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import { useAuthProvider } from 'Auth/MAPI/MapiAuthProvider';
import { Text } from 'grommet';
import { extractErrorMessage, getProviderClusterLocation } from 'MAPI/utils';
import { GenericResponseError } from 'model/clients/GenericResponseError';
import { getProviderClusterLocation } from 'MAPI/utils';
import * as capzv1beta1 from 'model/services/mapi/capzv1beta1';
import * as legacyCredentials from 'model/services/mapi/legacy/credentials';
import { selectOrganizations } from 'model/stores/organization/selectors';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router';
import React from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import OptionalValue from 'UI/Display/OptionalValue/OptionalValue';
import ErrorReporter from 'utils/errors/ErrorReporter';
import { FlashMessage, messageTTL, messageType } from 'utils/flashMessage';
import { useHttpClient } from 'utils/hooks/useHttpClient';

import { usePermissionsForOrgCredentials } from '../permissions/usePermissionsForOrgCredentials';
import { getCredentialsAccountID, getCredentialsAzureTenantID } from './utils';
import { getAzureCredentialDetails } from './utils';

const ValueWrapper = styled.div`
display: inline-block;
Expand All @@ -25,69 +15,19 @@ const ValueWrapper = styled.div`

interface IClusterDetailWidgetProviderAzureProps {
providerCluster: capzv1beta1.IAzureCluster;
providerCredential?:
| legacyCredentials.ICredential
| capzv1beta1.IAzureClusterIdentity;
}

const ClusterDetailWidgetProviderAzure: React.FC<
React.PropsWithChildren<IClusterDetailWidgetProviderAzureProps>
> = ({ providerCluster }) => {
const { orgId } = useParams<{ clusterId: string; orgId: string }>();
const organizations = useSelector(selectOrganizations());
const selectedOrg = orgId ? organizations[orgId] : undefined;
const selectedOrgID = selectedOrg?.name ?? selectedOrg?.id;

const credentialListClient = useHttpClient();
const auth = useAuthProvider();

const provider = window.config.info.general.provider;

const { canList } = usePermissionsForOrgCredentials(
provider,
selectedOrg?.namespace ?? ''
);

const credentialListKey =
canList && selectedOrgID
? legacyCredentials.getCredentialListKey(selectedOrgID)
: undefined;

const {
data: credentialList,
error: credentialListError,
isLoading: credentialListIsLoading,
} = useSWR<legacyCredentials.ICredentialList, GenericResponseError>(
credentialListKey,
() =>
legacyCredentials.getCredentialList(
credentialListClient,
auth,
selectedOrgID!
)
> = ({ providerCluster, providerCredential }) => {
const credentialDetails = getAzureCredentialDetails(
providerCluster,
providerCredential
);

useEffect(() => {
if (credentialListError) {
new FlashMessage(
`Could not fetch provider-specific credentials for organization ${orgId}`,
messageType.ERROR,
messageTTL.LONG,
extractErrorMessage(credentialListError)
);

ErrorReporter.getInstance().notify(credentialListError);
}
}, [credentialListError, orgId]);

const credentials = credentialListIsLoading
? undefined
: credentialList?.items;

const subscriptionID = credentialListIsLoading
? undefined
: credentials
? getCredentialsAccountID(credentials)
: providerCluster.spec?.subscriptionID ?? '';
const tenantID = credentials ? getCredentialsAzureTenantID(credentials) : '';

return (
<>
<Text>Azure region</Text>
Expand All @@ -96,12 +36,12 @@ const ClusterDetailWidgetProviderAzure: React.FC<
</ValueWrapper>

<Text>Subscription ID</Text>
<OptionalValue value={subscriptionID} loaderWidth={250}>
<OptionalValue value={credentialDetails.subscriptionID} loaderWidth={250}>
{(value) => <code>{value}</code>}
</OptionalValue>

<Text>Tenant ID</Text>
<OptionalValue value={tenantID} loaderWidth={250}>
<OptionalValue value={credentialDetails.tenantID} loaderWidth={250}>
{(value) => <code>{value}</code>}
</OptionalValue>
</>
Expand Down
Loading

0 comments on commit d9e1df3

Please sign in to comment.