Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc of kubernetes entity provider #51

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@ jobs:
push: false
tags: roadiehq/broker:base
context: dockerfiles
file: dockerfiles/base.Dockerfile
file: dockerfiles/plugins/base.Dockerfile
- name: Build Kubernetes
uses: docker/build-push-action@v4
with:
push: false
file: dockerfiles/Dockerfile
context: dockerfiles/kubernetes
file: dockerfiles/plugins/Dockerfile
context: dockerfiles/plugins/kubernetes
tags: roadiehq/broker:pr-${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }}-kubernetes
- name: Build Sonarqube
uses: docker/build-push-action@v4
with:
push: false
file: dockerfiles/Dockerfile
context: dockerfiles/sonarqube
file: dockerfiles/plugins/Dockerfile
context: dockerfiles/plugins/sonarqube
tags: roadiehq/broker:pr-${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }}-sonarqube
- name: Build Jenkins
uses: docker/build-push-action@v4
with:
push: false
file: dockerfiles/Dockerfile
context: dockerfiles/jenkins
file: dockerfiles/plugins/Dockerfile
context: dockerfiles/plugins/jenkins
tags: roadiehq/broker:pr-${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }}-jenkins
14 changes: 7 additions & 7 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,25 @@ jobs:
push: true
tags: roadiehq/broker:base
context: dockerfiles
file: dockerfiles/base.Dockerfile
file: dockerfiles/plugins/base.Dockerfile
- name: Build Kubernetes
uses: docker/build-push-action@v4
with:
push: true
file: dockerfiles/Dockerfile
context: dockerfiles/kubernetes
file: dockerfiles/plugins/Dockerfile
context: dockerfiles/plugins/kubernetes
tags: roadiehq/broker:${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }}-kubernetes,roadiehq/broker:kubernetes
- name: Build Sonarqube
uses: docker/build-push-action@v4
with:
push: true
file: dockerfiles/Dockerfile
context: dockerfiles/sonarqube
file: dockerfiles/plugins/Dockerfile
context: dockerfiles/plugins/sonarqube
tags: roadiehq/broker:${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }}-sonarqube,roadiehq/broker:sonarqube
- name: Build Jenkins
uses: docker/build-push-action@v4
with:
push: true
file: dockerfiles/Dockerfile
context: dockerfiles/jenkins
file: dockerfiles/plugins/Dockerfile
context: dockerfiles/plugins/jenkins
tags: roadiehq/broker:${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }}-jenkins,roadiehq/broker:jenkins
2 changes: 2 additions & 0 deletions bin/kubernetes-entity-provider
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../build/lib/providers/kubernetes/start')
8 changes: 8 additions & 0 deletions dockerfiles/entity-providers/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:18-alpine
MAINTAINER Roadie
WORKDIR /usr/src/app
COPY package.json .
RUN npm install && npm install typescript -g
COPY . .
RUN yarn build
CMD ["node", "./bin/kubernetes-entity-provider"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need the ability to configure the entity provider entity mappings and the broker client server url and token.

2 changes: 1 addition & 1 deletion dockerfiles/Dockerfile → dockerfiles/plugins/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ RUN node --version
RUN apt-get update && apt-get install -y ca-certificates
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH=$PATH:/home/node/.npm-global/bin
RUN npm install --global snyk-broker
RUN npm install --global snyk-broker@4.157.5

# Removing [email protected] (setheader transitive) to satisfy some scanners still reporting this false positive
RUN rm -rf /home/node/.npm-global/lib/node_modules/snyk-broker/node_modules/setheader/node_modules/debug
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"build/types/**/*",
"config/accept.json"
],
"bin": {
"kubernetes-entity-provider": "./bin/kubernetes-entity-provider"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be maybe better in a format like agent-entity-provider --variant kubernetes or something

},
"scripts": {
"test": "yarn build && mocha --inspect --require source-map-support/register --bail ./build/test/",
"build": "rimraf build && tsc && tsc-alias && yarn copy-files",
Expand All @@ -39,6 +42,8 @@
"@backstage/catalog-model": "^1.4.0",
"@backstage/plugin-catalog-backend": "^1.9.1",
"@backstage/plugin-catalog-node": "^1.3.6",
"@kubernetes/client-node": "^0.19.0",
"nunjucks": "^3.2.4",
"@changesets/cli": "^2.26.2",
"express": "^4.17.1",
"node-fetch": "^2.6.11",
Expand All @@ -53,6 +58,7 @@
"@types/node-fetch": "^2.6.2",
"@types/sinon": "^10.0.13",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@types/nunjucks": "^3.2.4",
"@typescript-eslint/parser": "^4.25.0",
"chai": "^4.3.4",
"conventional-changelog-cli": "^2.1.1",
Expand Down
98 changes: 98 additions & 0 deletions src/lib/providers/kubernetes/EntityProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {EntityProviderHandler} from "$/types";
import {ComponentEntity, Entity, SystemEntity} from "@backstage/catalog-model";
import {isError} from "@backstage/errors";
import {getRootLogger} from "@/logger";
import {KubeConfig, CoreV1Api, V1Namespace, V1ServiceList, CustomObjectsApi} from '@kubernetes/client-node';
import {renderString} from 'nunjucks';
import yaml from 'yaml'

const ROADIE_OWNER_ANNOTATION = 'roadie.io/owner'
const ROADIE_LIFECYCLE_ANNOTATION = 'roadie.io/lifecycle'

type KubernetesEntityProviderOptions = {
namespaceFilters?: {
labelSelector?: string
fieldSelector?: string
},
objectMappings: Array<{
group?: string,
version?: string,
plural: string,
labelSelector?: string,
fieldSelector?: string,
template: string
}>
}

export class KubernetesEntityProvider {
private readonly opts: KubernetesEntityProviderOptions;
private readonly kc: KubeConfig;

constructor(opts: KubernetesEntityProviderOptions) {
this.opts = opts
this.kc = new KubeConfig();
this.kc.loadFromDefault()
}

private async listNamespaces() {
const k8sApi = this.kc.makeApiClient(CoreV1Api);
const {body: namespaces} = await k8sApi.listNamespace(undefined, undefined, undefined, this.opts.namespaceFilters?.fieldSelector, this.opts.namespaceFilters?.labelSelector);
return namespaces
}

private async listPods(namespace: string, labelSelector?: string, fieldSelector?: string) {
const k8sApi = this.kc.makeApiClient(CoreV1Api);
const {body: pods} = await k8sApi.listNamespacedPod(namespace, undefined, undefined, undefined, fieldSelector, labelSelector);
return pods
}

handler: EntityProviderHandler = async (emit) => {
const logger = getRootLogger();
logger.info(`handling entity request`)
try {
const entities: Entity[] = []

const k8sCustomObjectsApi = this.kc.makeApiClient(CustomObjectsApi)
const namespaces = await this.listNamespaces();

await Promise.all(namespaces.items.map(async namespace => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think flatMapping should probably achieve the same things here, depending how you decide to modify the loop below

await Promise.all(this.opts.objectMappings.map(async objectMapping => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await for...of probably easier to read here since you are mutating the external var already

if (namespace.metadata?.name) {
if (objectMapping.plural === 'pods') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the core api, I am only allow pods to be collected, in the ultimate version we would allow collecting any of the other core api resources.

const list = await this.listPods(namespace.metadata?.name, objectMapping.labelSelector, objectMapping.fieldSelector);
list.items.forEach(pod => {
const entityString = renderString(objectMapping.template, pod);
const items = yaml.parseAllDocuments(entityString).map(doc => doc.toJSON())
entities.push(...items)
})
}

if (objectMapping.group && objectMapping.version) {
const { response } = await k8sCustomObjectsApi.listNamespacedCustomObject(
objectMapping.group,
objectMapping.version,
namespace.metadata.name,
objectMapping.plural
);
(response as any).body.items.forEach((customResource: any) => {
const entityString = renderString(objectMapping.template, customResource);
const items = yaml.parseAllDocuments(entityString).map(doc => doc.toJSON())
entities.push(...items)
})
}
}
}))
}));

await emit({
type: "full",
entities: entities.map(entity => ({
entity,
locationKey: 'test'
}))
})
} catch (e: any) {
logger.info(`handling entity request failed: ${e}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need better error handling here.

}
}
}
15 changes: 15 additions & 0 deletions src/lib/providers/kubernetes/accept.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"private": [
{
"method": "GET",
"path": "/agent-provider/*",
"origin": "http://localhost:7342"
}
],
"public": [
{
"method": "any",
"path": "/*"
}
]
}
74 changes: 74 additions & 0 deletions src/lib/providers/kubernetes/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env ts-node
import {createRoadieAgentEntityProvider, RoadieAgent} from "../../";
import {KubernetesEntityProvider} from "@/providers/kubernetes/EntityProvider";
import {getRootLogger} from "@/logger";
import yaml from 'yaml';

const podTemplate = `
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: {{ metadata.name }}
namespace: {{ metadata.namespace }}
spec:
type: service
lifecycle: unknown
owner: test
`;

const helmChartTemplate = `
---
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: {{ metadata.name }}
namespace: {{ metadata.namespace }}
spec:
type: 'helm-chart'
lifecycle: unknown
owner: test
`;

const main = async () => {
const logger = getRootLogger();
const brokerServer = process.env.BROKER_SERVER || 'http://localhost:7341';

RoadieAgent.fromConfig({
server: brokerServer,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings here will need to be provided from the outside.. and some of the settings should be defaulted in the implementation of fromConfig.

port: 7342,
identifier: 'kubernetes-entity-agent',
accept: '/Users/brianfletcher/git-repos/roadie-agent/config/accept.json',
agentPort: 7044
})
.addEntityProvider(
createRoadieAgentEntityProvider({
name: 'kube',
handler: new KubernetesEntityProvider({
namespaceFilters: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these settings should be configurable with sensible defaults.


},
objectMappings: [{
template: podTemplate,
plural: 'pods',
},
{
template: helmChartTemplate,
version: 'v1beta1',
group: 'source.toolkit.fluxcd.io',
plural: 'helmcharts',
}]}).handler
}),
)
.start();
}

void (async () => {
try {
await main()
} catch(err) {
console.error('Something bad')
}
})()


5 changes: 4 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ export interface AgentConfiguration<
type: AgentConfigurationType;
name: string;
}

export type EntityProviderHandler = (emit: (mutation: EntityProviderMutation) => Promise<void>) => void;

export interface EntityProviderAgentConfiguration
extends AgentConfiguration<'entity-provider'> {
handler: (emit: (mutation: EntityProviderMutation) => Promise<void>) => void;
handler: EntityProviderHandler;
}
export type TechInsightsDataSourceAgentConfiguration =
AgentConfiguration<'tech-insights-data-source'>;
Expand Down
Loading