refactor(k8s-plugin): update to use annotation based label selector

add changeset
This commit is contained in:
Moustafa Baiou
2020-12-01 00:31:13 -05:00
parent 6bc7bb2ffb
commit bcc211a081
10 changed files with 64 additions and 77 deletions
+7
View File
@@ -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';
@@ -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 {
+12 -17
View File
@@ -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