diff --git a/.changeset/strange-stingrays-enjoy.md b/.changeset/strange-stingrays-enjoy.md new file mode 100644 index 0000000000..26b42a5611 --- /dev/null +++ b/.changeset/strange-stingrays-enjoy.md @@ -0,0 +1,7 @@ +--- +'@backstage/catalog-model': minor +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-backend': patch +--- + +k8s-plugin: refactor approach to use annotation based label-selector diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 5fd4f88537..680674f16e 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -32,15 +32,6 @@ const schema = yup.object>({ implementsApis: yup.array(yup.string().required()).notRequired(), providesApis: yup.array(yup.string().required()).notRequired(), consumesApis: yup.array(yup.string().required()).notRequired(), - kubernetes: yup - .object({ - selector: yup - .object({ - matchLabels: yup.object().required(), - }) - .required(), - }) - .notRequired(), }) .required(), }); @@ -60,13 +51,6 @@ export interface ComponentEntityV1alpha1 extends Entity { implementsApis?: string[]; providesApis?: string[]; consumesApis?: string[]; - kubernetes?: { - selector: { - matchLabels: { - [key: string]: string; - }; - }; - }; }; } diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md index bf3e7d261a..5f541bb61d 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/README.md +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -42,7 +42,7 @@ kubernetes: Mac copy to clipboard: ``` -kubectl get secret $(kubectl get sa dice-roller -o=json | jq -r .secrets[0].name) -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy +kubectl get secret $(kubectl get sa dice-roller -o=json | jq -r '.secrets[0].name') -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy ``` Paste into `app-config.local.yaml` `kubernetes.clusters[0].serviceAccountToken` diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 32a09cb496..5677fa2d0d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Config, ConfigReader } from '../../../../packages/config/src'; +import { Config, ConfigReader } from '@backstage/config'; import { getCombinedClusterDetails } from './index'; describe('getCombinedClusterDetails', () => { diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index ea418768b4..9e6558510c 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -15,7 +15,7 @@ */ import { ClusterDetails, ClusterLocatorMethod } from '..'; -import { Config } from '../../../../packages/config/src'; +import { Config } from '@backstage/config'; import { ConfigClusterLocator } from './ConfigClusterLocator'; export { ConfigClusterLocator } from './ConfigClusterLocator'; diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index adebebe1c5..5c6513e57c 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -16,7 +16,6 @@ import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; -import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; import { ObjectFetchParams } from '..'; const TEST_SERVICE_ID = 'my-service'; @@ -25,26 +24,6 @@ const fetchObjectsForService = jest.fn(); const getClustersByServiceId = jest.fn(); -const goodEntity: ComponentEntityV1alpha1 = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'test-component', - }, - spec: { - type: 'service', - lifecycle: 'production', - owner: 'joe', - kubernetes: { - selector: { - matchLabels: { - 'backstage.io/test-label': 'test-component', - }, - }, - }, - }, -}; - const mockFetch = (mock: jest.Mock) => { mock.mockImplementation((params: ObjectFetchParams) => Promise.resolve({ @@ -111,7 +90,24 @@ describe('handleGetKubernetesObjectsForService', () => { getClustersByServiceId, }, getVoidLogger(), - { entity: goodEntity }, + { + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', + }, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', + }, + }, + }, ); expect(getClustersByServiceId.mock.calls.length).toBe(1); @@ -186,10 +182,25 @@ describe('handleGetKubernetesObjectsForService', () => { }, getVoidLogger(), { - entity: goodEntity, auth: { google: 'google_token_123', }, + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', + }, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', + }, + }, }, ); diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 82220f5d6c..2dd1e32b96 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -15,7 +15,6 @@ */ import { Logger } from 'winston'; -import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; import { KubernetesRequestBody, ClusterDetails, @@ -47,18 +46,6 @@ const DEFAULT_OBJECTS = new Set([ 'ingresses', ]); -function parseLabelSelector(entity: ComponentEntityV1alpha1): string { - const matchLabels = entity?.spec?.kubernetes?.selector?.matchLabels; - if (matchLabels) { - // TODO: figure out how to convert the selector to the full query param from the yaml - // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) - return Object.keys(matchLabels) - .map(key => `${key}=${matchLabels[key.toString()]}`) - .join(','); - } - return ''; -} - // Fans out the request to all clusters that the service lives in, aggregates their responses together export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async ( serviceId, @@ -92,7 +79,10 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic .join(', ')}]`, ); - const labelSelector = parseLabelSelector(requestBody.entity); + const labelSelector: string = + requestBody.entity?.metadata?.annotations?.[ + 'backstage.io/kubernetes-label-selector' + ] || `backstage.io/kubernetes-id=${requestBody.entity.metadata.name}`; return Promise.all( clusterDetailsDecoratedForAuth.map(clusterDetails => { diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index b2a5ef432c..9f0b90c032 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -23,7 +23,7 @@ import { V1ReplicaSet, V1Service, } from '@kubernetes/client-node'; -import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; export interface ClusterDetails { name: string; @@ -36,7 +36,7 @@ export interface KubernetesRequestBody { auth?: { google?: string; }; - entity: ComponentEntityV1alpha1; + entity: Entity; } export interface ClusterObjects { diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index 8f3542a46e..a343764b70 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -17,23 +17,7 @@ It is only meant for local development, and the setup for it can be found inside There are two ways to surface your kubernetes components as part of an entity. The label selector takes precedence over the annotation/service id. -### Full label selector - -#### Adding the entity label selector to the spec - -In order for Backstage to detect that an entity has kubernetes components the `kubernetes.selector` must have a valid selector. (Currently only matchLabels is supported) - -```yaml -spec: - kubernetes: - selector: - matchLabels: - someKey: someValue - other-key: other-value - app.kubernetes.io/name: dice-roller -``` - -### Common `backstage.io/kubernetes-id` label on objects +### Common `backstage.io/kubernetes-id` label #### Adding the entity annotation @@ -53,3 +37,14 @@ as a part of an entity, Kubernetes components must be labeled with the following ```yaml 'backstage.io/kubernetes-id': ``` + +### label selector query annotation + +#### Adding a label selector query annotation + +You can write your own custom label selector query that backstage will use to lookup the objects (similar to `kubectl --selector="your query here"`) +review the documentation [here](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) for more info + +```yaml +'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end' +``` diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 14cda3b5f7..65c1102d47 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -26,7 +26,7 @@ import { TabbedCard, useApi, } from '@backstage/core'; -import { ComponentEntityV1alpha1, Entity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../../api/types'; import { KubernetesRequestBody, @@ -121,7 +121,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { (async () => { // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider let requestBody: KubernetesRequestBody = { - entity: entity as ComponentEntityV1alpha1, + entity, }; for (const authProviderStr of authProviders) { // Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously