add Azure as a Kubernetes auth provider (#11299)

* add azure auth provider

Signed-off-by: Guilherme Oenning <goenning@eshopworld.com>

* fix prettier-S

Signed-off-by: Guilherme Oenning <goenning@eshopworld.com>

* add aks dashboard formatter

Signed-off-by: Guilherme Oenning <goenning@eshopworld.com>

* ammend patch notes

Signed-off-by: Guilherme Oenning <goenning@eshopworld.com>

* typo

Signed-off-by: Guilherme Oenning <goenning@eshopworld.com>

* update enum to include azure

Signed-off-by: goenning <me@goenning.net>

* fix typo

Signed-off-by: goenning <me@goenning.net>

* add plugin to changeset

Signed-off-by: goenning <me@goenning.net>

Co-authored-by: Guilherme Oenning <goenning@eshopworld.com>
This commit is contained in:
Guilherme Oenning
2022-05-06 18:24:56 +01:00
committed by GitHub
parent ccfb9f972d
commit 1ef98cfe48
15 changed files with 219 additions and 8 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-kubernetes-backend': patch
'@backstage/plugin-kubernetes-common': patch
---
add Azure Identity auth provider and AKS dashboard formatter
@@ -91,6 +91,7 @@ cluster. Valid values are:
| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
| `aws` | This will use AWS credentials to access resources in EKS clusters |
| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters |
| `azure` | This will use [Azure Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to access resources in clusters |
##### `clusters.\*.skipTLSVerify`
+5
View File
@@ -24,6 +24,11 @@ export interface AWSClusterDetails extends ClusterDetails {
externalId?: string;
}
// Warning: (ae-missing-release-tag) "AzureClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface AzureClusterDetails extends ClusterDetails {}
// Warning: (ae-missing-release-tag) "ClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+1
View File
@@ -35,6 +35,7 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@azure/identity": "^2.0.4",
"@backstage/backend-common": "^0.13.3-next.0",
"@backstage/catalog-model": "^1.0.1",
"@backstage/config": "^1.0.0",
+1 -1
View File
@@ -52,7 +52,7 @@ export interface Config {
/** @visibility secret */
serviceAccountToken?: string;
/** @visibility frontend */
authProvider: 'aws' | 'google' | 'serviceAccount';
authProvider: 'aws' | 'google' | 'serviceAccount' | 'azure';
/** @visibility frontend */
skipTLSVerify?: boolean;
}>;
@@ -61,6 +61,9 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
return { assumeRole, externalId, ...clusterDetails };
}
case 'azure': {
return clusterDetails;
}
case 'serviceAccount': {
return clusterDetails;
}
@@ -0,0 +1,41 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KubernetesAuthTranslator } from './types';
import { AzureClusterDetails } from '../types/types';
import { DefaultAzureCredential } from '@azure/identity';
const aksScope = '6dae42f8-4368-4678-94ff-3960e28e3630/.default'; // This scope is the same for all Azure Managed Kubernetes
export class AzureIdentityKubernetesAuthTranslator
implements KubernetesAuthTranslator
{
async decorateClusterDetailsWithAuth(
clusterDetails: AzureClusterDetails,
): Promise<AzureClusterDetails> {
const clusterDetailsWithAuthToken: AzureClusterDetails = Object.assign(
{},
clusterDetails,
);
const credentials = new DefaultAzureCredential();
// TODO: can we cache this? It's inneficiant to get a new token every time
const accessToken = await credentials.getToken(aksScope);
clusterDetailsWithAuthToken.serviceAccountToken = accessToken.token;
return clusterDetailsWithAuthToken;
}
}
@@ -19,6 +19,7 @@ import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
import { GoogleServiceAccountAuthTranslator } from './GoogleServiceAccountAuthProvider';
import { AzureIdentityKubernetesAuthTranslator } from './AzureIdentityKubernetesAuthTranslator';
export class KubernetesAuthTranslatorGenerator {
static getKubernetesAuthTranslatorInstance(
@@ -31,6 +32,9 @@ export class KubernetesAuthTranslatorGenerator {
case 'aws': {
return new AwsIamKubernetesAuthTranslator();
}
case 'azure': {
return new AzureIdentityKubernetesAuthTranslator();
}
case 'serviceAccount': {
return new ServiceAccountKubernetesAuthTranslator();
}
@@ -147,6 +147,7 @@ export interface ClusterDetails {
}
export interface GKEClusterDetails extends ClusterDetails {}
export interface AzureClusterDetails extends ClusterDetails {}
export interface ServiceAccountClusterDetails extends ClusterDetails {}
export interface AWSClusterDetails extends ClusterDetails {
assumeRole?: string;
+1 -1
View File
@@ -18,7 +18,7 @@ import { V1Service } from '@kubernetes/client-node';
// Warning: (ae-missing-release-tag) "AuthProviderType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws' | 'azure';
// Warning: (ae-missing-release-tag) "ClientContainerStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+1 -1
View File
@@ -84,7 +84,7 @@ export interface ObjectsByEntityResponse {
items: ClusterObjects[];
}
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws' | 'azure';
export type FetchResponse =
| PodFetchResponse
@@ -0,0 +1,27 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KubernetesAuthProvider } from './types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
export class AzureKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
// No-op, with azure auth, server's Azure credentials are used for access
return requestBody;
}
}
@@ -21,6 +21,7 @@ import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetes
import { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider';
import { OAuthApi } from '@backstage/core-plugin-api';
import { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider';
import { AzureKubernetesAuthProvider } from './AzureKubernetesAuthProvider';
export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
private readonly kubernetesAuthProviderMap: Map<
@@ -43,6 +44,10 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
new GoogleServiceAccountAuthProvider(),
);
this.kubernetesAuthProviderMap.set('aws', new AwsKubernetesAuthProvider());
this.kubernetesAuthProviderMap.set(
'azure',
new AzureKubernetesAuthProvider(),
);
}
async decorateRequestBodyForAuth(
@@ -16,10 +16,9 @@
import { aksFormatter } from './aks';
describe('clusterLinks - AKS formatter', () => {
it('should return an url on the workloads when there is a namespace only', () => {
it('should provide a dashboardParameters in the options', () => {
expect(() =>
aksFormatter({
dashboardUrl: new URL('https://k8s.foo.com'),
object: {
metadata: {
name: 'foobar',
@@ -28,6 +27,90 @@ describe('clusterLinks - AKS formatter', () => {
},
kind: 'Deployment',
}),
).toThrowError('AKS formatter is not yet implemented. Please, contribute!');
).toThrowError('AKS dashboard requires a dashboardParameters option');
});
it('should provide a subscriptionId in the dashboardParameters options', () => {
expect(() =>
aksFormatter({
dashboardParameters: {
resourceGroup: 'rg-1',
clusterName: 'cluster-1',
},
object: {
metadata: {
name: 'foobar',
namespace: 'bar',
},
},
kind: 'Deployment',
}),
).toThrowError(
'AKS dashboard requires a "subscriptionId" of type string in the dashboardParameters option',
);
});
it('should provide a resourceGroup in the dashboardParameters options', () => {
expect(() =>
aksFormatter({
dashboardParameters: {
subscriptionId: '1234-GUID-5678',
clusterName: 'cluster-1',
},
object: {
metadata: {
name: 'foobar',
namespace: 'bar',
},
},
kind: 'Deployment',
}),
).toThrowError(
'AKS dashboard requires a "resourceGroup" of type string in the dashboardParameters option',
);
});
it('should provide a clusterName in the dashboardParameters options', () => {
expect(() =>
aksFormatter({
dashboardParameters: {
subscriptionId: '1234-GUID-5678',
resourceGroup: 'us-east1-c',
},
object: {
metadata: {
name: 'foobar',
namespace: 'bar',
},
},
kind: 'Deployment',
}),
).toThrowError(
'AKS dashboard requires a "clusterName" of type string in the dashboardParameters option',
);
});
it('should return an url on the cluster with object details', () => {
const url = aksFormatter({
dashboardParameters: {
subscriptionId: '1234-GUID-5678',
resourceGroup: 'rg-1',
clusterName: 'cluster-1',
},
object: {
metadata: {
name: 'my-deployment',
namespace: 'my-namespace',
uid: '111-GUID-222',
},
spec: {
selector: {
matchLabels: {
app: 'foo',
},
},
},
},
kind: 'Deployment',
});
expect(url.href).toBe(
'https://portal.azure.com/#blade/Microsoft_Azure_ContainerService/AksK8ResourceMenuBlade/overview-Deployment/aksClusterId/%2Fsubscriptions%2F1234-GUID-5678%2FresourceGroups%2Frg-1%2Fproviders%2FMicrosoft.ContainerService%2FmanagedClusters%2Fcluster-1/resource/%7B%22kind%22%3A%22Deployment%22%2C%22metadata%22%3A%7B%22name%22%3A%22my-deployment%22%2C%22namespace%22%3A%22my-namespace%22%2C%22uid%22%3A%22111-GUID-222%22%7D%2C%22spec%22%3A%7B%22selector%22%3A%7B%22matchLabels%22%3A%7B%22app%22%3A%22foo%22%7D%7D%7D%7D',
);
});
});
@@ -15,6 +15,39 @@
*/
import { ClusterLinksFormatterOptions } from '../../../types/types';
export function aksFormatter(_options: ClusterLinksFormatterOptions): URL {
throw new Error('AKS formatter is not yet implemented. Please, contribute!');
const basePath =
'https://portal.azure.com/#blade/Microsoft_Azure_ContainerService/AksK8ResourceMenuBlade/overview-Deployment/aksClusterId';
const requiredParams = ['subscriptionId', 'resourceGroup', 'clusterName'];
export function aksFormatter(options: ClusterLinksFormatterOptions): URL {
if (!options.dashboardParameters) {
throw new Error('AKS dashboard requires a dashboardParameters option');
}
const args = options.dashboardParameters;
for (const param of requiredParams) {
if (typeof args[param] !== 'string') {
throw new Error(
`AKS dashboard requires a "${param}" of type string in the dashboardParameters option`,
);
}
}
const path = `/subscriptions/${args.subscriptionId}/resourceGroups/${args.resourceGroup}/providers/Microsoft.ContainerService/managedClusters/${args.clusterName}`;
const { name, namespace, uid } = options.object.metadata;
const { selector } = options.object.spec;
const params = {
kind: options.kind,
metadata: { name, namespace, uid },
spec: {
selector,
},
};
return new URL(
`${basePath}/${encodeURIComponent(path)}/resource/${encodeURIComponent(
JSON.stringify(params),
)}`,
);
}