diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index c6c5787af..88acf7deb 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -177,7 +177,7 @@ value="git" > - - + +
+ - + + + + + + +
@@ -1442,6 +1457,7 @@ export default defineComponent({ docker: { image: 'ghcr.io/kubero-dev/idler', tag: 'latest', + command: '', }, autodeploy: true, sslIndex: [] as (boolean|undefined)[], @@ -1630,20 +1646,16 @@ export default defineComponent({ }, }, mounted() { - this.loadPipeline(); + this.loadPipelineAndApp(); this.loadStorageClasses(); this.loadPodsizeList(); this.loadBuildpacks(); this.loadClusterIssuers(); this.getDomains(); - if (this.app != 'new') { - this.loadApp(); // this may lead into a race condition with the buildpacks loaded in loadPipeline - } - if (this.$route.query.template && this.$route.query.catalogId) { - const catalogId = this.$route.query.catalogId as string; + if (this.$route.query.template) { const template = this.$route.query.template as string; - this.loadTemplate(catalogId, template); + this.loadTemplate(template); } //this.buildPipeline = this.$vuetify.buildPipeline @@ -1696,8 +1708,8 @@ export default defineComponent({ this.letsecryptClusterIssuer = response.data.id; }); }, - loadTemplate(catalogId: string, template: string) { - axios.get('/api/templates/'+catalogId+'/'+template).then(response => { + loadTemplate(template: string) { + axios.get('/api/templates/'+template).then(response => { this.appname = response.data.name; this.containerPort = response.data.image.containerPort; @@ -1746,7 +1758,7 @@ export default defineComponent({ changeName(name: string) { this.ingress.hosts[0].host = name+"."+this.pipelineData.domain; }, - loadPipeline() { + loadPipelineAndApp() { axios.get('/api/pipelines/'+this.pipeline).then(response => { this.pipelineData = response.data; @@ -1787,6 +1799,11 @@ export default defineComponent({ this.buildpack.run.readOnlyAppStorage = true; } + if (this.app != 'new') { + this.loadApp(); + } + + }); }, loadStorageClasses() { @@ -1893,6 +1910,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 +1931,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 +2031,13 @@ export default defineComponent({ this.cleanupIngressAnnotations(); this.setSSL(); + let command = [] as string[]; + if (this.docker.command.length > 0) { + command = this.docker.command.split(' '); + } else { + command = []; + } + let postdata = { resourceVersion: this.resourceVersion, buildpack: this.buildpack, @@ -2021,6 +2051,7 @@ export default defineComponent({ containerport: this.containerPort, repository: this.docker.image, tag: this.docker.tag, + command: command, fetch: this.buildpack?.fetch, build: this.buildpack?.build, run: this.buildpack?.run, diff --git a/client/src/components/pipelines/form.vue b/client/src/components/pipelines/form.vue index 184fce898..40bee6467 100644 --- a/client/src/components/pipelines/form.vue +++ b/client/src/components/pipelines/form.vue @@ -58,7 +58,7 @@
- Deployment + Continuous Deployment + + + + +

+ Repository +

+
When connected, webhooks and deployment keys are stored in the repository. This means that the apps configured in this project can be automatically redeployed with a 'git push' and opening a PR starts a new instance in the "review" phase.
+
+
- + + Webhook created + + Deploy keys added + + {{repository_status.statusTxt}} + + + + + + + {{repository_status.statusTxt}} + + + + + + + + Connect + Reconnect + + + Disconnect + + +
+
+ + + Build + + + @@ -236,51 +332,6 @@ > - - - - Webhook created - - Deploy keys added - - - - - - - - {{repository_status.statusTxt}} - - Connect - - @@ -326,11 +377,7 @@ md="4" class="mt-8" > - + 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/client/src/components/settings/form-templates.vue b/client/src/components/settings/form-templates.vue index 67a41bdbc..2df18e1e7 100644 --- a/client/src/components/settings/form-templates.vue +++ b/client/src/components/settings/form-templates.vue @@ -43,17 +43,17 @@ - + > - + + - - - Load template @@ -153,6 +153,7 @@ type Template = { website: string, screenshots: string[], dirname: string, + template: string, } type Templates = { @@ -209,9 +210,10 @@ export default defineComponent({ } this.dialog = false; }, - openInstall(templatename: string, pipeline: string, phase: string, catalogId: number) { + openInstall(templateurl: string, pipeline: string, phase: string) { // redirect to install page - this.$router.push({ name: 'App Form', params: { pipeline: pipeline, phase: phase, app: 'new'}, query: { template: templatename, catalogId: catalogId }}) + const templateurlB64 = btoa(templateurl); + this.$router.push({ name: 'App Form', params: { pipeline: pipeline, phase: phase, app: 'new'}, query: { template: templateurlB64 }}) }, openInstallDialog(template: Template) { diff --git a/client/src/layouts/default/NavDrawer.vue b/client/src/layouts/default/NavDrawer.vue index 183af53a0..3264f8524 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 e60a0ba65..b44f40cc8 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 c503ed3e0..883c32f69 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/git/repo.ts b/server/src/git/repo.ts index 8a20d4e20..10b9dbf77 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/kubero.ts b/server/src/kubero.ts index 2a3980490..e4456b15a 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 = []; @@ -718,6 +726,7 @@ export class Kubero { containerPort: 8080, //TODO use custom containerport repository: pipeline.dockerimage, // FIXME: Maybe needs a lookup into buildpack tag: "main", + command: [''], pullPolicy: "Always", fetch: pipeline.buildpack.fetch, build: pipeline.buildpack.build, @@ -849,7 +858,6 @@ export class Kubero { { name: 'Kubero', description: 'Kubero Templates', - templateBasePath: 'https://raw.githubusercontent.com/kubero-dev/kubero/main/services/', index: { url: 'https://raw.githubusercontent.com/kubero-dev/templates/main/index.json', format: 'json', @@ -1449,10 +1457,6 @@ export class Kubero { return this.config.templates; } - public async getTemplateBasePath(catalogId: number) { - return this.config.templates.catalogs[catalogId].templateBasePath; - } - public getTemplateEnabled() { return this.config.templates.enabled; } diff --git a/server/src/modules/application.ts b/server/src/modules/application.ts index a75159be3..aa0746a0b 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/modules/config.ts b/server/src/modules/config.ts index 06c416669..072630321 100644 --- a/server/src/modules/config.ts +++ b/server/src/modules/config.ts @@ -94,7 +94,7 @@ export class KuberoConfig { { name: string; description: string; - templateBasePath: string; + templateBasePath?: string; // deprecated v2.4.4 index: { url: string; format: string; diff --git a/server/src/modules/kubectl.ts b/server/src/modules/kubectl.ts index 8b9c18f0b..cd4311e19 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; @@ -1138,7 +1176,7 @@ export class Kubectl { if (buildstrategy === 'buildpacks') { // configure build container - job.spec.template.spec.initContainers[1].args[1] = repository.image+":"+repository.tag+"-"+id; + job.spec.template.spec.initContainers[2].args[1] = repository.image+":"+repository.tag+"-"+id; } if (buildstrategy === 'dockerfile') { // configure push container diff --git a/server/src/modules/repositories.ts b/server/src/modules/repositories.ts index 77330c1a2..f1f90b929 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/modules/templates/buildpacks.yaml b/server/src/modules/templates/buildpacks.yaml index 882e2f733..f5b706cb5 100644 --- a/server/src/modules/templates/buildpacks.yaml +++ b/server/src/modules/templates/buildpacks.yaml @@ -75,6 +75,19 @@ spec: - mountPath: /app name: app-storage workingDir: /app + - command: + - sh + - -c + - chmod -R g+w /app + image: busybox:latest + imagePullPolicy: IfNotPresent + name: permissions + securityContext: + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /app + name: app-storage + workingDir: /app - name: build args: - '-app=.' diff --git a/server/src/routes/apps.ts b/server/src/routes/apps.ts index bd403a68b..832215a1c 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/routes/auth.ts b/server/src/routes/auth.ts index c62311fe9..91bd6da7a 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(), diff --git a/server/src/routes/repo.ts b/server/src/routes/repo.ts index b61f30dc5..e1eb0f5b7 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' diff --git a/server/src/routes/templates.ts b/server/src/routes/templates.ts index 7e9c18be9..9d6968112 100644 --- a/server/src/routes/templates.ts +++ b/server/src/routes/templates.ts @@ -8,46 +8,18 @@ export const auth = new Auth(); auth.init(); export const authMiddleware = auth.getAuthMiddleware(); export const bearerMiddleware = auth.getBearerMiddleware(); -/* -// load all services from github repo -Router.get('/services', authMiddleware, async function (req: Request, res: Response) { - // #swagger.tags = ['UI'] - // #swagger.summary = 'Get all services' - - axios.get('https://raw.githubusercontent.com/kubero-dev/kubero/main/services/index.yaml') -}); - - -// load a specific service from github repo -Router.get('/services/:name', authMiddleware, async function (req: Request, res: Response) { - // #swagger.tags = ['UI'] - // #swagger.summary = 'Get a specific service' - // #deprecated = true // since v1.11.0 - - const serviceName = req.params.name.replace(/[^\w.-]+/g, ''); - - const service = await axios.get('https://raw.githubusercontent.com/kubero-dev/kubero/main/services/' + serviceName + '/app.yaml') - .catch((err) => { - res - .status(500) - .send(err); - }); - if (service) { - const ret = YAML.parse(service.data); - res.send(ret.spec); - } -}); -*/ // load a specific service from github repo -Router.get('/templates/:catalogId/:template', authMiddleware, async function (req: Request, res: Response) { +Router.get('/templates/:template', authMiddleware, async function (req: Request, res: Response) { // #swagger.tags = ['UI'] // #swagger.summary = 'Get a specific template' + // #swagger.description = 'Get a specific template from a catalog' + // #swagger.parameters['template'] = { description: 'A base64 encoded URL', type: 'string' } - const templateName = req.params.template.replace(/[^\w.-]+/g, ''); - const templateBasePath = await req.app.locals.kubero.getTemplateBasePath(parseInt(req.params.catalogId)); + // decode the base64 encoded URL + const templateUrl = Buffer.from(req.params.template, 'base64').toString('ascii'); - const template = await axios.get(templateBasePath + templateName + '/app.yaml') + const template = await axios.get(templateUrl) .catch((err) => { res .status(500) @@ -58,15 +30,3 @@ Router.get('/templates/:catalogId/:template', authMiddleware, async function (re res.send(ret.spec); } }); - -// load a specific service from github repo -Router.get('/templates/:catalogId', authMiddleware, async function (req: Request, res: Response) { - // #swagger.tags = ['UI'] - // #swagger.summary = 'Get a specific template' - - const templateBasePath = await req.app.locals.kubero.getTemplateBasePath(parseInt(req.params.catalogId)); - - - axios.get(templateBasePath + '/index.yaml') -}); - diff --git a/server/src/types.ts b/server/src/types.ts index f7c2cf4ed..c2cc4c1a1 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: { @@ -383,7 +384,7 @@ export interface IKuberoConfig { { name: string; description: string; - templateBasePath: string; + templateBasePath?: string; // deprecated v2.4.4 index: { url: string; format: string;