From 74fbecbbf93839293e3ede0d3e6cf37d1a2110db Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Mon, 26 Feb 2024 22:15:45 +0100 Subject: [PATCH 01/43] (WIP) add first forms --- .../components/settings/form-buildpacks.vue | 130 +++++++ .../src/components/settings/form-general.vue | 234 +++++++++++++ .../src/components/settings/form-podsizes.vue | 118 +++++++ client/src/components/settings/form.vue | 319 ++---------------- 4 files changed, 508 insertions(+), 293 deletions(-) create mode 100644 client/src/components/settings/form-buildpacks.vue create mode 100644 client/src/components/settings/form-general.vue create mode 100644 client/src/components/settings/form-podsizes.vue diff --git a/client/src/components/settings/form-buildpacks.vue b/client/src/components/settings/form-buildpacks.vue new file mode 100644 index 00000000..c042e012 --- /dev/null +++ b/client/src/components/settings/form-buildpacks.vue @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/settings/form-general.vue b/client/src/components/settings/form-general.vue new file mode 100644 index 00000000..23da5b09 --- /dev/null +++ b/client/src/components/settings/form-general.vue @@ -0,0 +1,234 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/settings/form-podsizes.vue b/client/src/components/settings/form-podsizes.vue new file mode 100644 index 00000000..091635a8 --- /dev/null +++ b/client/src/components/settings/form-podsizes.vue @@ -0,0 +1,118 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/settings/form.vue b/client/src/components/settings/form.vue index c9727466..89e7a5c5 100644 --- a/client/src/components/settings/form.vue +++ b/client/src/components/settings/form.vue @@ -1,306 +1,32 @@ + + + \ No newline at end of file diff --git a/client/src/components/settings/form.vue b/client/src/components/settings/form.vue index 69f1d88b..4e812916 100644 --- a/client/src/components/settings/form.vue +++ b/client/src/components/settings/form.vue @@ -11,7 +11,7 @@ Podsizes Buildpacks Secrets - Templates + Templates Notifications @@ -32,6 +32,10 @@ + + + + Date: Thu, 7 Mar 2024 12:50:59 -0800 Subject: [PATCH 06/43] New service account annotation feature. --- client/src/components/apps/appstats.vue | 24 ++ client/src/components/apps/form.vue | 91 ++++- server/src/kubero.ts | 16 + server/src/modules/application.ts | 16 +- server/src/modules/kubectl.ts | 60 ++++ server/src/routes/apps.ts | 20 +- server/src/routes/pipelines.ts | 13 +- server/src/types.ts | 23 +- server/swagger.json | 419 +++++++++++++++++++++++- 9 files changed, 656 insertions(+), 26 deletions(-) diff --git a/client/src/components/apps/appstats.vue b/client/src/components/apps/appstats.vue index 0afea99e..d37e0713 100644 --- a/client/src/components/apps/appstats.vue +++ b/client/src/components/apps/appstats.vue @@ -110,6 +110,28 @@ +
+

Service Acccount Annotations

+ + + + + Name + + + Value + + + + + + {{ saAnnotation.name }} + {{ saAnnotation.value }} + + + +

Volumes

@@ -300,6 +322,7 @@ interface Spec { podsize: PodSize; autoscale: boolean; envVars: any[]; + saAnnotations: any[]; extraVolumes: any[]; cronjobs: any[]; addons: any[]; @@ -356,6 +379,7 @@ type appData = { podsize: PodSize, autoscale: boolean, envVars: any[], + saAnnotations: any[], extraVolumes: any[], cronjobs: any[], addons: any[], diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index 437249e2..ca70250d 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -684,6 +684,68 @@ + + + + ServiceAcccount Annotations + + {{ sAAnnotations }} + + + + + + + + + + + mdi-minus + + + + + + + + + + mdi-plus + + + + + + + Resources @@ -1012,7 +1074,7 @@ import { defineComponent } from 'vue' import { useKuberoStore } from '../../stores/kubero' import { mapState } from 'pinia' import Breadcrumbs from "../breadcrumbs.vue"; - +import { remove } from "lodash"; type App = { name: string, @@ -1127,6 +1189,10 @@ type EnvVar = { value: string, } +type SAAnnotations { + [key: string]: string; +} + export default defineComponent({ props: { pipeline: { @@ -1140,7 +1206,7 @@ export default defineComponent({ app: { type: String, default: "new" - } + }, }, data () { return { @@ -1291,6 +1357,7 @@ export default defineComponent({ envvars: [ //{ name: '', value: '' }, ] as EnvVar[], + sAAnnotations: {} as SAAnnotations, containerPort: 8080, podsize: '', podsizes: [ @@ -1517,6 +1584,7 @@ export default defineComponent({ this.docker.tag = response.data.image.tag; this.envvars = response.data.envVars; + this.sAAnnotations = response.data.sAAnnotations; this.extraVolumes = response.data.extraVolumes; this.cronjobs = response.data.cronjobs; this.addons = response.data.addons; @@ -1534,6 +1602,9 @@ export default defineComponent({ if (this.envvars.length > 0) { this.panel.push(1) } + if (Object.keys(this.sAAnnotations).length > 0) { + this.panel.push(1) + } if (this.extraVolumes.length > 0) { this.panel.push(3) } @@ -1679,6 +1750,9 @@ export default defineComponent({ if (response.data.spec.envVars.length > 0) { this.panel.push(1) } + if (Object.keys(response.data.sAAnnotations).length > 0) { + this.panel.push(2) + } if (response.data.spec.extraVolumes.length > 0) { this.panel.push(3) } @@ -1705,6 +1779,7 @@ export default defineComponent({ this.autodeploy = response.data.spec.autodeploy; this.domain = response.data.spec.domain; this.envvars = response.data.spec.envVars; + this.sAAnnotations = response.data.sAAnnotations; this.extraVolumes = response.data.spec.extraVolumes; this.containerPort = response.data.spec.image.containerPort; this.podsize = response.data.spec.podsize; @@ -1807,6 +1882,8 @@ export default defineComponent({ domain: this.domain, ssl: this.ssl, envvars: this.envvars, + // loop through serviceaccount annotations and convert to object + sAAnnotations: this.sAAnnotations, podsize: this.podsize, autoscale: this.autoscale, web: { @@ -1898,6 +1975,7 @@ export default defineComponent({ domain: this.domain.toLowerCase(), ssl: this.ssl, envvars: this.envvars, + sAAnnotations: this.sAAnnotations, podsize: this.podsize, autoscale: this.autoscale, web: { @@ -1971,6 +2049,15 @@ export default defineComponent({ } } }, + addSAAnnotationLine() { + this.sAAnnotations = { ...this.sAAnnotations, '': ''}; + }, + removeSAAnnotationLine(key: any) { + if (key in this.sAAnnotations) { + + delete this.sAAnnotations[key]; + } + }, addVolumeLine() { this.extraVolumes.push({ name: 'example-volume', diff --git a/server/src/kubero.ts b/server/src/kubero.ts index da077df3..4b7ae809 100644 --- a/server/src/kubero.ts +++ b/server/src/kubero.ts @@ -386,6 +386,8 @@ export class Kubero { const contextName = this.getContext(app.pipeline, app.phase); if (contextName) { await this.kubectl.updateApp(app, resourceVersion, contextName); + // TODO: Update serviceaccountannotations + await this.kubectl.updateServiceAccountAnnotations(app, resourceVersion, contextName); // IMPORTANT TODO : Update this.appStateList !! this.kubectl.createEvent('Normal', 'Updated', 'app.updated', 'updated app: '+app.name+' in '+ app.pipeline+' phase: '+app.phase); this.audit?.log({ @@ -461,14 +463,27 @@ export class Kubero { public async getApp(pipelineName: string, phaseName: string, appName: string) { debug.debug('get App: '+appName+' in '+ pipelineName+' phase: '+phaseName); const contextName = this.getContext(pipelineName, phaseName); + if (contextName) { let app = await this.kubectl.getApp(pipelineName, phaseName, appName, contextName); return app; } } + // get a app in a pipeline and phase + public async getServiceAccount(pipelineName: string, phaseName: string, appName: string) { + debug.debug('get App: '+appName+' in '+ pipelineName+' phase: '+phaseName); + const contextName = this.getContext(pipelineName, phaseName); + + if (contextName) { + let app = await this.kubectl.getServiceAccount(pipelineName, phaseName, appName, contextName); + return app; + } + } + public async getTemplate(pipelineName: string, phaseName: string, appName: string ) { const app = await this.getApp(pipelineName, phaseName, appName); + const a = app?.body as IKubectlApp; let t = new KubectlTemplate(a.spec as IApp); @@ -830,6 +845,7 @@ export class Kubero { podsize: this.config.podSizeList[0], //TODO select from podsizelist autoscale: false, envVars: [], //TODO use custom env vars, + sAAnnotations: {}, //TODO use custom serviceaccount annotations extraVolumes: [], //TODO Not sure how to handlle extra Volumes on PR Apps image: { containerPort: 8080, //TODO use custom containerport diff --git a/server/src/modules/application.ts b/server/src/modules/application.ts index 76acd161..1d95a7ab 100644 --- a/server/src/modules/application.ts +++ b/server/src/modules/application.ts @@ -128,11 +128,7 @@ export class App implements IApp{ port: 80, type: 'ClusterIP' }; - private serviceAccount: { - annotations: {}, - create: true, - name: "", - }; + public sAAnnotations: Object private tolerations: []; constructor( @@ -153,6 +149,8 @@ export class App implements IApp{ this.envVars = app.envVars + this.sAAnnotations = app.sAAnnotations + this.extraVolumes = app.extraVolumes this.cronjobs = app.cronjobs @@ -210,11 +208,6 @@ export class App implements IApp{ port: 80, type: 'ClusterIP' }, - this.serviceAccount= { - annotations: {}, - create: true, - name: "", - }, this.tolerations= [] } } @@ -242,6 +235,7 @@ export class Template implements ITemplate{ public name: string public deploymentstrategy: 'git' | 'docker' public envVars: {}[] = [] + public sAAnnotations: {} public extraVolumes: IExtraVolume[] = [] public cronjobs: ICronjob[] = [] public addons: IAddon[] = [] @@ -274,6 +268,8 @@ export class Template implements ITemplate{ this.envVars = app.envVars + this.sAAnnotations = app.sAAnnotations + this.extraVolumes = app.extraVolumes this.cronjobs = app.cronjobs diff --git a/server/src/modules/kubectl.ts b/server/src/modules/kubectl.ts index bd07a152..b3616ec7 100644 --- a/server/src/modules/kubectl.ts +++ b/server/src/modules/kubectl.ts @@ -25,6 +25,7 @@ import { StorageV1Api, BatchV1Api, NetworkingV1Api, + V1ServiceAccount } from '@kubernetes/client-node' import { IPipeline, IKubectlPipeline, IKubectlPipelineList, IKubectlAppList, IKuberoConfig, Uptime} from '../types'; import { App, KubectlApp } from './application'; @@ -271,7 +272,30 @@ export class Kubectl { let namespace = pipelineName+'-'+phaseName; this.kc.setCurrentContext(context); + + /* + let serviceAccount = await this.coreV1Api.readNamespacedServiceAccount( + appName+'-kuberoapp', + namespace + ).catch(error => { + debug.log(error); + }); + update serviceAccount + if (serviceAccount && serviceAccount.body) { + serviceAccount.body.metadata = { + ...serviceAccount.body.metadata, + // update metadata here + }; + await this.coreV1Api.replaceNamespacedServiceAccount( + appName+'-kuberoapp', + namespace, + serviceAccount.body + ).catch(error => { + debug.log(error); + }); + } */ + let app = await this.customObjectsApi.getNamespacedCustomObject( "application.kubero.dev", "v1alpha1", @@ -285,6 +309,42 @@ export class Kubectl { return app; } + public async getServiceAccount(pipelineName: string, phaseName: string, appName: string, context: string) { + + let namespace = pipelineName+'-'+phaseName; + this.kc.setCurrentContext(context); + + let serviceAccount = await this.coreV1Api.readNamespacedServiceAccount( + appName+'-kuberoapp', + namespace + ).catch(error => { + debug.log(error); + }); + + return serviceAccount; + } + + //public async updateServiceAccountAnnotations(pipelineName: string, phaseName: string, appName: string, context: string) { + public async updateServiceAccountAnnotations(app: App, resourceVersion: string, context: string) { + let pipelineName = app.pipeline; + let phaseName = app.phase; + let appName = app.name; + let serviceAccount = await this.getServiceAccount(app.pipeline, app.phase, app.name, context); + let namespace = pipelineName+'-'+phaseName; + this.kc.setCurrentContext(context); + + if (serviceAccount && serviceAccount.body.metadata) { + serviceAccount.body.metadata.annotations = app.sAAnnotations as { [key: string]: string }; + await this.coreV1Api.replaceNamespacedServiceAccount( + appName+'-kuberoapp', + namespace, + serviceAccount.body + ).catch(error => { + debug.log(error); + }); + } + } + public async getAppsList(namespace: string, context: string): Promise { this.kc.setCurrentContext(context); try { diff --git a/server/src/routes/apps.ts b/server/src/routes/apps.ts index 92a7b165..17850735 100644 --- a/server/src/routes/apps.ts +++ b/server/src/routes/apps.ts @@ -87,6 +87,22 @@ Router.post('/cli/apps', bearerMiddleware, async function (req: Request, res: Re } } }, + saAnnotations: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "your.sa.annotation.enabled" + }, + value: { + type: "string", + example: "true" + } + } + } + }, image: { type: "object", properties: { @@ -212,6 +228,7 @@ function createApp(req: Request) : IApp { autoscale: req.body.autoscale, envVars: req.body.envvars, extraVolumes: req.body.extraVolumes, + sAAnnotations: req.body.sAAnnotations, image: { containerPort: req.body.image.containerport, repository: req.body.image.repository, @@ -272,6 +289,8 @@ Router.put('/pipelines/:pipeline/:phase/:app', authMiddleware, async function (r autoscale: req.body.autoscale, extraVolumes: req.body.extraVolumes, envVars: req.body.envvars, + //sAAnnotations: req.body.sAAnotations, + sAAnnotations: req.body.sAAnnotations, image: { containerPort: req.body.image.containerport, repository: req.body.image.repository, @@ -296,7 +315,6 @@ Router.put('/pipelines/:pipeline/:phase/:app', authMiddleware, async function (r const user = auth.getUser(req); req.app.locals.kubero.updateApp(app, req.body.resourceVersion, user); - res.send("updated"); }); diff --git a/server/src/routes/pipelines.ts b/server/src/routes/pipelines.ts index 01cd8f65..91c57f3e 100644 --- a/server/src/routes/pipelines.ts +++ b/server/src/routes/pipelines.ts @@ -4,6 +4,8 @@ import { IKubectlApp, IgitLink } from '../types'; import { IApp, IPipeline } from '../types'; import { App } from '../modules/application'; import { Webhooks } from '@octokit/webhooks'; +import { init } from '../socket'; +import get from 'lodash/get'; const Router = express.Router(); export const RouterPipelines = Router; @@ -275,11 +277,20 @@ Router.get('/pipelines/:pipeline/:phase/:app', authMiddleware, async function (r return; } + let serviceAccount = await req.app.locals.kubero.getServiceAccount(req.params.pipeline, req.params.phase, req.params.app); + if (serviceAccount == undefined) { + res.status(404); + res.send("not found"); + return; + } + + const b = serviceAccount.body.metadata.annotations; // TODO: this is not the right way to get annotations const a = new App(app.body.spec as IApp); res.send({ resourceVersion: app.body.metadata.resourceVersion, - spec: a + spec: a, + sAAnnotations: b }); } catch (error) { console.log(error); diff --git a/server/src/types.ts b/server/src/types.ts index ab9eccad..99b23cc9 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -14,7 +14,7 @@ export interface IApp { podsize: IPodSize, autoscale: boolean, envVars: {}[], - + sAAnnotations: {} image : { repository: string, tag: string, @@ -127,10 +127,12 @@ export interface IApp { } + export interface ITemplate { name: string, deploymentstrategy: 'git' | 'docker', envVars: {}[], + sAAnnotations: {}, image : { repository: string, tag: string, @@ -400,14 +402,13 @@ export interface Workload { age: Date | undefined, startTime: Date | undefined, containers: WorkloadContainer[] - } - - export interface WorkloadContainer { - name: string, - image: string, - restartCount?: number, - ready?: boolean, - started?: boolean, - age: Date | undefined, - } +} +export interface WorkloadContainer { + name: string, + image: string, + restartCount?: number, + ready?: boolean, + started?: boolean, + age: Date | undefined, +} diff --git a/server/swagger.json b/server/swagger.json index 6fb8eb4f..97a09c95 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.10.1", + "version": "2.0.0", "title": "Kubero", "description": "Kubero is a web-based tool deploy applications on a Kubernetes clusters. It provides a simple and intuitive interface to manage your clusters, applications, and pipelines." }, @@ -175,6 +175,22 @@ } } }, + "saAnnotations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "your.sa.annotation.enabled" + }, + "value": { + "type": "string", + "example": "true" + } + } + } + }, "image": { "type": "object", "properties": { @@ -358,6 +374,9 @@ "envvars": { "example": "any" }, + "sAAnnotations": { + "example": "any" + }, "image": { "example": "any" }, @@ -435,6 +454,9 @@ "envvars": { "example": "any" }, + "sAAnnotations": { + "example": "any" + }, "image": { "example": "any" }, @@ -500,6 +522,9 @@ "responses": { "200": { "description": "OK" + }, + "503": { + "description": "Service Unavailable" } } }, @@ -538,6 +563,9 @@ "responses": { "200": { "description": "OK" + }, + "404": { + "description": "Not Found" } } } @@ -1021,6 +1049,78 @@ } } }, + "/audit": { + "get": { + "tags": [ + "UI" + ], + "summary": "Get the Kubero audit log", + "description": "", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "pipeline", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "phase", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/uptimes/{pipeline}/{phase}/": { + "get": { + "description": "", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/metrics/{pipeline}/{phase}/{app}": { "get": { "tags": [ @@ -1078,6 +1178,147 @@ } } }, + "/console/{pipeline}/{phase}/{app}/exec": { + "get": { + "tags": [ + "UI" + ], + "summary": "Start a container console", + "description": "", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": [ + "UI" + ], + "summary": "Start a container console", + "description": "", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "podName": { + "example": "any" + }, + "containerName": { + "example": "any" + }, + "command": { + "example": "any" + } + } + } + } + } + } + } + }, + "/status/pods/{pipeline}/{phase}/{app}": { + "get": { + "tags": [ + "UI" + ], + "summary": "Get the Pod workload from an Namespace", + "description": "", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/cli/pipelines": { "post": { "tags": [ @@ -1451,6 +1692,9 @@ "responses": { "200": { "description": "OK" + }, + "503": { + "description": "Service Unavailable" } }, "security": [ @@ -1500,6 +1744,9 @@ "responses": { "200": { "description": "OK" + }, + "503": { + "description": "Service Unavailable" } }, "security": [ @@ -1547,6 +1794,9 @@ "responses": { "200": { "description": "OK" + }, + "404": { + "description": "Not Found" } }, "security": [ @@ -1580,6 +1830,9 @@ "responses": { "200": { "description": "OK" + }, + "404": { + "description": "Not Found" } }, "security": [ @@ -1613,6 +1866,9 @@ "responses": { "200": { "description": "OK" + }, + "404": { + "description": "Not Found" } } } @@ -1653,6 +1909,59 @@ "responses": { "200": { "description": "OK" + }, + "503": { + "description": "Service Unavailable" + } + } + } + }, + "/pipelines/{pipeline}/{phase}/{app}/download": { + "get": { + "tags": [ + "UI" + ], + "summary": "Get app details", + "description": "", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" } } } @@ -1831,6 +2140,100 @@ } } }, + "/security/{pipeline}/{phase}/{app}/scan": { + "get": { + "tags": [ + "UI" + ], + "summary": "Scan an app for vulnerabilities", + "description": "Scan an app for vulnerabilities", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Pipeline name" + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Phase name" + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "App name" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/security/{pipeline}/{phase}/{app}/scan/result": { + "get": { + "tags": [ + "UI" + ], + "summary": "Get scan result (vulnerabilities) from a app", + "description": "Get scan result (vulnerabilities) from a app", + "parameters": [ + { + "name": "pipeline", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Pipeline name" + }, + { + "name": "phase", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Phase name" + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "App name" + }, + { + "name": "logdetails", + "description": "Add Logs", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/cli/settings": { "get": { "tags": [ @@ -1882,6 +2285,20 @@ } } }, + "/domains": { + "get": { + "tags": [ + "UI" + ], + "summary": "Get a list of all configured domains on this cluster", + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/templates/{catalogId}/{template}": { "get": { "tags": [ From 9a2913bcb17f757bbdaf638cec5de11d08b15ef5 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 8 Mar 2024 13:26:51 +0100 Subject: [PATCH 07/43] implement config safe oin dev mode --- server/{config.yaml => config.yaml.example} | 0 server/src/kubero.ts | 4 + server/src/modules/config.ts | 50 ++++++++++++- server/src/modules/settings.ts | 83 ++++++++++++++++++--- server/src/routes/settings.ts | 9 +++ server/src/types.ts | 81 +------------------- 6 files changed, 137 insertions(+), 90 deletions(-) rename server/{config.yaml => config.yaml.example} (100%) diff --git a/server/config.yaml b/server/config.yaml.example similarity index 100% rename from server/config.yaml rename to server/config.yaml.example diff --git a/server/src/kubero.ts b/server/src/kubero.ts index da077df3..e01e5cfe 100644 --- a/server/src/kubero.ts +++ b/server/src/kubero.ts @@ -945,6 +945,10 @@ export class Kubero { } } + public setConfig(config: IKuberoConfig) { + this.config = config; + } + // Loads the Kubero config from the local config file private loadConfig(path:string): IKuberoConfig { try { diff --git a/server/src/modules/config.ts b/server/src/modules/config.ts index 1ab98a00..da91e66d 100644 --- a/server/src/modules/config.ts +++ b/server/src/modules/config.ts @@ -1,4 +1,4 @@ -import { IBuildpack, ISecurityContext} from '../types'; +import { IBuildpack, IKuberoConfig, IPodSize, ISecurityContext} from '../types'; export class Buildpack implements IBuildpack { public name: string; @@ -6,16 +6,19 @@ export class Buildpack implements IBuildpack { public fetch: { repository: string; tag: string; + readOnlyAppStorage: boolean; securityContext: ISecurityContext }; public build: { repository: string; tag: string; + readOnlyAppStorage: boolean; securityContext: ISecurityContext }; public run: { repository: string; tag: string; + readOnlyAppStorage: boolean; securityContext: ISecurityContext }; public tag: string; @@ -79,4 +82,49 @@ export class Buildpack implements IBuildpack { } +} + +export class KuberoConfig { + public podSizeList: IPodSize[]; + public buildpacks: IBuildpack[]; + public clusterissuer: string; + public templates: { // introduced v1.11.0 + enabled: boolean; + catalogs: [ + { + name: string; + description: string; + templateBasePath: string; + index: { + url: string; + format: string; + } + } + ] + } + public kubero: { + namespace?: string; // deprecated v1.9.0 + console: { + enabled: boolean; + } + readonly: boolean; + banner: { + message: string; + bgcolor: string; + fontcolor: string; + show: boolean; + } + } + constructor(kc: IKuberoConfig) { + + this.podSizeList = kc.podSizeList; + this.buildpacks = kc.buildpacks; + this.clusterissuer = kc.clusterissuer; + this.templates = kc.templates; + this.kubero = kc.kubero; + + for (let i = 0; i < this.buildpacks.length; i++) { + this.buildpacks[i] = new Buildpack(kc.buildpacks[i]); + } + } } \ No newline at end of file diff --git a/server/src/modules/settings.ts b/server/src/modules/settings.ts index 48df3082..6f2dcd07 100644 --- a/server/src/modules/settings.ts +++ b/server/src/modules/settings.ts @@ -1,6 +1,9 @@ import { Kubectl } from './kubectl'; -import { IKuberoConfig, KuberoConfig} from '../types'; +import { IKuberoConfig} from '../types'; +import { KuberoConfig } from './config'; import YAML from 'yaml' +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; export interface SettingsOptions { kubectl: Kubectl; @@ -15,21 +18,83 @@ export class Settings { } public async getSettings(): Promise { - let settings = await this.kubectl.getKuberoconfig() + + let configMap: KuberoConfig + if (process.env.NODE_ENV === "production") { + configMap = await this.loadConfig() + } else { + configMap = new KuberoConfig(this.readConfig()) + } let config: any = {} + config["podSizeList"] = configMap.podSizeList + config["kubero"] = configMap.kubero + config["buildpacks"] = configMap.buildpacks + config["templates"] = configMap.templates + + // TODO: not sure if it is a good idea to expose the whole env to the frontend + config["env"] = process.env + return config + } + + public async updateSettings(config: any): Promise { + + let configMap: KuberoConfig + if (process.env.NODE_ENV === "production") { + configMap = await this.loadConfig() + } else { + configMap = new KuberoConfig(this.readConfig()) + } + + if (configMap) { + configMap.podSizeList = config.podSizeList + configMap.kubero = config.kubero + configMap.buildpacks = config.buildpacks + configMap.templates = config.templates + } else { + configMap = new KuberoConfig(config) + } + + if (process.env.NODE_ENV === "production") { + //this.kubectl.updateKuberoconfig(configMap) + console.log("not implemented") + } else { + this.writeConfig(configMap) + } + return configMap + } + + // load config from kubernetes cluster + private async loadConfig(): Promise { + let settings = await this.kubectl.getKuberoconfig() + if (settings && settings.data) { const IkuberoConfig = YAML.parse(settings.data["config.yaml"]) as IKuberoConfig const configMap = new KuberoConfig(IkuberoConfig) - config["podSizeList"] = configMap.podSizeList - config["kubero"] = configMap.kubero - config["buildpacks"] = configMap.buildpacks - config["templates"] = configMap.templates + return configMap + } else { + return new KuberoConfig( {} as IKuberoConfig) } + } - // TODO: not sure if it is a good idea to expose the whole env to the frontend - config["env"] = process.env - return config + // read config from local filesystem (dev mode) + private readConfig(): IKuberoConfig { + // read config from local filesystem (dev mode) + //const path = join(__dirname, 'config.yaml') + const path = process.env.KUBERO_CONFIG_PATH || join(__dirname, 'config.yaml') + let settings = readFileSync( path, 'utf8') + return YAML.parse(settings) as IKuberoConfig + } + + // write config to local filesystem (dev mode) + private writeConfig(configMap: KuberoConfig) { + console.log("writeConfig") + + const path = process.env.KUBERO_CONFIG_PATH || join(__dirname, 'config.yaml') + writeFileSync(path, YAML.stringify(configMap), { + flag: 'w', + encoding: 'utf8' + }); } public async getDomains(): Promise { diff --git a/server/src/routes/settings.ts b/server/src/routes/settings.ts index 43a2f49d..9937c862 100644 --- a/server/src/routes/settings.ts +++ b/server/src/routes/settings.ts @@ -31,6 +31,15 @@ Router.get('/settings', allwaysAuthMiddleware, async function (req: Request, res }); +// get the settings +Router.post('/settings', allwaysAuthMiddleware, async function (req: Request, res: Response) { + // #swagger.tags = ['UI'] + // #swagger.summary = 'Get the Kubero settings' + const result = await req.app.locals.settings.updateSettings(req.body); + req.app.locals.kubero.setConfig(result); + res.send({config: result}) +}); + // get the dashboard banner Router.get('/banner', async function (req: Request, res: Response) { // #swagger.tags = ['UI'] diff --git a/server/src/types.ts b/server/src/types.ts index 7cb6860b..e3f3394f 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -352,90 +352,11 @@ export interface IKuberoConfig { message: string; bgcolor: string; fontcolor: string; + show: boolean; } } } - -export class KuberoConfig { - public podSizeList: IPodSize[]; - public buildpacks: IBuildpack[]; - public clusterissuer: string; - public templates: { // introduced v1.11.0 - enabled: boolean; - catalogs: [ - { - name: string; - description: string; - templateBasePath: string; - index: { - url: string; - format: string; - } - } - ] - } - public kubero: { - namespace?: string; // deprecated v1.9.0 - console: { - enabled: boolean; - } - readonly: boolean; - banner: { - message: string; - bgcolor: string; - fontcolor: string; - } - } - - constructor(kc: IKuberoConfig) { - - const defaultbuildpack = { - readOnlyRootFilesystem: false, - allowPrivilegeEscalation: false, - runAsUser: 1000, - runAsGroup: 1000, - runAsNonRoot: false, - capabilities: { - drop: [], - add: [] - } - }; - - this.podSizeList = kc.podSizeList; - this.buildpacks = kc.buildpacks; - this.clusterissuer = kc.clusterissuer; - this.templates = kc.templates; - this.kubero = kc.kubero; - - for (let i = 0; i < this.buildpacks.length; i++) { - - this.buildpacks[i].fetch.readOnlyAppStorage = kc.buildpacks[i].fetch.readOnlyAppStorage || false; - this.buildpacks[i].build.readOnlyAppStorage = kc.buildpacks[i].build.readOnlyAppStorage || false; - this.buildpacks[i].run.readOnlyAppStorage = kc.buildpacks[i].run.readOnlyAppStorage || false; - - this.buildpacks[i].fetch.securityContext = this.getSecurityContext(kc.buildpacks[i].fetch.securityContext); - this.buildpacks[i].build.securityContext = this.getSecurityContext(kc.buildpacks[i].build.securityContext); - this.buildpacks[i].run.securityContext = this.getSecurityContext(kc.buildpacks[i].run.securityContext); - } - } - - private getSecurityContext (sc: any) { - let securityContext = { - readOnlyRootFilesystem: sc.readOnlyRootFilesystem || false, - allowPrivilegeEscalation: sc.allowPrivilegeEscalation || false, - runAsUser: sc.runAsUser || 1000, - runAsGroup: sc.runAsGroup || 1000, - runAsNonRoot: sc.runAsNonRoot || false, - capabilities: { - drop: sc.capabilities?.drop || [], - add: sc.capabilities?.add || [] - } - } - return securityContext - } -} - export interface IDeployKeyPair { fingerprint: string; pubKey: string; From 260f897b07b510009b96963c753de9cf9c4f92df Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 8 Mar 2024 13:27:15 +0100 Subject: [PATCH 08/43] implement config safe oin dev mode --- .gitignore | 1 + client/src/components/settings/form-buildpacks.vue | 1 + client/src/components/settings/form-general.vue | 13 +++++-------- client/src/components/settings/form-podsizes.vue | 1 + client/src/components/settings/form-templates.vue | 1 + client/src/components/settings/form.vue | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index a6463dbf..3ec7f42d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,6 @@ example-*.json .dockerdata secrets.yaml +config.yaml db \ No newline at end of file diff --git a/client/src/components/settings/form-buildpacks.vue b/client/src/components/settings/form-buildpacks.vue index b99152dd..b34c4c7a 100644 --- a/client/src/components/settings/form-buildpacks.vue +++ b/client/src/components/settings/form-buildpacks.vue @@ -26,6 +26,7 @@ label="Name" required density="compact" + readonly > @@ -78,7 +77,6 @@ v-model="settings.env.KUBERO_WEBHOOK_SECRET" label="Secret" required - readonly :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'" :type="show ? 'text' : 'password'" @click:append="show = !show" @@ -95,7 +93,7 @@ md="2" > - + @@ -154,7 +151,7 @@ export type Kubero = { enabled: boolean, }, banner: { - enabled: boolean, + show: boolean, bgcolor: string, fontcolor: string, message: string, diff --git a/client/src/components/settings/form-podsizes.vue b/client/src/components/settings/form-podsizes.vue index 2cb600d7..8fe3e16c 100644 --- a/client/src/components/settings/form-podsizes.vue +++ b/client/src/components/settings/form-podsizes.vue @@ -32,6 +32,7 @@ v-model="podSize.name" label="Name" required + readonly > Date: Fri, 8 Mar 2024 05:11:58 +0100 Subject: [PATCH 09/43] improve forms and breadcrumbs --- server/src/modules/config.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/server/src/modules/config.ts b/server/src/modules/config.ts index da91e66d..06c41666 100644 --- a/server/src/modules/config.ts +++ b/server/src/modules/config.ts @@ -126,5 +126,40 @@ export class KuberoConfig { for (let i = 0; i < this.buildpacks.length; i++) { this.buildpacks[i] = new Buildpack(kc.buildpacks[i]); } + + for (let i = 0; i < this.podSizeList.length; i++) { + this.podSizeList[i] = new PodSize(kc.podSizeList[i]); + } + } +} + +export class PodSize implements IPodSize { + public name: string; + public description: string; + public default?: boolean | undefined; + public resources: { + requests?: { + memory: string; + cpu: string; + } | undefined; + limits?: { + memory: string; + cpu: string; + } | undefined; + }; + constructor(ps: IPodSize) { + this.name = ps.name; + this.description = ps.description; + this.default = ps.default; + this.resources = { + requests: { + memory: ps.resources.requests?.memory || "", + cpu: ps.resources.requests?.cpu || "" + }, + limits: { + memory: ps.resources.limits?.memory || "", + cpu: ps.resources.limits?.cpu || "" + } + } } } \ No newline at end of file From c52de21849489a068ca3ae963dfc7ac860c8eeba Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 8 Mar 2024 05:12:17 +0100 Subject: [PATCH 10/43] improve forms and breadcrumbs --- client/src/components/apps/detail.vue | 12 ++- client/src/components/breadcrumbs.vue | 17 ++++- client/src/components/pipelines/detail.vue | 6 +- client/src/components/pipelines/list.vue | 3 +- .../components/settings/form-buildpacks.vue | 3 +- .../src/components/settings/form-general.vue | 14 ++-- .../src/components/settings/form-podsizes.vue | 75 ++++++++++++++++++- client/src/components/settings/form.vue | 13 +++- client/src/layouts/default/AppBar.vue | 2 +- 9 files changed, 124 insertions(+), 21 deletions(-) diff --git a/client/src/components/apps/detail.vue b/client/src/components/apps/detail.vue index 61e57db2..5c545767 100644 --- a/client/src/components/apps/detail.vue +++ b/client/src/components/apps/detail.vue @@ -98,22 +98,26 @@ export default defineComponent({ tab: null, breadcrumbItems: [ { - text: 'DASHBOARD', + title: 'dashboard', + text: '-', disabled: false, to: { name: 'Pipelines', params: {}} }, { - text: 'PIPELINE:'+this.pipeline, + title: 'Pipeline', + text: this.pipeline, disabled: false, to: { name: 'Pipeline Apps', params: { pipeline: this.pipeline }} }, { - text: 'PHASE:'+this.phase, + title: 'Phase', + text: this.phase, disabled: true, href: `/pipeline/${this.pipeline}/${this.phase}/${this.app}/detail`, }, { - text: 'APP:'+this.app, + title: 'App', + text: this.app, disabled: true, href: `/pipeline/${this.pipeline}/${this.phase}/${this.app}/detail`, } diff --git a/client/src/components/breadcrumbs.vue b/client/src/components/breadcrumbs.vue index 3cb03623..0181bd5c 100644 --- a/client/src/components/breadcrumbs.vue +++ b/client/src/components/breadcrumbs.vue @@ -1,12 +1,15 @@ @@ -116,6 +157,8 @@ import { Settings } from './form.vue' export type PodSize = { name: string, description: string, + editable?: boolean, + default?: boolean, resources: { requests: { cpu: string, @@ -144,6 +187,34 @@ export default defineComponent({ } }, methods: { + makeDefaultUnique(podSize: PodSize) { + this.settings.podSizeList.forEach((ps) => { + if (ps !== podSize) { + ps.default = false + } + }) + }, + deletePodSize(podSize: PodSize) { + const index = this.settings.podSizeList.indexOf(podSize) + this.settings.podSizeList.splice(index, 1) + }, + addPodSize() { + this.settings.podSizeList.push({ + name: '', + description: '', + editable: true, + resources: { + requests: { + cpu: '', + memory: '', + }, + limits: { + cpu: '', + memory: '', + } + } + }) + } } }) diff --git a/client/src/components/settings/form.vue b/client/src/components/settings/form.vue index 3d094fcc..1805263c 100644 --- a/client/src/components/settings/form.vue +++ b/client/src/components/settings/form.vue @@ -151,6 +151,15 @@ export default defineComponent({ methods: { saveSettings() { const self = this; + + self.settings.podSizeList.forEach((podSize: PodSize) => { + delete podSize.editable; + }); + + self.settings.buildpacks.forEach((buildpack: Buildpack) => { + delete buildpack.advanced; + }); + axios.post(`/api/settings`, self.settings) .then(response => { console.log('saveSettings', response); @@ -159,7 +168,7 @@ export default defineComponent({ console.log('saveSettings', error); }); }, - +/* sanitizeBuildpacks() { const self = this; self.settings.buildpacks.forEach((buildpack: Buildpack) => { @@ -169,7 +178,7 @@ export default defineComponent({ buildpack.fetch.securityContext.runAsNonRoot = buildpack.fetch.securityContext.runAsNonRoot || false; }); }, - +*/ async loadSettings() { const self = this; axios.get(`/api/settings`) diff --git a/client/src/layouts/default/AppBar.vue b/client/src/layouts/default/AppBar.vue index 46ca528b..7c2626b0 100644 --- a/client/src/layouts/default/AppBar.vue +++ b/client/src/layouts/default/AppBar.vue @@ -1,6 +1,6 @@ From bdb10dc25166f06df21fec4ef546fab7293d7e10 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 8 Mar 2024 05:18:05 +0100 Subject: [PATCH 11/43] add buildpackform --- .../components/settings/form-buildpacks.vue | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/client/src/components/settings/form-buildpacks.vue b/client/src/components/settings/form-buildpacks.vue index d3ec7ff2..4a4e05fb 100644 --- a/client/src/components/settings/form-buildpacks.vue +++ b/client/src/components/settings/form-buildpacks.vue @@ -82,6 +82,21 @@ +

+ + + mdi-plus + + + Add a new pod size +

@@ -131,6 +146,64 @@ export default defineComponent({ deleteBuildpack(buildpack: Buildpack) { this.panel = null this.settings.buildpacks.splice(this.settings.buildpacks.indexOf(buildpack), 1) + }, + addBuildpack() { + this.settings.buildpacks.push({ + name: '', + language: '', + advanced: true, + fetch: { + repository: 'ghcr.io/kubero-dev/buildpacks/fetch', + tag: 'latest', + command: '', + readOnlyAppStorage: false, + securityContext: { + runAsUser: 1000, + runAsGroup: 1000, + runAsNonRoot: false, + readOnlyRootFilesystem: false, + allowPrivilegeEscalation: false, + capabilities: { + add: [], + drop: [] + } + } + }, + build: { + repository: '', + tag: '', + command: '', + readOnlyAppStorage: false, + securityContext: { + runAsUser: 1000, + runAsGroup: 1000, + runAsNonRoot: false, + readOnlyRootFilesystem: false, + allowPrivilegeEscalation: false, + capabilities: { + add: [], + drop: [] + } + } + }, + run: { + repository: '', + tag: '', + command: '', + readOnlyAppStorage: false, + securityContext: { + runAsUser: 1000, + runAsGroup: 1000, + runAsNonRoot: false, + readOnlyRootFilesystem: false, + allowPrivilegeEscalation: false, + capabilities: { + add: [], + drop: [] + } + } + } + }) } } }) From 4a81684c4ec3f0d8d3559aa6d4d2c7e6bb305630 Mon Sep 17 00:00:00 2001 From: Ryan Lewon Date: Fri, 8 Mar 2024 20:26:55 -0800 Subject: [PATCH 12/43] Fix for sAAnnotations. --- client/src/components/apps/form.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index ca70250d..9c77a177 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -1189,7 +1189,7 @@ type EnvVar = { value: string, } -type SAAnnotations { +type SAAnnotations = { [key: string]: string; } From e9d6d576e7ccbbe8f94b0705e7376135043f0200 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Sat, 9 Mar 2024 20:37:44 +0100 Subject: [PATCH 13/43] make forms editable --- .../components/settings/form-buildpacks.vue | 11 +++--- .../components/settings/form-templates.vue | 36 ++++++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/client/src/components/settings/form-buildpacks.vue b/client/src/components/settings/form-buildpacks.vue index 4a4e05fb..d9b43fb6 100644 --- a/client/src/components/settings/form-buildpacks.vue +++ b/client/src/components/settings/form-buildpacks.vue @@ -13,7 +13,7 @@ multiple > - + {{ buildpack.name }} @@ -26,8 +26,6 @@ label="Name" required density="compact" - readonly - disabled > - + {{ catalog.name }} @@ -26,7 +26,6 @@ label="Name" required density="compact" - readonly > +

+ + + mdi-plus + + + Add a new pod size +

@@ -131,13 +145,27 @@ export default defineComponent({ data() { return { show: false, - panel: null + panel: -1 } }, methods: { deleteBuildpack(catalog: Catalog) { - this.panel = null + this.panel = -1 this.settings.templates.catalogs.splice(this.settings.templates.catalogs.indexOf(catalog), 1) + }, + addTemplateCatalog() { + this.settings.templates.catalogs.push({ + name: '', + description: '', + templateBasePath: '', + index: { + url: '', + format: 'json' + } + }) + + // open panel + this.panel = this.settings.templates.catalogs.length - 1 } } }) From 42263f782e38fdffe3d22c27c9a1c3e93a067657 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Tue, 12 Mar 2024 13:20:08 +0100 Subject: [PATCH 14/43] refactoring Forms --- .../settings/form-buildpacks-item.vue | 21 +- .../components/settings/form-buildpacks.vue | 30 +- .../{form-secrets.vue => form-deployment.vue} | 50 +- .../src/components/settings/form-general.vue | 74 +-- .../src/components/settings/form-podsizes.vue | 61 +-- .../components/settings/form-templates.vue | 14 +- client/src/components/settings/form.vue | 437 +++++++++++++++--- server/src/modules/kubectl.ts | 75 ++- server/src/modules/settings.ts | 96 +++- 9 files changed, 611 insertions(+), 247 deletions(-) rename client/src/components/settings/{form-secrets.vue => form-deployment.vue} (76%) diff --git a/client/src/components/settings/form-buildpacks-item.vue b/client/src/components/settings/form-buildpacks-item.vue index 162a1091..3ba8a9b8 100644 --- a/client/src/components/settings/form-buildpacks-item.vue +++ b/client/src/components/settings/form-buildpacks-item.vue @@ -137,25 +137,6 @@