refactor(k8s-plugin): update to use annotation based label selector
add changeset
This commit is contained in:
@@ -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
|
||||
@@ -32,15 +32,6 @@ const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
|
||||
implementsApis: yup.array(yup.string().required()).notRequired(),
|
||||
providesApis: yup.array(yup.string().required()).notRequired(),
|
||||
consumesApis: yup.array(yup.string().required()).notRequired(),
|
||||
kubernetes: yup
|
||||
.object<any>({
|
||||
selector: yup
|
||||
.object<any>({
|
||||
matchLabels: yup.object<any>().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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
+34
-23
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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<KubernetesObjectTypes>([
|
||||
'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 => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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': <ENTITY_NAME>
|
||||
```
|
||||
|
||||
### 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'
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user