diff --git a/.changeset/quiet-pots-agree.md b/.changeset/quiet-pots-agree.md new file mode 100644 index 0000000000..c42ef32c3a --- /dev/null +++ b/.changeset/quiet-pots-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-node': patch +--- + +Accept auth credentials to get kubernetes clusters diff --git a/.changeset/thirty-donkeys-provide.md b/.changeset/thirty-donkeys-provide.md new file mode 100644 index 0000000000..b1516f2073 --- /dev/null +++ b/.changeset/thirty-donkeys-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Pass user credentials when calling catalog get entities api. diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 60331ca044..aabdba355b 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -6,6 +6,7 @@ import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; import { AuthMetadata as AuthMetadata_2 } from '@backstage/plugin-kubernetes-node'; import { AuthService } from '@backstage/backend-plugin-api'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; @@ -212,6 +213,9 @@ export class KubernetesBuilder { // (undocumented) protected fetchClusterDetails( clusterSupplier: KubernetesClustersSupplier_2, + options: { + credentials: BackstageCredentials; + }, ): Promise; // (undocumented) protected getAuthStrategyMap(): { diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index d6bc64221d..5ce316099e 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -23,6 +23,7 @@ import { } from '@backstage/plugin-kubernetes-common'; import { CatalogClusterLocator } from './CatalogClusterLocator'; import { CatalogApi } from '@backstage/catalog-client'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; const mockCatalogApi = { getEntityByRef: jest.fn(), @@ -77,29 +78,46 @@ describe('CatalogClusterLocator', () => { items: [], }), } as Partial as CatalogApi; + const auth = mockServices.auth(); - const clusterSupplier = - CatalogClusterLocator.fromConfig(emptyMockCatalogApi); + const clusterSupplier = CatalogClusterLocator.fromConfig( + emptyMockCatalogApi, + auth, + ); - const result = await clusterSupplier.getClusters(); + const credentials = mockCredentials.user(); + + const result = await clusterSupplier.getClusters({ credentials }); expect(result).toHaveLength(0); expect(result).toStrictEqual([]); }); it('returns the cluster details provided by annotations', async () => { - const clusterSupplier = CatalogClusterLocator.fromConfig(mockCatalogApi); + const auth = mockServices.auth(); + const clusterSupplier = CatalogClusterLocator.fromConfig( + mockCatalogApi, + auth, + ); - const result = await clusterSupplier.getClusters(); + const credentials = mockCredentials.user(); + + const result = await clusterSupplier.getClusters({ credentials }); expect(result).toHaveLength(2); expect(result[0]).toMatchSnapshot(); }); it('returns the aws cluster details provided by annotations', async () => { - const clusterSupplier = CatalogClusterLocator.fromConfig(mockCatalogApi); + const auth = mockServices.auth(); + const clusterSupplier = CatalogClusterLocator.fromConfig( + mockCatalogApi, + auth, + ); - const result = await clusterSupplier.getClusters(); + const credentials = mockCredentials.user(); + + const result = await clusterSupplier.getClusters({ credentials }); expect(result).toHaveLength(2); expect(result[1]).toMatchSnapshot(); diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index a2c5f0376a..76268457c5 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + AuthService, + BackstageCredentials, +} from '@backstage/backend-plugin-api'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { CATALOG_FILTER_EXISTS, CatalogApi } from '@backstage/catalog-client'; import { @@ -34,16 +38,23 @@ function isObject(obj: unknown): obj is JsonObject { export class CatalogClusterLocator implements KubernetesClustersSupplier { private catalogClient: CatalogApi; + private auth: AuthService; - constructor(catalogClient: CatalogApi) { + constructor(catalogClient: CatalogApi, auth: AuthService) { this.catalogClient = catalogClient; + this.auth = auth; } - static fromConfig(catalogApi: CatalogApi): CatalogClusterLocator { - return new CatalogClusterLocator(catalogApi); + static fromConfig( + catalogApi: CatalogApi, + auth: AuthService, + ): CatalogClusterLocator { + return new CatalogClusterLocator(catalogApi, auth); } - async getClusters(): Promise { + async getClusters(options: { + credentials: BackstageCredentials; + }): Promise { const apiServerKey = `metadata.annotations.${ANNOTATION_KUBERNETES_API_SERVER}`; const apiServerCaKey = `metadata.annotations.${ANNOTATION_KUBERNETES_API_SERVER_CA}`; const authProviderKey = `metadata.annotations.${ANNOTATION_KUBERNETES_AUTH_PROVIDER}`; @@ -56,9 +67,21 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { [authProviderKey]: CATALOG_FILTER_EXISTS, }; - const clusters = await this.catalogClient.getEntities({ - filter: [filter], - }); + const clusters = await this.catalogClient.getEntities( + { + filter: [filter], + }, + options.credentials + ? { + token: ( + await this.auth.getPluginRequestToken({ + onBehalfOf: options.credentials, + targetPluginId: 'catalog', + }) + ).token, + } + : undefined, + ); return clusters.items.map(entity => { const annotations = entity.metadata.annotations!; const clusterDetails: ClusterDetails = { diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index fcdac32ecf..1ac2a49737 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -21,6 +21,7 @@ import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernete import { getCombinedClusterSupplier } from './index'; import { ClusterDetails } from '../types/types'; import { AuthenticationStrategy, DispatchStrategy } from '../auth'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; describe('getCombinedClusterSupplier', () => { let catalogApi: CatalogApi; @@ -57,13 +58,18 @@ describe('getCombinedClusterSupplier', () => { presentAuthMetadata: jest.fn(), }; + const auth = mockServices.auth(); + const credentials = mockCredentials.user(); + const clusterSupplier = getCombinedClusterSupplier( config, catalogApi, mockStrategy, getVoidLogger(), + undefined, + auth, ); - const result = await clusterSupplier.getClusters(); + const result = await clusterSupplier.getClusters({ credentials }); expect(result).toStrictEqual([ { @@ -96,12 +102,16 @@ describe('getCombinedClusterSupplier', () => { 'ctx', ); + const auth = mockServices.auth(); + expect(() => getCombinedClusterSupplier( config, catalogApi, new DispatchStrategy({ authStrategyMap: {} }), getVoidLogger(), + undefined, + auth, ), ).toThrow( new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'), @@ -151,13 +161,19 @@ describe('getCombinedClusterSupplier', () => { validateEntity: jest.fn(), }; + const auth = mockServices.auth(); + const credentials = mockCredentials.user(); + const clusterSupplier = getCombinedClusterSupplier( config, catalogApi, mockStrategy, logger, + undefined, + auth, ); - await clusterSupplier.getClusters(); + + await clusterSupplier.getClusters({ credentials }); expect(warn).toHaveBeenCalledWith(`Duplicate cluster name 'cluster'`); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index 2dfd3178f7..cab1d1df1f 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -24,6 +24,10 @@ import { ConfigClusterLocator } from './ConfigClusterLocator'; import { GkeClusterLocator } from './GkeClusterLocator'; import { CatalogClusterLocator } from './CatalogClusterLocator'; import { LocalKubectlProxyClusterLocator } from './LocalKubectlProxyLocator'; +import { + AuthService, + BackstageCredentials, +} from '@backstage/backend-plugin-api'; class CombinedClustersSupplier implements KubernetesClustersSupplier { constructor( @@ -31,9 +35,11 @@ class CombinedClustersSupplier implements KubernetesClustersSupplier { readonly logger: Logger, ) {} - async getClusters(): Promise { + async getClusters(options: { + credentials: BackstageCredentials; + }): Promise { const clusters = await Promise.all( - this.clusterSuppliers.map(supplier => supplier.getClusters()), + this.clusterSuppliers.map(supplier => supplier.getClusters(options)), ) .then(res => { return res.flat(); @@ -67,6 +73,7 @@ export const getCombinedClusterSupplier = ( authStrategy: AuthenticationStrategy, logger: Logger, refreshInterval: Duration | undefined = undefined, + auth: AuthService, ): KubernetesClustersSupplier => { const clusterSuppliers = rootConfig .getConfigArray('kubernetes.clusterLocatorMethods') @@ -74,7 +81,7 @@ export const getCombinedClusterSupplier = ( const type = clusterLocatorMethod.getString('type'); switch (type) { case 'catalog': - return CatalogClusterLocator.fromConfig(catalogClient); + return CatalogClusterLocator.fromConfig(catalogClient, auth); case 'localKubectlProxy': return new LocalKubectlProxyClusterLocator(); case 'config': diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index c63d039ce9..aca8ec229b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -67,6 +67,7 @@ import { KubernetesProxy } from './KubernetesProxy'; import { createLegacyAuthAdapters } from '@backstage/backend-common'; import { AuthService, + BackstageCredentials, DiscoveryService, HttpAuthService, } from '@backstage/backend-plugin-api'; @@ -257,12 +258,14 @@ export class KubernetesBuilder { refreshInterval: Duration, ): KubernetesClustersSupplier { const config = this.env.config; + const { auth } = createLegacyAuthAdapters(this.env); this.clusterSupplier = getCombinedClusterSupplier( config, this.env.catalogApi, new DispatchStrategy({ authStrategyMap: this.getAuthStrategyMap() }), this.env.logger, refreshInterval, + auth, ); return this.clusterSupplier; @@ -385,8 +388,11 @@ export class KubernetesBuilder { } }); - router.get('/clusters', async (_, res) => { - const clusterDetails = await this.fetchClusterDetails(clusterSupplier); + router.get('/clusters', async (req, res) => { + const credentials = await httpAuth.credentials(req); + const clusterDetails = await this.fetchClusterDetails(clusterSupplier, { + credentials, + }); res.json({ items: clusterDetails.map(cd => { const oidcTokenProvider = @@ -438,8 +444,9 @@ export class KubernetesBuilder { protected async fetchClusterDetails( clusterSupplier: KubernetesClustersSupplier, + options: { credentials: BackstageCredentials }, ) { - const clusterDetails = await clusterSupplier.getClusters(); + const clusterDetails = await clusterSupplier.getClusters(options); this.env.logger.info( `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`, diff --git a/plugins/kubernetes-node/api-report.md b/plugins/kubernetes-node/api-report.md index 509416fa4a..36212ccf24 100644 --- a/plugins/kubernetes-node/api-report.md +++ b/plugins/kubernetes-node/api-report.md @@ -4,6 +4,7 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; import { Entity } from '@backstage/catalog-model'; @@ -87,7 +88,9 @@ export const kubernetesAuthStrategyExtensionPoint: ExtensionPoint; + getClusters(options?: { + credentials: BackstageCredentials; + }): Promise; } // @public diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index 665b06178a..b89811ae99 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { CustomResourceMatcher, @@ -134,7 +135,9 @@ export interface KubernetesClustersSupplier { * Implementations _should_ cache the clusters and refresh them periodically, * as getClusters is called whenever the list of clusters is needed. */ - getClusters(): Promise; + getClusters(options?: { + credentials: BackstageCredentials; + }): Promise; } /**