From cdce2fcc73993ae9942f0a46fae846298cf700c8 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 18 Oct 2024 00:03:34 +0200 Subject: [PATCH 01/12] add kubero operator Version and debug modal --- client/src/layouts/default/NavDrawer.vue | 87 +++++++++++++++++++++++- client/src/layouts/default/View.vue | 2 + client/src/stores/kubero.ts | 1 + server/src/kubero.ts | 8 +++ server/src/modules/kubectl.ts | 38 +++++++++++ server/src/routes/auth.ts | 1 + 6 files changed, 134 insertions(+), 3 deletions(-) diff --git a/client/src/layouts/default/NavDrawer.vue b/client/src/layouts/default/NavDrawer.vue index 183af53a..3264f852 100644 --- a/client/src/layouts/default/NavDrawer.vue +++ b/client/src/layouts/default/NavDrawer.vue @@ -92,13 +92,93 @@ --> + + + + + + + + List of latest Kubero releases + + + + + + + @@ -127,7 +207,8 @@ export default defineComponent({ return { version: '0.0.1', templatesEnabled: false, - session: false + session: false, + debugDialog: false } }, computed: { diff --git a/client/src/layouts/default/View.vue b/client/src/layouts/default/View.vue index e60a0ba6..b44f40cc 100644 --- a/client/src/layouts/default/View.vue +++ b/client/src/layouts/default/View.vue @@ -29,6 +29,7 @@ export default defineComponent({ templatesEnabled: true, version: "dev", kubernetesVersion: "unknown", + operatorVersion: "unknown", }), methods: { checkSession() { @@ -41,6 +42,7 @@ export default defineComponent({ // safe version to vuetufy gloabl scope for use in components this.kubero.templatesEnabled = result.data.templatesEnabled; this.kubero.version = result.data.version; + this.kubero.operatorVersion = result.data.operatorVersion; this.kubero.kubernetesVersion = result.data.kubernetesVersion; this.kubero.isAuthenticated = result.data.isAuthenticated; this.kubero.adminDisabled = result.data.adminDisabled; diff --git a/client/src/stores/kubero.ts b/client/src/stores/kubero.ts index c503ed3e..883c32f6 100644 --- a/client/src/stores/kubero.ts +++ b/client/src/stores/kubero.ts @@ -4,6 +4,7 @@ export const useKuberoStore = defineStore('kubero', { state: () => ({ kubero: { version: "dev", + operatorVersion: "unknown", session: false, kubernetesVersion: "", isAuthenticated: false, diff --git a/server/src/kubero.ts b/server/src/kubero.ts index 2a398049..9fcbf687 100644 --- a/server/src/kubero.ts +++ b/server/src/kubero.ts @@ -100,6 +100,14 @@ export class Kubero { } } + public getOperatorVersion() { + if (this.kubectl.kuberoOperatorVersion) { + return this.kubectl.kuberoOperatorVersion; + } else { + return 'unknown'; + } + } + public updateState() { this.pipelineStateList = []; this.appStateList = []; diff --git a/server/src/modules/kubectl.ts b/server/src/modules/kubectl.ts index 8b9c18f0..1bc68efa 100644 --- a/server/src/modules/kubectl.ts +++ b/server/src/modules/kubectl.ts @@ -47,6 +47,7 @@ export class Kubectl { private customObjectsApi: CustomObjectsApi = {} as CustomObjectsApi; private networkingV1Api: NetworkingV1Api = {} as NetworkingV1Api; public kubeVersion: VersionInfo | void; + public kuberoOperatorVersion: string | undefined; private patchUtils: PatchUtils = {} as PatchUtils; public log: KubeLog; //public config: IKuberoConfig; @@ -100,6 +101,16 @@ export class Kubectl { .then(v => { this.kubeVersion = v; }) + .catch(error => { + debug.log("❌ Error getting kube version"); + debug.log(error); + }); + + this.getOperatorVersion() + .then(v => { + debug.log("ℹ️ Operator version: " + v); + this.kuberoOperatorVersion = v || 'unknown'; + }) } @@ -115,6 +126,29 @@ export class Kubectl { } } + private async getOperatorVersion(): Promise { + const contextName = this.getCurrentContext(); + const namespace = "kubero-operator-system"; + + if (contextName) { + const pods = await this.getPods(namespace, contextName) + .catch(error => { + debug.log("Failed to get Operator Version", error); + //return 'error'; + }); + if (pods) { + for (const pod of pods) { + if (pod?.metadata?.name?.startsWith('kubero-operator-controller-manager')) { + const container = pod?.spec?.containers.filter((c: any) => c.name == 'manager')[0]; + return container?.image?.split(':')[1] || 'unknown'; + } + } + }else{ + return 'error getting operator version'; + } + } + } + public getContexts() { return this.kc.getContexts() } @@ -123,6 +157,10 @@ export class Kubectl { this.kc.setCurrentContext(context) } + public getCurrentContext() { + return this.kc.getCurrentContext() + } + public async getNamespaces(): Promise { const namespaces = await this.coreV1Api.listNamespace(); return namespaces.body.items; diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index c62311fe..91bd6da7 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -25,6 +25,7 @@ Router.all("/session", (req: Request, res: Response) => { "isAuthenticated": isAuthenticated, "version": process.env.npm_package_version, "kubernetesVersion": req.app.locals.kubero.getKubernetesVersion(), + "operatorVersion": req.app.locals.kubero.getOperatorVersion(), "buildPipeline": req.app.locals.kubero.getBuildpipelineEnabled(), "templatesEnabled": req.app.locals.kubero.getTemplateEnabled(), "auditEnabled": req.app.locals.audit.getAuditEnabled(), From b3608a022520009709f16f2b7999674c96983e67 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 1 Nov 2024 02:36:42 +0100 Subject: [PATCH 02/12] add field for command when deploying container images --- client/src/components/apps/form.vue | 38 +++++++++++++++++++++++++---- server/src/modules/application.ts | 2 ++ server/src/routes/apps.ts | 6 +++++ server/src/types.ts | 1 + 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index c6c5787a..78528dc0 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -381,9 +381,9 @@ - - + +
+ - + + + + + + +
@@ -1442,6 +1457,7 @@ export default defineComponent({ docker: { image: 'ghcr.io/kubero-dev/idler', tag: 'latest', + command: '', }, autodeploy: true, sslIndex: [] as (boolean|undefined)[], @@ -1893,6 +1909,11 @@ export default defineComponent({ this.panel.push(8) } + let command = ''; + if (response.data.spec.image.command) { + command = response.data.spec.image.command.join(' '); + } + this.security = response.data.spec.image.run.securityContext || {}; this.deploymentstrategy = response.data.spec.deploymentstrategy; @@ -1909,6 +1930,7 @@ export default defineComponent({ this.imageTag = response.data.spec.imageTag; this.docker.image = response.data.spec.image.repository || ''; this.docker.tag = response.data.spec.image.tag || 'latest'; + this.docker.command = command; this.autodeploy = response.data.spec.autodeploy; this.envvars = response.data.spec.envVars; this.serviceAccount = response.data.spec.serviceAccount; @@ -2008,6 +2030,11 @@ export default defineComponent({ this.cleanupIngressAnnotations(); this.setSSL(); + let command = [] as string[]; + if (this.docker.command != '') { + command = this.docker.command.split(' '); + } + let postdata = { resourceVersion: this.resourceVersion, buildpack: this.buildpack, @@ -2021,6 +2048,7 @@ export default defineComponent({ containerport: this.containerPort, repository: this.docker.image, tag: this.docker.tag, + command: this.docker.command.split(' '), fetch: this.buildpack?.fetch, build: this.buildpack?.build, run: this.buildpack?.run, diff --git a/server/src/modules/application.ts b/server/src/modules/application.ts index a75159be..aa0746a0 100644 --- a/server/src/modules/application.ts +++ b/server/src/modules/application.ts @@ -71,6 +71,7 @@ export class App implements IApp{ pullPolicy: 'Always', repository: string, tag: string, + command: [string], fetch: { repository: string, tag: string, @@ -175,6 +176,7 @@ export class App implements IApp{ pullPolicy: 'Always', repository: app.image.repository || 'ghcr.io/kubero-dev/idler', tag: app.image.tag || 'v1', + command: app.image.command, fetch: app.image.fetch, build: app.image.build, run: app.image.run, diff --git a/server/src/routes/apps.ts b/server/src/routes/apps.ts index bd403a68..832215a1 100644 --- a/server/src/routes/apps.ts +++ b/server/src/routes/apps.ts @@ -101,6 +101,10 @@ Router.post('/cli/apps', bearerMiddleware, async function (req: Request, res: Re type: "string", example: "latest" }, + command: { + type: "string", + example: "npm start" + }, fetch: { type: "object", }, @@ -216,6 +220,7 @@ function createApp(req: Request) : IApp { containerPort: req.body.image.containerport, repository: req.body.image.repository, tag: req.body.image.tag || "main", + command: req.body.image.command, pullPolicy: "Always", fetch: req.body.image.fetch, build: req.body.image.build, @@ -276,6 +281,7 @@ Router.put('/pipelines/:pipeline/:phase/:app', authMiddleware, async function (r containerPort: req.body.image.containerport, repository: req.body.image.repository, tag: req.body.image.tag || "latest", + command: req.body.image.command, pullPolicy: "Always", fetch: req.body.image.fetch, build: req.body.image.build, diff --git a/server/src/types.ts b/server/src/types.ts index f7c2cf4e..e895c2ca 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -16,6 +16,7 @@ export interface IApp { image : { repository: string, tag: string, + command: [string], pullPolicy: 'Always', containerPort: number, fetch: { From a1ceb5b5c63897ea452a50cceba737b4cae239a8 Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 1 Nov 2024 02:56:37 +0100 Subject: [PATCH 03/12] fix empty command --- client/src/components/apps/form.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index 78528dc0..7552eebd 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -2031,8 +2031,10 @@ export default defineComponent({ this.setSSL(); let command = [] as string[]; - if (this.docker.command != '') { + if (this.docker.command.length > 0) { command = this.docker.command.split(' '); + } else { + command = []; } let postdata = { @@ -2048,7 +2050,7 @@ export default defineComponent({ containerport: this.containerPort, repository: this.docker.image, tag: this.docker.tag, - command: this.docker.command.split(' '), + command: command, fetch: this.buildpack?.fetch, build: this.buildpack?.build, run: this.buildpack?.run, From 8ab7c69e8e96a292884f36ae6ea947465b910cee Mon Sep 17 00:00:00 2001 From: Gianni Carafa Date: Fri, 1 Nov 2024 03:48:11 +0100 Subject: [PATCH 04/12] rename docker to container --- 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 7552eebd..d8b188f7 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -177,7 +177,7 @@ value="git" > + Create Update
@@ -496,10 +544,13 @@ export default defineComponent({ (v: any) => /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^localhost$|^$/.test(v) || 'Not a domain', ], repositoryRules: [ - (v: any) => !!v || 'Repository is required', - (v: any) => v.length <= 120 || 'Repository must be less than 120 characters', - // ((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)? - (v: any) => /((git|ssh|http(s)?)|(git@[\w.]+))(:(\/\/)?)([\w.@:/\-~]+)(\.git)(\/)?/.test(v) || 'Format "owner/repository"', + //(v: any) => !!v || 'Repository is required', + //(v: any) => v.length <= 120 || 'Repository must be less than 120 characters', + // ((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)? + // ((git|ssh|http(s)?)|(git@[\w.]+))(:(\/\/)?)([\w.@:\/\-~]+)(\.git) + // (git@[\w.]+:\/\/)([\w.\/\-~]+)(\.git) // not working + // ((git|ssh|http(s)?)|(git@[\w\.-]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)? + (v: any) => /^((git|ssh|http(s)?)|(git@[\w\.-]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/.test(v) || 'Format "git@github.com:organisation/repository.git"', ], }}, computed: { @@ -551,6 +602,20 @@ export default defineComponent({ this.buildpack = response.data[0]; }); }, + disconnectRepo(){ + const repo = this.repotab; + axios.post(`/api/repo/${repo}/disconnect`, { + gitrepo: this.gitrepo + }).then(response => { + this.repository_status.connected = false; + }).catch(error => { + console.log(error); + }); + }, + reconnectRepo(){ + this.repository_status.connected = false; + this.connectRepo(); + }, connectRepo() { //console.log(this.gitrepo); //console.log(this.repotab); diff --git a/server/src/git/repo.ts b/server/src/git/repo.ts index 8a20d4e2..10b9dbf7 100644 --- a/server/src/git/repo.ts +++ b/server/src/git/repo.ts @@ -106,6 +106,18 @@ export abstract class Repo { } + public async disconnectRepo(gitrepo: string): Promise { + debug.log('disconnectPipeline: '+gitrepo); + + const {owner, repo} = this.parseRepo(gitrepo); + + // TODO: implement remove deploy key and webhook for all providers + //this.removeDeployKey(owner, repo, 0); + //this.removeWebhook(owner, repo, 0); + + return true; + } + protected parseRepo(gitrepo: string): {owner: string, repo: string} { let owner = gitrepo.match(/^git@.*:(.*)\/.*$/)?.[1] as string; let repo = gitrepo.match(/^git@.*:.*\/(.*).git$/)?.[1] as string; @@ -113,9 +125,11 @@ export abstract class Repo { } protected abstract addDeployKey(owner: string, repo: string): Promise + //protected abstract removeDeployKey(owner: string, repo: string, id: number): Promise protected abstract getRepository(gitrepo: string): Promise; protected abstract addWebhook(owner: string, repo: string, url: string, secret: string): Promise; protected abstract getWebhook(event: string, delivery: string, signature: string, body: any): IWebhook | boolean; + //protected abstract removeWebhook(owner: string, repo: string, id: number): Promise; protected abstract getBranches(repo: string): Promise | undefined; protected abstract getReferences(repo: string): Promise | undefined; protected abstract getPullrequests(repo: string): Promise | undefined; diff --git a/server/src/modules/repositories.ts b/server/src/modules/repositories.ts index 77330c1a..f1f90b92 100644 --- a/server/src/modules/repositories.ts +++ b/server/src/modules/repositories.ts @@ -93,6 +93,26 @@ export class Repositories { } } + public async disconnectRepo(repoProvider: string, repoAddress: string) { + debug.log('disconnectRepo: '+repoProvider+' '+repoAddress); + + switch (repoProvider) { + case 'github': + return this.githubApi.disconnectRepo(repoAddress); + case 'gitea': + return this.giteaApi.disconnectRepo(repoAddress); + case 'gogs': + return this.gogsApi.disconnectRepo(repoAddress); + case 'gitlab': + return this.gitlabApi.disconnectRepo(repoAddress); + case 'bitbucket': + return this.bitbucketApi.disconnectRepo(repoAddress); + case 'onedev': + default: + return {'error': 'unknown repo provider'}; + } + } + public async listRepoBranches(repoProvider: string, repoB64: string ): Promise { //return this.git.listRepoBranches(repo, repoProvider); let branches: Promise = new Promise((resolve, reject) => { diff --git a/server/src/routes/repo.ts b/server/src/routes/repo.ts index b61f30dc..e1eb0f5b 100644 --- a/server/src/routes/repo.ts +++ b/server/src/routes/repo.ts @@ -14,6 +14,13 @@ Router.get('/repo/:repoprovider/list', async function (req: Request, res: Respon res.send(repolist); }); +Router.post('/repo/:repoprovider/disconnect', async function (req: Request, res: Response) { + // #swagger.tags = ['UI'] + // #swagger.summary = 'Disconnect a repository from a pipeline by removing the webhook and deployment key' + let con = await req.app.locals.repositories.disconnectRepo(req.params.repoprovider, req.body.gitrepo); + res.send(con); +}); + Router.post('/repo/:repoprovider/connect', async function (req: Request, res: Response) { // #swagger.tags = ['UI'] // #swagger.summary = 'Connect a repository to a pipeline'