From a59c758e5684afa6a273cca524a080cc51963d6d Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:10:49 +0300 Subject: [PATCH 01/16] chore(deps): add CodeMirror, xterm, and js-yaml Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .deps/EXCLUDED/prod.md | 21 ++ .deps/dev.md | 2 - packages/dashboard-frontend/package.json | 24 +- .../dashboard-frontend/src/typings/json.d.ts | 16 ++ .../webpack.config.common.js | 2 +- tsconfig.json | 3 +- yarn.lock | 247 ++++++++++-------- 7 files changed, 189 insertions(+), 126 deletions(-) create mode 100644 packages/dashboard-frontend/src/typings/json.d.ts diff --git a/.deps/EXCLUDED/prod.md b/.deps/EXCLUDED/prod.md index fd619d1bf3..4e3538ec84 100755 --- a/.deps/EXCLUDED/prod.md +++ b/.deps/EXCLUDED/prod.md @@ -2,10 +2,31 @@ This file lists dependencies that do not need CQs or auto-detection does not wor | Packages | Resolved CQs | | --- | --- | +| `@codemirror/autocomplete@6.20.1` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/autocomplete/6.20.1) | +| `@codemirror/commands@6.10.3` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/commands/6.10.3) | +| `@codemirror/lang-yaml@6.1.3` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/lang-yaml/6.1.3) | +| `@codemirror/language@6.12.3` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/language/6.12.3) | +| `@codemirror/lint@6.9.5` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/lint/6.9.5) | +| `@codemirror/state@6.6.0` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/state/6.6.0) | +| `@codemirror/view@6.41.0` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@codemirror/view/6.41.0) | +| `@eclipse-che/che-devworkspace-generator@7.113.0-next-7b6a101` | ecd.che | +| `@fastify/busboy@3.0.1` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@fastify/busboy/3.0.1) | +| `@fastify/cors@11.2.0` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@fastify/cors/11.2.0) | +| `@fastify/http-proxy@11.4.4` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@fastify/http-proxy/11.4.4) | +| `@fastify/reply-from@12.6.2` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@fastify/reply-from/12.6.2) | +| `@fastify/static@9.1.3` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@fastify/static/9.1.3) | +| `@isaacs/brace-expansion@5.0.1` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@isaacs/brace-expansion/5.0.1) | +| `axios@1.15.2` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/axios/1.15.2) | +| `@lezer/common@1.5.2` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@lezer/common/1.5.2) | +| `@lezer/lr@1.4.8` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@lezer/lr/1.4.8) | +| `@lezer/yaml@1.0.4` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@lezer/yaml/1.0.4) | +| `@uiw/codemirror-theme-github@4.25.8` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@uiw/codemirror-theme-github/4.25.8) | +| `@uiw/codemirror-themes@4.25.8` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@uiw/codemirror-themes/4.25.8) | | `any-signal@4.2.0` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/any-signal/4.2.0) | | `cronstrue@3.13.0` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/cronstrue/3.13.0) | | `fast-uri@3.1.3` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/fast-uri/3.1.3) | | `fastify-plugin@5.2.1` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/fastify-plugin/5.2.1) | +| `fastify@5.8.4` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/fastify/5.8.4) | | `jsep@1.3.9` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/jsep/1.3.9) | | `undici@7.28.0` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/undici/7.28.0) | | `@hapi/wreck@18.1.2` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@hapi/wreck/18.1.2) | diff --git a/.deps/dev.md b/.deps/dev.md index 3c19b0ef8f..97b62d3b25 100644 --- a/.deps/dev.md +++ b/.deps/dev.md @@ -168,7 +168,6 @@ | `@types/babel__template@7.4.3` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/babel__template/7.4.3) | | `@types/babel__traverse@7.20.3` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/babel__traverse/7.20.3) | | `@types/caseless@0.12.4` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/caseless/0.12.4) | -| `@types/codemirror@5.60.15` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/codemirror/5.60.15) | | `@types/eslint-scope@3.7.7` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/eslint-scope/3.7.7) | | `@types/eslint@8.44.6` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/eslint/8.44.6) | | `@types/eslint@9.6.1` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/eslint/9.6.1) | @@ -211,7 +210,6 @@ | `@types/stack-utils@2.0.2` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/stack-utils/2.0.2) | | `@types/stack-utils@2.0.3` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/stack-utils/2.0.3) | | `@types/stream-buffers@3.0.8` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/stream-buffers/3.0.8) | -| `@types/tern@0.23.9` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/tern/0.23.9) | | `@types/tough-cookie@4.0.4` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/tough-cookie/4.0.4) | | `@types/webpack@5.28.5` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/webpack/5.28.5) | | `@types/ws@8.18.1` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@types/ws/8.18.1) | diff --git a/packages/dashboard-frontend/package.json b/packages/dashboard-frontend/package.json index 3a96e3f54f..7c6eff22a1 100644 --- a/packages/dashboard-frontend/package.json +++ b/packages/dashboard-frontend/package.json @@ -32,18 +32,24 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.10.8", - "@codemirror/view": "^6.36.2", - "@lezer/highlight": "^1.2.1", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "^6.12.3", + "@codemirror/lint": "^6.9.5", + "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.41.0", "@patternfly/react-core": "^6.4.0", "@patternfly/react-icons": "^6.4.0", "@patternfly/react-table": "^6.4.0", "@reduxjs/toolkit": "^2.2.7", - "@uiw/codemirror-theme-github": "^4.25.4", - "@uiw/codemirror-themes": "^4.25.4", - "@uiw/react-codemirror": "4.23.5", + "@xterm/addon-fit": "0.10.0", + "@xterm/addon-web-links": "0.11.0", + "@xterm/xterm": "5.5.0", + "ajv": "^8.18.0", "buffer": "^6.0.3", + "codemirror": "^6.0.2", "cron-parser": "^5.5.0", "cronstrue": "^3.13.0", "crypto-browserify": "^3.12.0", @@ -68,7 +74,8 @@ "reconnecting-websocket": "^4.4.0", "redux": "^5.0.1", "reflect-metadata": "^0.1.13", - "sanitize-html": "^2.17.4" + "sanitize-html": "^2.17.4", + "yaml": "^2.8.3" }, "devDependencies": { "@devfile/api": "2.3.0-1747843475", @@ -78,7 +85,6 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/codemirror": "^5.60.15", "@types/history": "^4.7.6", "@types/jest": "^29.5.3", "@types/js-yaml": "^4.0.5", diff --git a/packages/dashboard-frontend/src/typings/json.d.ts b/packages/dashboard-frontend/src/typings/json.d.ts new file mode 100644 index 0000000000..cbb3a7be13 --- /dev/null +++ b/packages/dashboard-frontend/src/typings/json.d.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +declare module '*.json' { + const value: Record; + export default value; +} diff --git a/packages/dashboard-frontend/webpack.config.common.js b/packages/dashboard-frontend/webpack.config.common.js index f24a23d252..4b903e2e7c 100644 --- a/packages/dashboard-frontend/webpack.config.common.js +++ b/packages/dashboard-frontend/webpack.config.common.js @@ -44,7 +44,7 @@ const config = { optimization: { splitChunks: { chunks: (chunk) => { - // exclude preload chunks from being split (they should be standalone) + // exclude preload and worker chunks from being split (they should be standalone) return ( chunk.name !== 'accept-factory-link' && chunk.name !== 'branding-loader' diff --git a/tsconfig.json b/tsconfig.json index 93ac93fc0e..262a03284d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "noEmit": false, - "sourceMap": true + "sourceMap": true, + "resolveJsonModule": true } } diff --git a/yarn.lock b/yarn.lock index d6b8cd6721..484b079eb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -659,7 +659,19 @@ __metadata: languageName: node linkType: hard -"@codemirror/commands@npm:^6.0.0, @codemirror/commands@npm:^6.1.0": +"@codemirror/autocomplete@npm:^6.20.1": + version: 6.20.3 + resolution: "@codemirror/autocomplete@npm:6.20.3" + dependencies: + "@codemirror/language": "npm:^6.0.0" + "@codemirror/state": "npm:^6.0.0" + "@codemirror/view": "npm:^6.17.0" + "@lezer/common": "npm:^1.0.0" + checksum: 10/1e66faedba3c7c1520b938838de2a95d4d4f16d168158e8dfd4d34bb72fe9538f4d9e73449bd2c8bba6f1f0a40f4a754e656637296d434216529c6426828aaf5 + languageName: node + linkType: hard + +"@codemirror/commands@npm:^6.0.0": version: 6.8.1 resolution: "@codemirror/commands@npm:6.8.1" dependencies: @@ -671,9 +683,21 @@ __metadata: languageName: node linkType: hard -"@codemirror/lang-yaml@npm:^6.1.2": - version: 6.1.2 - resolution: "@codemirror/lang-yaml@npm:6.1.2" +"@codemirror/commands@npm:^6.10.3": + version: 6.10.4 + resolution: "@codemirror/commands@npm:6.10.4" + dependencies: + "@codemirror/language": "npm:^6.0.0" + "@codemirror/state": "npm:^6.7.0" + "@codemirror/view": "npm:^6.27.0" + "@lezer/common": "npm:^1.1.0" + checksum: 10/384808f8767967f7a2bf4007cb57e2b137e98fbeb6bb7239502b79ad06fb8039b67b43ab2f5eaf955a2569c65203c7a0e59ad758e7d90ba727936f9b2a77e47b + languageName: node + linkType: hard + +"@codemirror/lang-yaml@npm:^6.1.3": + version: 6.1.3 + resolution: "@codemirror/lang-yaml@npm:6.1.3" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/language": "npm:^6.0.0" @@ -682,11 +706,11 @@ __metadata: "@lezer/highlight": "npm:^1.2.0" "@lezer/lr": "npm:^1.0.0" "@lezer/yaml": "npm:^1.0.0" - checksum: 10/1a1ad16554b27d9f66ad2a342170e7c7e51781876280727790e763e9a770163772d4880a9c344705ca65acc2b5fb228962dc3281ba05a71d2c071515541258ae + checksum: 10/0af746beca691bbab992843b7803b30529da1e8b089978cde58de335bbad68de78f905d3cef4391011b64e9e77537bb5b39ff0bfc53c4e408903bc814a50168e languageName: node linkType: hard -"@codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.10.8": +"@codemirror/language@npm:^6.0.0": version: 6.11.3 resolution: "@codemirror/language@npm:6.11.3" dependencies: @@ -700,6 +724,20 @@ __metadata: languageName: node linkType: hard +"@codemirror/language@npm:^6.12.3": + version: 6.12.4 + resolution: "@codemirror/language@npm:6.12.4" + dependencies: + "@codemirror/state": "npm:^6.0.0" + "@codemirror/view": "npm:^6.23.0" + "@lezer/common": "npm:^1.5.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + style-mod: "npm:^4.0.0" + checksum: 10/6a9af11337d15036761ad959dceba4cfd054cdfae84afa4d54534927d277591a57c2b581f66df4428510dd06c641151aa699ab1dc7c338d923ac06a219d83bc6 + languageName: node + linkType: hard + "@codemirror/lint@npm:^6.0.0": version: 6.8.5 resolution: "@codemirror/lint@npm:6.8.5" @@ -711,6 +749,17 @@ __metadata: languageName: node linkType: hard +"@codemirror/lint@npm:^6.9.5": + version: 6.9.7 + resolution: "@codemirror/lint@npm:6.9.7" + dependencies: + "@codemirror/state": "npm:^6.0.0" + "@codemirror/view": "npm:^6.42.0" + crelt: "npm:^1.0.5" + checksum: 10/f1af8295e1741a8d0c155cd7552c35d21c859d8848571fa084f5a6f2a50cac51ed25f04e423cab0f094e11b4828a54de4383a97b6b8cf6a242c5941034bffa04 + languageName: node + linkType: hard + "@codemirror/search@npm:^6.0.0": version: 6.5.11 resolution: "@codemirror/search@npm:6.5.11" @@ -722,7 +771,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.1, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0": +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0": version: 6.5.2 resolution: "@codemirror/state@npm:6.5.2" dependencies: @@ -731,19 +780,28 @@ __metadata: languageName: node linkType: hard -"@codemirror/theme-one-dark@npm:^6.0.0": - version: 6.1.2 - resolution: "@codemirror/theme-one-dark@npm:6.1.2" +"@codemirror/state@npm:^6.6.0, @codemirror/state@npm:^6.7.0": + version: 6.7.1 + resolution: "@codemirror/state@npm:6.7.1" + dependencies: + "@marijn/find-cluster-break": "npm:^1.0.0" + checksum: 10/71493cc2061ba0c89fd675fa350a9cd91538caeccaac7ba318e631a5f1b43bf52c2a165042600414b4441952af555a75e4d4258ef7897cfec8d9c82a4674ddd1 + languageName: node + linkType: hard + +"@codemirror/theme-one-dark@npm:^6.1.3": + version: 6.1.3 + resolution: "@codemirror/theme-one-dark@npm:6.1.3" dependencies: "@codemirror/language": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" "@codemirror/view": "npm:^6.0.0" "@lezer/highlight": "npm:^1.0.0" - checksum: 10/ea4517975b4004bd7d3ef7731a861b59c36d2ddc603d9c4ceca2c5b7637d62a8290f3f9e15004cd0f0e2ea1cc0f6c882a5ad2dd79862a6971b6654325914ccbc + checksum: 10/f62c2393aad715626a1fe3a766c87f3f835bfdae7c56f738601cdee56f02417d56b203cfbbaffbd601887865ce0b1a662c1473296a107616adb311d63fc81146 languageName: node linkType: hard -"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.35.0, @codemirror/view@npm:^6.36.2": +"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.35.0": version: 6.37.1 resolution: "@codemirror/view@npm:6.37.1" dependencies: @@ -755,6 +813,18 @@ __metadata: languageName: node linkType: hard +"@codemirror/view@npm:^6.41.0, @codemirror/view@npm:^6.42.0": + version: 6.43.6 + resolution: "@codemirror/view@npm:6.43.6" + dependencies: + "@codemirror/state": "npm:^6.7.0" + crelt: "npm:^1.0.6" + style-mod: "npm:^4.1.0" + w3c-keyname: "npm:^2.2.4" + checksum: 10/be058e86523d5770921c6bad7963eb5ef0a7db340922db93043394e1e93d1f3125611a3c2dbf143f1ab69b426038cd2034ffa7484d4b55d1ac909f2ceed2ee06 + languageName: node + linkType: hard + "@commitlint/cli@npm:^21.0.2": version: 21.0.2 resolution: "@commitlint/cli@npm:21.0.2" @@ -1145,12 +1215,16 @@ __metadata: version: 0.0.0-use.local resolution: "@eclipse-che/dashboard-frontend@workspace:packages/dashboard-frontend" dependencies: - "@codemirror/lang-yaml": "npm:^6.1.2" - "@codemirror/language": "npm:^6.10.8" - "@codemirror/view": "npm:^6.36.2" + "@codemirror/autocomplete": "npm:^6.20.1" + "@codemirror/commands": "npm:^6.10.3" + "@codemirror/lang-yaml": "npm:^6.1.3" + "@codemirror/language": "npm:^6.12.3" + "@codemirror/lint": "npm:^6.9.5" + "@codemirror/state": "npm:^6.6.0" + "@codemirror/theme-one-dark": "npm:^6.1.3" + "@codemirror/view": "npm:^6.41.0" "@devfile/api": "npm:2.3.0-1747843475" "@kubernetes/client-node": "npm:^0.22.1" - "@lezer/highlight": "npm:^1.2.1" "@patternfly/react-core": "npm:^6.4.0" "@patternfly/react-icons": "npm:^6.4.0" "@patternfly/react-table": "npm:^6.4.0" @@ -1160,7 +1234,6 @@ __metadata: "@testing-library/jest-dom": "npm:^6.5.0" "@testing-library/react": "npm:^16.0.1" "@testing-library/user-event": "npm:^14.5.2" - "@types/codemirror": "npm:^5.60.15" "@types/history": "npm:^4.7.6" "@types/jest": "npm:^29.5.3" "@types/js-yaml": "npm:^4.0.5" @@ -1176,11 +1249,13 @@ __metadata: "@types/webpack": "npm:^5.28.5" "@typescript-eslint/eslint-plugin": "npm:^6.4.0" "@typescript-eslint/parser": "npm:^6.4.0" - "@uiw/codemirror-theme-github": "npm:^4.25.4" - "@uiw/codemirror-themes": "npm:^4.25.4" - "@uiw/react-codemirror": "npm:4.23.5" + "@xterm/addon-fit": "npm:0.10.0" + "@xterm/addon-web-links": "npm:0.11.0" + "@xterm/xterm": "npm:5.5.0" + ajv: "npm:^8.18.0" buffer: "npm:^6.0.3" clean-terminal-webpack-plugin: "npm:^3.0.0" + codemirror: "npm:^6.0.2" copy-webpack-plugin: "npm:^11.0.0" cron-parser: "npm:^5.5.0" cronstrue: "npm:^3.13.0" @@ -1245,6 +1320,7 @@ __metadata: webpack-bundle-analyzer: "npm:^4.10.0" webpack-cli: "npm:^5.1.4" webpack-merge: "npm:^5.9.0" + yaml: "npm:^2.8.3" languageName: unknown linkType: soft @@ -2237,7 +2313,14 @@ __metadata: languageName: node linkType: hard -"@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.2.0, @lezer/highlight@npm:^1.2.1": +"@lezer/common@npm:^1.5.0": + version: 1.5.2 + resolution: "@lezer/common@npm:1.5.2" + checksum: 10/62c0a0ce431cb14bfa6cded04e24a05efa566e9260314bb3f3cf547d501efa1db5c83fc9a7871f8e0b6a4030212840f0e6567433168960c7c83bb3ffbeef8b99 + languageName: node + linkType: hard + +"@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.2.0": version: 1.2.1 resolution: "@lezer/highlight@npm:1.2.1" dependencies: @@ -2697,15 +2780,6 @@ __metadata: languageName: node linkType: hard -"@types/codemirror@npm:^5.60.15": - version: 5.60.15 - resolution: "@types/codemirror@npm:5.60.15" - dependencies: - "@types/tern": "npm:*" - checksum: 10/925ad6b76a703e0ed8ea07f11593cb32498960ece111d21d57a867f30cf151773beeefa2e593df536766d2f3240aa6d3eaa7d5398c8380e0b996a19bf058032e - languageName: node - linkType: hard - "@types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -3067,15 +3141,6 @@ __metadata: languageName: node linkType: hard -"@types/tern@npm:*": - version: 0.23.9 - resolution: "@types/tern@npm:0.23.9" - dependencies: - "@types/estree": "npm:*" - checksum: 10/72d26a1abc2b13aa7ee6a34abc0558bfbff1f977beb1c9f2554c4193c6cfc7e1677e4f84663d3f971135b436c5b847f170c74c3b1d6c320e08a1aba7e83cc3c2 - languageName: node - linkType: hard - "@types/tough-cookie@npm:*": version: 4.0.4 resolution: "@types/tough-cookie@npm:4.0.4" @@ -3257,75 +3322,6 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.23.5": - version: 4.23.5 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.23.5" - dependencies: - "@codemirror/autocomplete": "npm:^6.0.0" - "@codemirror/commands": "npm:^6.0.0" - "@codemirror/language": "npm:^6.0.0" - "@codemirror/lint": "npm:^6.0.0" - "@codemirror/search": "npm:^6.0.0" - "@codemirror/state": "npm:^6.0.0" - "@codemirror/view": "npm:^6.0.0" - peerDependencies: - "@codemirror/autocomplete": ">=6.0.0" - "@codemirror/commands": ">=6.0.0" - "@codemirror/language": ">=6.0.0" - "@codemirror/lint": ">=6.0.0" - "@codemirror/search": ">=6.0.0" - "@codemirror/state": ">=6.0.0" - "@codemirror/view": ">=6.0.0" - checksum: 10/1e786f0e2ed784b886a294ae2261a41876393713669d2aa1b315c0f73cf3bef33669db4cabf4985e5fee8e2a451f56f0a93d5a42c286ea438f9b3d439d3d6b97 - languageName: node - linkType: hard - -"@uiw/codemirror-theme-github@npm:^4.25.4": - version: 4.25.9 - resolution: "@uiw/codemirror-theme-github@npm:4.25.9" - dependencies: - "@uiw/codemirror-themes": "npm:4.25.9" - checksum: 10/e26010bffa600165a2d67a50200b640a61f3341a96e9c3d62d544e96d07423c79be265fb90b543a673ea0f681eb7855e641a7cd4ee70ea0caf2faf4455589bc7 - languageName: node - linkType: hard - -"@uiw/codemirror-themes@npm:4.25.9, @uiw/codemirror-themes@npm:^4.25.4": - version: 4.25.9 - resolution: "@uiw/codemirror-themes@npm:4.25.9" - dependencies: - "@codemirror/language": "npm:^6.0.0" - "@codemirror/state": "npm:^6.0.0" - "@codemirror/view": "npm:^6.0.0" - peerDependencies: - "@codemirror/language": ">=6.0.0" - "@codemirror/state": ">=6.0.0" - "@codemirror/view": ">=6.0.0" - checksum: 10/4a4fe7ae5f6c2bd37170b46c75ccabb67b47b7d3cee0de45c63fafe4f2b7569461b009e0ff5386480574e651627ed03c077833d53b6d3391102b79107ae39d15 - languageName: node - linkType: hard - -"@uiw/react-codemirror@npm:4.23.5": - version: 4.23.5 - resolution: "@uiw/react-codemirror@npm:4.23.5" - dependencies: - "@babel/runtime": "npm:^7.18.6" - "@codemirror/commands": "npm:^6.1.0" - "@codemirror/state": "npm:^6.1.1" - "@codemirror/theme-one-dark": "npm:^6.0.0" - "@uiw/codemirror-extensions-basic-setup": "npm:4.23.5" - codemirror: "npm:^6.0.0" - peerDependencies: - "@babel/runtime": ">=7.11.0" - "@codemirror/state": ">=6.0.0" - "@codemirror/theme-one-dark": ">=6.0.0" - "@codemirror/view": ">=6.0.0" - codemirror: ">=6.0.0" - react: ">=16.8.0" - react-dom: ">=16.8.0" - checksum: 10/a4ae09f44b5ccc90c8560d6c211a8b7ecdcdee08f94419b0aeb0c23aed5ca5e554d4bccf979d57aca434d8279ccd40eb974f1ba183718dcc222e17a993cffd23 - languageName: node - linkType: hard - "@ungap/structured-clone@npm:^1.2.0, @ungap/structured-clone@npm:^1.3.0": version: 1.3.0 resolution: "@ungap/structured-clone@npm:1.3.0" @@ -3803,6 +3799,31 @@ __metadata: languageName: node linkType: hard +"@xterm/addon-fit@npm:0.10.0": + version: 0.10.0 + resolution: "@xterm/addon-fit@npm:0.10.0" + peerDependencies: + "@xterm/xterm": ^5.0.0 + checksum: 10/8edfad561c0d0316c5883cbe2ce56109f105a2b2bf53b71d5f8c788e656a3205c1093a659dddcf4025a459e4b7ff8e07b6c6a19815c8711deeded560de5f1893 + languageName: node + linkType: hard + +"@xterm/addon-web-links@npm:0.11.0": + version: 0.11.0 + resolution: "@xterm/addon-web-links@npm:0.11.0" + peerDependencies: + "@xterm/xterm": ^5.0.0 + checksum: 10/c8eb08a5c7bccb7d30ab04c4a246ca912778c32a8de6e4ae29ca20c3ef017f953cd303d5f0fed67fa7a68b3ac66bf613f8ea66c79f25059633093f3264d86bf5 + languageName: node + linkType: hard + +"@xterm/xterm@npm:5.5.0": + version: 5.5.0 + resolution: "@xterm/xterm@npm:5.5.0" + checksum: 10/d4cdc402de81a83a3e0ef93f38072cb8f54abe4d65865f2e29b92cbc2593f86d052f6b993895c9e5dec97f47548f504e90bcea0aad6845917c09b03f2f3a4629 + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -4033,7 +4054,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.11.0": +"ajv@npm:^8.11.0, ajv@npm:^8.18.0": version: 8.20.0 resolution: "ajv@npm:8.20.0" dependencies: @@ -5187,9 +5208,9 @@ __metadata: languageName: node linkType: hard -"codemirror@npm:^6.0.0": - version: 6.0.1 - resolution: "codemirror@npm:6.0.1" +"codemirror@npm:^6.0.2": + version: 6.0.2 + resolution: "codemirror@npm:6.0.2" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/commands": "npm:^6.0.0" @@ -5198,7 +5219,7 @@ __metadata: "@codemirror/search": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" "@codemirror/view": "npm:^6.0.0" - checksum: 10/4f858cde1cf8ce4670de9df4a64f4990bb8abdb8e13d3e437f278c40c86d841ef505aa1e5dc798582109ceaac8577a3bb4a1f026c0e5ce730465c89652ee6036 + checksum: 10/bd73c9ebbd8cb709a697fa4e2484c188c44aafce4b4d2a342807a5648c6be5bd5876f0caac47db0c1181f1ec8d86e38bd2b6efe51b971bf240fd3382bef0b371 languageName: node linkType: hard From 8f10b0c75232736c0bc27e09e31a62b8e197604b Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:10:56 +0300 Subject: [PATCH 02/16] feat(common): add terminal themes and WebSocket types Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .../__tests__/terminal-themes.spec.ts | 37 ++++++ packages/common/src/constants/index.ts | 1 + .../common/src/constants/terminal-themes.ts | 119 ++++++++++++++++++ .../src/dto/api/__tests__/webSocket.spec.ts | 35 ++++++ packages/common/src/dto/api/index.ts | 23 ++++ packages/common/src/dto/api/webSocket.ts | 42 +++++-- 6 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 packages/common/src/constants/__tests__/terminal-themes.spec.ts create mode 100644 packages/common/src/constants/terminal-themes.ts diff --git a/packages/common/src/constants/__tests__/terminal-themes.spec.ts b/packages/common/src/constants/__tests__/terminal-themes.spec.ts new file mode 100644 index 0000000000..d61e7b8242 --- /dev/null +++ b/packages/common/src/constants/__tests__/terminal-themes.spec.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { isTerminalThemeName, TERMINAL_THEMES } from '../terminal-themes'; + +describe('terminal-themes', () => { + test('TERMINAL_THEMES has all expected themes', () => { + expect(TERMINAL_THEMES).toHaveProperty('dracula'); + expect(TERMINAL_THEMES).toHaveProperty('dark'); + expect(TERMINAL_THEMES).toHaveProperty('light'); + }); + + describe('isTerminalThemeName', () => { + it.each(['dracula', 'dark', 'light'])( + 'should return true for "%s"', + name => { + expect(isTerminalThemeName(name)).toBe(true); + }, + ); + + it.each(['', 'invalid', 'Dracula', 'DARK', 'solarized'])( + 'should return false for "%s"', + name => { + expect(isTerminalThemeName(name)).toBe(false); + }, + ); + }); +}); diff --git a/packages/common/src/constants/index.ts b/packages/common/src/constants/index.ts index 44d670c240..7dc81f19d7 100644 --- a/packages/common/src/constants/index.ts +++ b/packages/common/src/constants/index.ts @@ -11,3 +11,4 @@ */ export * from './backup'; +export * from './terminal-themes'; diff --git a/packages/common/src/constants/terminal-themes.ts b/packages/common/src/constants/terminal-themes.ts new file mode 100644 index 0000000000..912d2eae54 --- /dev/null +++ b/packages/common/src/constants/terminal-themes.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +export interface TerminalTheme { + foreground: string; + background: string; + cursor: string; + cursorAccent: string; + selectionBackground: string; + selectionForeground: string; + black: string; + red: string; + green: string; + yellow: string; + blue: string; + magenta: string; + cyan: string; + white: string; + brightBlack: string; + brightRed: string; + brightGreen: string; + brightYellow: string; + brightBlue: string; + brightMagenta: string; + brightCyan: string; + brightWhite: string; +} + +export type TerminalThemeName = 'dracula' | 'dark' | 'light'; + +export const TERMINAL_THEMES: Record = { + dracula: { + foreground: '#f8f8f2', + background: '#282a36', + cursor: '#f8f8f2', + cursorAccent: '#282a36', + selectionBackground: '#44475a', + selectionForeground: '#f8f8f2', + black: '#21222c', + red: '#ff5555', + green: '#50fa7b', + yellow: '#f1fa8c', + blue: '#bd93f9', + magenta: '#ff79c6', + cyan: '#8be9fd', + white: '#f8f8f2', + brightBlack: '#6272a4', + brightRed: '#ff6e6e', + brightGreen: '#69ff94', + brightYellow: '#ffffa5', + brightBlue: '#d6acff', + brightMagenta: '#ff92df', + brightCyan: '#a4ffff', + brightWhite: '#ffffff', + }, + dark: { + foreground: '#d4d4d4', + background: '#1e1e1e', + cursor: '#d4d4d4', + cursorAccent: '#1e1e1e', + selectionBackground: '#264f78', + selectionForeground: '#ffffff', + black: '#000000', + red: '#cd3131', + green: '#0dbc79', + yellow: '#e5e510', + blue: '#2472c8', + magenta: '#bc3fbc', + cyan: '#11a8cd', + white: '#e5e5e5', + brightBlack: '#666666', + brightRed: '#f14c4c', + brightGreen: '#23d18b', + brightYellow: '#f5f543', + brightBlue: '#3b8eea', + brightMagenta: '#d670d6', + brightCyan: '#29b8db', + brightWhite: '#e5e5e5', + }, + light: { + foreground: '#383a42', + background: '#fafafa', + cursor: '#6083ad', + cursorAccent: '#fafafa', + selectionBackground: '#d7d4f0', + selectionForeground: '#383a42', + black: '#000000', + red: '#e45649', + green: '#50a14f', + yellow: '#BDB76B', + blue: '#4078f2', + magenta: '#a626a4', + cyan: '#0184bc', + white: '#a0a1a7', + brightBlack: '#5c6370', + brightRed: '#e06c75', + brightGreen: '#98c379', + brightYellow: '#e5c07b', + brightBlue: '#61afef', + brightMagenta: '#c678dd', + brightCyan: '#56b6c2', + brightWhite: '#909090', + }, +}; + +export function isTerminalThemeName(value: string): value is TerminalThemeName { + return value === 'dracula' || value === 'dark' || value === 'light'; +} diff --git a/packages/common/src/dto/api/__tests__/webSocket.spec.ts b/packages/common/src/dto/api/__tests__/webSocket.spec.ts index c29e4e081f..e780665234 100644 --- a/packages/common/src/dto/api/__tests__/webSocket.spec.ts +++ b/packages/common/src/dto/api/__tests__/webSocket.spec.ts @@ -13,6 +13,7 @@ import { webSocket } from '..'; import { Channel, + isConfigMapMessage, isDevWorkspaceMessage, isEventMessage, isLogsMessage, @@ -265,6 +266,40 @@ describe('api.webSocket typeguards', () => { expect(isWebSocketEventData('foo')).toBeFalsy(); }); + test('isConfigMapMessage', () => { + expect( + isConfigMapMessage({ + eventPhase: webSocket.EventPhase.ADDED, + configMap: {}, + }), + ).toBeTruthy(); + expect( + isConfigMapMessage({ + eventPhase: webSocket.EventPhase.DELETED, + configMap: {}, + }), + ).toBeTruthy(); + expect( + isConfigMapMessage({ + eventPhase: webSocket.EventPhase.MODIFIED, + configMap: {}, + }), + ).toBeTruthy(); + + expect(isConfigMapMessage(undefined)).toBeFalsy(); + expect(isConfigMapMessage({})).toBeFalsy(); + expect( + isConfigMapMessage({ + eventPhase: webSocket.EventPhase.ADDED, + }), + ).toBeFalsy(); + expect( + isConfigMapMessage({ + configMap: {}, + }), + ).toBeFalsy(); + }); + test('isLogsMessage', () => { expect( isLogsMessage({ diff --git a/packages/common/src/dto/api/index.ts b/packages/common/src/dto/api/index.ts index 33ca0821b1..fabdc2b5be 100644 --- a/packages/common/src/dto/api/index.ts +++ b/packages/common/src/dto/api/index.ts @@ -77,6 +77,29 @@ export interface IPatch { value?: any; } +// Generated by Claude Opus 4.6 + +export interface AiAgentDefinition { + id: string; + name: string; + publisher: string; + description: string; + icon: string; + docsUrl: string; + image: string; + tag: string; + memoryLimit: string; + cpuLimit: string; + terminalPort: number; + env: Array<{ name: string; value: string }>; + initCommand: string; +} + +export interface IAiAgentRegistry { + agents: AiAgentDefinition[]; + defaultAgentId: string; +} + export interface IGitConfig { resourceVersion?: string; gitconfig: { diff --git a/packages/common/src/dto/api/webSocket.ts b/packages/common/src/dto/api/webSocket.ts index 3f336d75c4..1f47506868 100644 --- a/packages/common/src/dto/api/webSocket.ts +++ b/packages/common/src/dto/api/webSocket.ts @@ -11,22 +11,29 @@ */ import { V1alpha2DevWorkspace } from '@devfile/api'; -import { CoreV1Event, V1Pod, V1Status } from '@kubernetes/client-node'; +import { + CoreV1Event, + V1ConfigMap, + V1Pod, + V1Status, +} from '@kubernetes/client-node'; export enum Channel { DEV_WORKSPACE = 'devWorkspace', EVENT = 'event', POD = 'pod', LOGS = 'logs', + CONFIGMAP = 'configmap', } export function isWebSocketChannel(channel: unknown): channel is Channel { return ( - (channel !== undefined && - ((channel as Channel) === Channel.DEV_WORKSPACE || - (channel as Channel) === Channel.EVENT)) || - (channel as Channel) === Channel.POD || - (channel as Channel) === Channel.LOGS + channel !== undefined && + ((channel as Channel) === Channel.DEV_WORKSPACE || + (channel as Channel) === Channel.EVENT || + (channel as Channel) === Channel.POD || + (channel as Channel) === Channel.LOGS || + (channel as Channel) === Channel.CONFIGMAP) ); } @@ -44,7 +51,11 @@ export type SubscribeMessage = { method: 'SUBSCRIBE'; } & ( | { - channel: Channel.DEV_WORKSPACE | Channel.EVENT | Channel.POD; + channel: + | Channel.DEV_WORKSPACE + | Channel.EVENT + | Channel.POD + | Channel.CONFIGMAP; params: SubscribeParams; } | { @@ -138,6 +149,10 @@ export type PodMessage = { eventPhase: EventPhase.ADDED | EventPhase.MODIFIED | EventPhase.DELETED; pod: V1Pod; }; +export type ConfigMapMessage = { + eventPhase: EventPhase.ADDED | EventPhase.MODIFIED | EventPhase.DELETED; + configMap: V1ConfigMap; +}; export type LogsMessage = { eventPhase: EventPhase.ADDED; podName: string; @@ -153,6 +168,7 @@ export type NotificationMessage = | EventMessage | DevWorkspaceMessage | PodMessage + | ConfigMapMessage | LogsMessage | StatusMessage; export type EventData = { @@ -220,6 +236,18 @@ export function isStatusMessage(message: unknown): message is StatusMessage { ); } +export function isConfigMapMessage( + message: unknown, +): message is ConfigMapMessage { + return ( + message !== undefined && + ((message as ConfigMapMessage).eventPhase === EventPhase.ADDED || + (message as ConfigMapMessage).eventPhase === EventPhase.MODIFIED || + (message as ConfigMapMessage).eventPhase === EventPhase.DELETED) && + (message as ConfigMapMessage).configMap !== undefined + ); +} + export function isLogsMessage(message: unknown): message is LogsMessage { return ( message !== undefined && From 0788f0cd544b07d7a74d3fbe547f0b117116e2d2 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:11:06 +0300 Subject: [PATCH 03/16] feat(backend): add devfile CRUD API with ConfigMap storage Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- packages/dashboard-backend/src/app.ts | 12 + .../src/constants/schemas.ts | 11 + .../src/constants/terminal-themes.ts | 26 + .../src/devfileSchemas/2.0.0/devfile.json | 2852 +++++++++++++++++ .../src/devfileSchemas/2.1.0/devfile.json | 1430 +++++++++ .../src/devfileSchemas/2.2.0/devfile.json | 2037 ++++++++++++ .../devfileSchemas/2.2.1-alpha/devfile.json | 2037 ++++++++++++ .../src/devfileSchemas/2.3.0/devfile.json | 1608 ++++++++++ .../src/devworkspaceClient/__mocks__/index.ts | 7 + .../src/devworkspaceClient/index.ts | 5 + .../services/configMapWatchApi.ts | 121 + .../services/helpers/patchOptions.ts | 9 + .../src/devworkspaceClient/types/index.ts | 1 + .../src/models/restParams.ts | 4 + .../src/routes/api/devfileSchema.ts | 50 + .../src/routes/api/devfiles.ts | 228 ++ .../src/routes/api/devworkspaces.ts | 20 +- .../src/routes/api/helpers/devfile/index.ts | 161 + 18 files changed, 10612 insertions(+), 7 deletions(-) create mode 100644 packages/dashboard-backend/src/constants/terminal-themes.ts create mode 100644 packages/dashboard-backend/src/devfileSchemas/2.0.0/devfile.json create mode 100644 packages/dashboard-backend/src/devfileSchemas/2.1.0/devfile.json create mode 100644 packages/dashboard-backend/src/devfileSchemas/2.2.0/devfile.json create mode 100644 packages/dashboard-backend/src/devfileSchemas/2.2.1-alpha/devfile.json create mode 100644 packages/dashboard-backend/src/devfileSchemas/2.3.0/devfile.json create mode 100644 packages/dashboard-backend/src/devworkspaceClient/services/configMapWatchApi.ts create mode 100644 packages/dashboard-backend/src/routes/api/devfileSchema.ts create mode 100644 packages/dashboard-backend/src/routes/api/devfiles.ts create mode 100644 packages/dashboard-backend/src/routes/api/helpers/devfile/index.ts diff --git a/packages/dashboard-backend/src/app.ts b/packages/dashboard-backend/src/app.ts index 21bc82afce..2053f0997f 100644 --- a/packages/dashboard-backend/src/app.ts +++ b/packages/dashboard-backend/src/app.ts @@ -22,6 +22,8 @@ import { registerCors } from '@/plugins/cors'; import { registerStaticServer } from '@/plugins/staticServer'; import { registerSwagger } from '@/plugins/swagger'; import { registerWebSocket } from '@/plugins/webSocket'; +import { registerAgentsRoute } from '@/routes/api/agents'; +import { registerAiAgentRegistryRoute } from '@/routes/api/aiAgentRegistry'; import { registerAiConfigRoutes } from '@/routes/api/aiConfig'; import { registerAiRegistryRoute } from '@/routes/api/aiRegistry'; import { registerAirGapSampleRoute } from '@/routes/api/airGapSample'; @@ -29,6 +31,8 @@ import { registerBackupRoutes } from '@/routes/api/backup'; import { registerClusterConfigRoute } from '@/routes/api/clusterConfig'; import { registerClusterInfoRoute } from '@/routes/api/clusterInfo'; import { registerDataResolverRoute } from '@/routes/api/dataResolver'; +import { registerDevfilesRoute } from '@/routes/api/devfiles'; +import { registerDevfileSchemaRoute } from '@/routes/api/devfileSchema'; import { registerDevWorkspaceClusterRoutes } from '@/routes/api/devworkspaceCluster'; import { registerDevworkspaceResourcesRoute } from '@/routes/api/devworkspaceResources'; import { registerDevworkspacesRoutes } from '@/routes/api/devworkspaces'; @@ -144,6 +148,14 @@ export default async function buildApp(server: FastifyInstance): Promise = { + dracula: TERMINAL_THEMES.dracula, + dark: TERMINAL_THEMES.dark, + light: { + ...TERMINAL_THEMES.light, + black: '#f7f7f7', + }, +}; diff --git a/packages/dashboard-backend/src/devfileSchemas/2.0.0/devfile.json b/packages/dashboard-backend/src/devfileSchemas/2.0.0/devfile.json new file mode 100644 index 0000000000..3f62b95697 --- /dev/null +++ b/packages/dashboard-backend/src/devfileSchemas/2.0.0/devfile.json @@ -0,0 +1,2852 @@ +{ + "description": "Devfile describes the structure of a cloud-native workspace and development environment.", + "type": "object", + "title": "Devfile schema - Version 2.0.0", + "required": ["schemaVersion"], + "properties": { + "commands": { + "description": "Predefined, ready-to-use, workspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["vscodeTask"] + }, + { + "required": ["vscodeLaunch"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "required": ["component"], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": ["commandLine", "component"], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "List of the workspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + }, + { + "required": ["plugin"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "required": ["image"], + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "default": "/projects" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "plugin": { + "description": "Allows importing a plugin.\n\nPlugins are mainly imported devfiles that contribute components, commands and events as a consistent single unit. They are defined in either YAML files following the devfile syntax, or as `DevWorkspaceTemplate` Kubernetes Custom Resources", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["id"] + }, + { + "required": ["kubernetes"] + } + ], + "properties": { + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["vscodeTask"] + }, + { + "required": ["vscodeLaunch"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "registryUrl": { + "type": "string" + }, + "uri": { + "description": "Uri of a Devfile yaml file", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the workspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + } + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the workspace.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStart": { + "description": "IDs of commands that should be executed before the workspace start. Kubernetes-wise, these commands would typically be executed in init containers of the workspace POD.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the workspace.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Optional devfile description", + "type": "string" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string" + }, + "icon": { + "description": "Optional devfile icon", + "type": "string" + }, + "name": { + "description": "Optional devfile name", + "type": "string" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + } + }, + "additionalProperties": true + }, + "parent": { + "description": "Parent workspace template", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["id"] + }, + { + "required": ["kubernetes"] + } + ], + "properties": { + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["vscodeTask"] + }, + { + "required": ["vscodeLaunch"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + }, + { + "required": ["plugin"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "plugin": { + "description": "Allows importing a plugin.\n\nPlugins are mainly imported devfiles that contribute components, commands and events as a consistent single unit. They are defined in either YAML files following the devfile syntax, or as `DevWorkspaceTemplate` Kubernetes Custom Resources", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["id"] + }, + { + "required": ["kubernetes"] + } + ], + "properties": { + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["vscodeTask"] + }, + { + "required": ["vscodeLaunch"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a workspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the workspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at workspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "vscodeLaunch": { + "description": "Command providing the definition of a VsCode launch action", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + }, + "vscodeTask": { + "description": "Command providing the definition of a VsCode Task", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "inlined": { + "description": "Inlined content of the VsCode configuration", + "type": "string" + }, + "uri": { + "description": "Location as an absolute of relative URI the VsCode configuration will be fetched from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring workspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main workspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main workspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "registryUrl": { + "type": "string" + }, + "uri": { + "description": "Uri of a Devfile yaml file", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["github"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "sparseCheckoutDirs": { + "description": "Populate the project sparsely with selected directories.", + "type": "array", + "items": { + "type": "string" + } + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "registryUrl": { + "type": "string" + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["github"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "uri": { + "description": "Uri of a Devfile yaml file", + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Projects worked on in the workspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["github"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "sparseCheckoutDirs": { + "description": "Populate the project sparsely with selected directories.", + "type": "array", + "items": { + "type": "string" + } + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^2\\.0\\.0$" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["github"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "github": { + "description": "Project's GitHub source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/packages/dashboard-backend/src/devfileSchemas/2.1.0/devfile.json b/packages/dashboard-backend/src/devfileSchemas/2.1.0/devfile.json new file mode 100644 index 0000000000..bc55619d30 --- /dev/null +++ b/packages/dashboard-backend/src/devfileSchemas/2.1.0/devfile.json @@ -0,0 +1,1430 @@ +{ + "description": "Devfile describes the structure of a cloud-native devworkspace and development environment.", + "type": "object", + "title": "Devfile schema - Version 2.1.0", + "required": ["schemaVersion"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Predefined, ready-to-use, devworkspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default.", + "type": "object", + "required": ["component"], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": ["commandLine", "component"], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "required": ["image"], + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "default": "/projects" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + } + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStart": { + "description": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD.", + "type": "array", + "items": { + "type": "string" + } + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Optional devfile description", + "type": "string" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string" + }, + "icon": { + "description": "Optional devfile icon, can be a URI or a relative path in the project", + "type": "string" + }, + "language": { + "description": "Optional devfile language", + "type": "string" + }, + "name": { + "description": "Optional devfile name", + "type": "string" + }, + "projectType": { + "description": "Optional devfile project type", + "type": "string" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "website": { + "description": "Optional devfile website", + "type": "string" + } + }, + "additionalProperties": true + }, + "parent": { + "description": "Parent devworkspace template", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["id"] + }, + { + "required": ["kubernetes"] + } + ], + "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "properties": { + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + } + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { + "description": "Size of the volume", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "registryUrl": { + "description": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the `regsitryURL` when `Id` is used.", + "type": "string" + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "uri": { + "description": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI.", + "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Projects worked on in the devworkspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^2\\.1\\.0$" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Must have at least one remote configured", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/packages/dashboard-backend/src/devfileSchemas/2.2.0/devfile.json b/packages/dashboard-backend/src/devfileSchemas/2.2.0/devfile.json new file mode 100644 index 0000000000..ed4afd332f --- /dev/null +++ b/packages/dashboard-backend/src/devfileSchemas/2.2.0/devfile.json @@ -0,0 +1,2037 @@ +{ + "description": "Devfile describes the structure of a cloud-native devworkspace and development environment.\n\nIDE-targeted variants of the schemas provide the following difference compared to the main schemas:\n- They contain additional non-standard `markdownDescription` attributes that are used by IDEs such a VSCode\nto provide markdown-rendered documentation hovers. \n- They don't contain `default` attributes, since this triggers unwanted addition of defaulted fields during completion in IDEs.", + "type": "object", + "title": "Devfile schema - Version 2.2.0 - IDE-targeted variant", + "required": ["schemaVersion"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "commands": { + "description": "Predefined, ready-to-use, devworkspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false.", + "type": "object", + "required": ["component"], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string", + "markdownDescription": "Describes component that will be applied" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + } + }, + "additionalProperties": false, + "markdownDescription": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false." + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The commands that comprise this composite command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean", + "markdownDescription": "Indicates if the sub-commands should be executed concurrently" + } + }, + "additionalProperties": false, + "markdownDescription": "Composite command that allows executing several sub-commands either sequentially or concurrently" + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": ["commandLine", "component"], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string", + "markdownDescription": "Describes component to which given action relates" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Optional list of environment variables that have to be set before running the command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + } + }, + "additionalProperties": false, + "markdownDescription": "CLI Command executed in an existing component container" + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events." + } + }, + "additionalProperties": false + }, + "markdownDescription": "Predefined, ready-to-use, devworkspace-related commands" + }, + "components": { + "description": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + }, + { + "required": ["image"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "required": ["image"], + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to deployment" + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to service" + } + }, + "additionalProperties": false, + "markdownDescription": "Annotations that should be added to specific resources for this container" + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`" + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean", + "markdownDescription": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true." + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "markdownDescription": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used." + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files." + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string", + "markdownDescription": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`." + } + }, + "additionalProperties": false, + "markdownDescription": "Volume that should be mounted to a component container" + }, + "markdownDescription": "List of volumes mounts that should be mounted is this container." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows adding and configuring devworkspace-related containers" + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "required": ["imageName"], + "oneOf": [ + { + "required": ["dockerfile"] + } + ], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the image should be built during startup.\n\nDefault value is `false`" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["devfileRegistry"] + }, + { + "required": ["git"] + } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the dockerfile build." + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container", + "type": "string", + "markdownDescription": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string", + "markdownDescription": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image." + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used.", + "type": "string", + "markdownDescription": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Devfile Registry source" + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string", + "markdownDescription": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile." + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Git source" + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a privileged builder pod is required.\n\nDefault value is `false`" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string", + "markdownDescription": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying dockerfile type build" + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string", + "markdownDescription": "Name of the image for the resulting outerloop build" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of an image for outer loop builds" + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production." + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin." + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production." + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean", + "markdownDescription": "Ephemeral volumes are not stored persistently across restarts. Defaults to false" + }, + "size": { + "description": "Size of the volume", + "type": "string", + "markdownDescription": "Size of the volume" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of a volume shared by several other components" + } + }, + "additionalProperties": false + }, + "markdownDescription": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components" + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser." + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed after stopping the devworkspace." + }, + "preStart": { + "description": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD." + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed before stopping the devworkspace." + } + }, + "additionalProperties": false, + "markdownDescription": "Bindings of commands to events. Each command is referred-to by its name." + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "architectures": { + "description": "Optional list of processor architectures that the devfile supports, empty list suggests that the devfile can be used on any architecture", + "type": "array", + "uniqueItems": true, + "items": { + "description": "Architecture describes the architecture type", + "type": "string", + "enum": ["amd64", "arm64", "ppc64le", "s390x"], + "markdownDescription": "Architecture describes the architecture type" + }, + "markdownDescription": "Optional list of processor architectures that the devfile supports, empty list suggests that the devfile can be used on any architecture" + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead." + }, + "description": { + "description": "Optional devfile description", + "type": "string", + "markdownDescription": "Optional devfile description" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string", + "markdownDescription": "Optional devfile display name" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string", + "markdownDescription": "Optional devfile global memory limit" + }, + "icon": { + "description": "Optional devfile icon, can be a URI or a relative path in the project", + "type": "string", + "markdownDescription": "Optional devfile icon, can be a URI or a relative path in the project" + }, + "language": { + "description": "Optional devfile language", + "type": "string", + "markdownDescription": "Optional devfile language" + }, + "name": { + "description": "Optional devfile name", + "type": "string", + "markdownDescription": "Optional devfile name" + }, + "projectType": { + "description": "Optional devfile project type", + "type": "string", + "markdownDescription": "Optional devfile project type" + }, + "provider": { + "description": "Optional devfile provider information", + "type": "string", + "markdownDescription": "Optional devfile provider information" + }, + "supportUrl": { + "description": "Optional link to a page that provides support information", + "type": "string", + "markdownDescription": "Optional link to a page that provides support information" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "Optional devfile tags" + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$", + "markdownDescription": "Optional semver-compatible version" + }, + "website": { + "description": "Optional devfile website", + "type": "string", + "markdownDescription": "Optional devfile website" + } + }, + "additionalProperties": true, + "markdownDescription": "Optional metadata" + }, + "parent": { + "description": "Parent devworkspace template", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["id"] + }, + { + "required": ["kubernetes"] + } + ], + "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string", + "markdownDescription": "Describes component that will be applied" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + } + }, + "additionalProperties": false, + "markdownDescription": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false." + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The commands that comprise this composite command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean", + "markdownDescription": "Indicates if the sub-commands should be executed concurrently" + } + }, + "additionalProperties": false, + "markdownDescription": "Composite command that allows executing several sub-commands either sequentially or concurrently" + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string", + "markdownDescription": "Describes component to which given action relates" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Optional list of environment variables that have to be set before running the command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + } + }, + "additionalProperties": false, + "markdownDescription": "CLI Command executed in an existing component container" + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events." + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules." + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + }, + { + "required": ["image"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to deployment" + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to service" + } + }, + "additionalProperties": false, + "markdownDescription": "Annotations that should be added to specific resources for this container" + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`" + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean", + "markdownDescription": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true." + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "markdownDescription": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used." + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files." + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string", + "markdownDescription": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`." + } + }, + "additionalProperties": false, + "markdownDescription": "Volume that should be mounted to a component container" + }, + "markdownDescription": "List of volumes mounts that should be mounted is this container." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows adding and configuring devworkspace-related containers" + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "oneOf": [ + { + "required": ["dockerfile"] + }, + { + "required": ["autoBuild"] + } + ], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the image should be built during startup.\n\nDefault value is `false`" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["devfileRegistry"] + }, + { + "required": ["git"] + } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the dockerfile build." + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container", + "type": "string", + "markdownDescription": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string", + "markdownDescription": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image." + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used.", + "type": "string", + "markdownDescription": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Devfile Registry source" + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string", + "markdownDescription": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile." + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Git source" + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a privileged builder pod is required.\n\nDefault value is `false`" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string", + "markdownDescription": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying dockerfile type build" + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string", + "markdownDescription": "Name of the image for the resulting outerloop build" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of an image for outer loop builds" + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production." + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin." + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production." + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean", + "markdownDescription": "Ephemeral volumes are not stored persistently across restarts. Defaults to false" + }, + "size": { + "description": "Size of the volume", + "type": "string", + "markdownDescription": "Size of the volume" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of a volume shared by several other components" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules." + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string", + "markdownDescription": "Id in a registry that contains a Devfile yaml file" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false, + "markdownDescription": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate" + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string", + "markdownDescription": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name." + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "registryUrl": { + "description": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the `registryUrl` when `id` is used.", + "type": "string", + "markdownDescription": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the `registryUrl` when `id` is used." + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "description": { + "description": "Description of a starter project", + "type": "string", + "markdownDescription": "Description of a starter project" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string", + "markdownDescription": "Sub-directory from a starter project to be used as root for starter project." + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "uri": { + "description": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI.", + "type": "string", + "markdownDescription": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI." + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "version": { + "description": "Specific stack/sample version to pull the parent devfile from, when using id in the parent reference. To specify `version`, `id` must be defined and used as the import reference source. `version` can be either a specific stack version, or `latest`. If no `version` specified, default version will be used.", + "type": "string", + "pattern": "^(latest)|(([1-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?)$", + "markdownDescription": "Specific stack/sample version to pull the parent devfile from, when using id in the parent reference. To specify `version`, `id` must be defined and used as the import reference source. `version` can be either a specific stack version, or `latest`. If no `version` specified, default version will be used." + } + }, + "additionalProperties": false, + "markdownDescription": "Parent devworkspace template" + }, + "projects": { + "description": "Projects worked on in the devworkspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string", + "markdownDescription": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name." + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Projects worked on in the devworkspace, containing names and sources locations" + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^([2-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$", + "markdownDescription": "Devfile schema version" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "description": { + "description": "Description of a starter project", + "type": "string", + "markdownDescription": "Description of a starter project" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string", + "markdownDescription": "Sub-directory from a starter project to be used as root for starter project." + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects" + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source\n\n - element identifiers, e.g. command id, component name, endpoint name, project name\n\n - references to identifiers, e.g. in events, a command's component, container's volume mount name\n\n - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Map of key-value variables used for string replacement in the devfile. Values can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source\n\n - element identifiers, e.g. command id, component name, endpoint name, project name\n\n - references to identifiers, e.g. in events, a command's component, container's volume mount name\n\n - string enums, e.g. command group kind, endpoint exposure" + } + }, + "additionalProperties": false, + "markdownDescription": "Devfile describes the structure of a cloud-native devworkspace and development environment.\n\nIDE-targeted variants of the schemas provide the following difference compared to the main schemas:\n- They contain additional non-standard `markdownDescription` attributes that are used by IDEs such a VSCode\nto provide markdown-rendered documentation hovers. \n- They don't contain `default` attributes, since this triggers unwanted addition of defaulted fields during completion in IDEs." +} diff --git a/packages/dashboard-backend/src/devfileSchemas/2.2.1-alpha/devfile.json b/packages/dashboard-backend/src/devfileSchemas/2.2.1-alpha/devfile.json new file mode 100644 index 0000000000..79c983ab45 --- /dev/null +++ b/packages/dashboard-backend/src/devfileSchemas/2.2.1-alpha/devfile.json @@ -0,0 +1,2037 @@ +{ + "description": "Devfile describes the structure of a cloud-native devworkspace and development environment.\n\nIDE-targeted variants of the schemas provide the following difference compared to the main schemas:\n- They contain additional non-standard `markdownDescription` attributes that are used by IDEs such a VSCode\nto provide markdown-rendered documentation hovers.\n- They don't contain `default` attributes, since this triggers unwanted addition of defaulted fields during completion in IDEs.", + "type": "object", + "title": "Devfile schema - Version 2.2.1-alpha - IDE-targeted variant", + "required": ["schemaVersion"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "commands": { + "description": "Predefined, ready-to-use, devworkspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false.", + "type": "object", + "required": ["component"], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string", + "markdownDescription": "Describes component that will be applied" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + } + }, + "additionalProperties": false, + "markdownDescription": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false." + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The commands that comprise this composite command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean", + "markdownDescription": "Indicates if the sub-commands should be executed concurrently" + } + }, + "additionalProperties": false, + "markdownDescription": "Composite command that allows executing several sub-commands either sequentially or concurrently" + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": ["commandLine", "component"], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string", + "markdownDescription": "Describes component to which given action relates" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Optional list of environment variables that have to be set before running the command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + } + }, + "additionalProperties": false, + "markdownDescription": "CLI Command executed in an existing component container" + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events." + } + }, + "additionalProperties": false + }, + "markdownDescription": "Predefined, ready-to-use, devworkspace-related commands" + }, + "components": { + "description": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + }, + { + "required": ["image"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "required": ["image"], + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to deployment" + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to service" + } + }, + "additionalProperties": false, + "markdownDescription": "Annotations that should be added to specific resources for this container" + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`" + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean", + "markdownDescription": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true." + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "markdownDescription": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used." + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files." + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string", + "markdownDescription": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`." + } + }, + "additionalProperties": false, + "markdownDescription": "Volume that should be mounted to a component container" + }, + "markdownDescription": "List of volumes mounts that should be mounted is this container." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows adding and configuring devworkspace-related containers" + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "required": ["imageName"], + "oneOf": [ + { + "required": ["dockerfile"] + } + ], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the image should be built during startup.\n\nDefault value is `false`" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["devfileRegistry"] + }, + { + "required": ["git"] + } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the dockerfile build." + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container", + "type": "string", + "markdownDescription": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string", + "markdownDescription": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image." + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used.", + "type": "string", + "markdownDescription": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Devfile Registry source" + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string", + "markdownDescription": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile." + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Git source" + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a privileged builder pod is required.\n\nDefault value is `false`" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string", + "markdownDescription": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying dockerfile type build" + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string", + "markdownDescription": "Name of the image for the resulting outerloop build" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of an image for outer loop builds" + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production." + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin." + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production." + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean", + "markdownDescription": "Ephemeral volumes are not stored persistently across restarts. Defaults to false" + }, + "size": { + "description": "Size of the volume", + "type": "string", + "markdownDescription": "Size of the volume" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of a volume shared by several other components" + } + }, + "additionalProperties": false + }, + "markdownDescription": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components" + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser." + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed after stopping the devworkspace." + }, + "preStart": { + "description": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD." + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the devworkspace.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "IDs of commands that should be executed before stopping the devworkspace." + } + }, + "additionalProperties": false, + "markdownDescription": "Bindings of commands to events. Each command is referred-to by its name." + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "architectures": { + "description": "Optional list of processor architectures that the devfile supports, empty list suggests that the devfile can be used on any architecture", + "type": "array", + "uniqueItems": true, + "items": { + "description": "Architecture describes the architecture type", + "type": "string", + "enum": ["amd64", "arm64", "ppc64le", "s390x"], + "markdownDescription": "Architecture describes the architecture type" + }, + "markdownDescription": "Optional list of processor architectures that the devfile supports, empty list suggests that the devfile can be used on any architecture" + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead." + }, + "description": { + "description": "Optional devfile description", + "type": "string", + "markdownDescription": "Optional devfile description" + }, + "displayName": { + "description": "Optional devfile display name", + "type": "string", + "markdownDescription": "Optional devfile display name" + }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string", + "markdownDescription": "Optional devfile global memory limit" + }, + "icon": { + "description": "Optional devfile icon, can be a URI or a relative path in the project", + "type": "string", + "markdownDescription": "Optional devfile icon, can be a URI or a relative path in the project" + }, + "language": { + "description": "Optional devfile language", + "type": "string", + "markdownDescription": "Optional devfile language" + }, + "name": { + "description": "Optional devfile name", + "type": "string", + "markdownDescription": "Optional devfile name" + }, + "projectType": { + "description": "Optional devfile project type", + "type": "string", + "markdownDescription": "Optional devfile project type" + }, + "provider": { + "description": "Optional devfile provider information", + "type": "string", + "markdownDescription": "Optional devfile provider information" + }, + "supportUrl": { + "description": "Optional link to a page that provides support information", + "type": "string", + "markdownDescription": "Optional link to a page that provides support information" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "Optional devfile tags" + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$", + "markdownDescription": "Optional semver-compatible version" + }, + "website": { + "description": "Optional devfile website", + "type": "string", + "markdownDescription": "Optional devfile website" + } + }, + "additionalProperties": true, + "markdownDescription": "Optional metadata" + }, + "parent": { + "description": "Parent devworkspace template", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["id"] + }, + { + "required": ["kubernetes"] + } + ], + "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["apply"] + }, + { + "required": ["composite"] + } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string", + "markdownDescription": "Describes component that will be applied" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + } + }, + "additionalProperties": false, + "markdownDescription": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false." + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The commands that comprise this composite command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean", + "markdownDescription": "Indicates if the sub-commands should be executed concurrently" + } + }, + "additionalProperties": false, + "markdownDescription": "Composite command that allows executing several sub-commands either sequentially or concurrently" + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string", + "markdownDescription": "Describes component to which given action relates" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Optional list of environment variables that have to be set before running the command" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean", + "markdownDescription": "Identifies the default command for a given group kind" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"], + "markdownDescription": "Kind of group the command is part of" + } + }, + "additionalProperties": false, + "markdownDescription": "Defines the group this command is part of" + }, + "hotReloadCapable": { + "description": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Whether the command is capable to reload itself when source code changes. If set to `true` the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is `false`" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string", + "markdownDescription": "Optional label that provides a label for this command to be used in Editor UI menus for example" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one.", + "type": "string", + "markdownDescription": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/\u003cproject-name\u003e). If there are multiple projects, this will point to the directory of the first one." + } + }, + "additionalProperties": false, + "markdownDescription": "CLI Command executed in an existing component container" + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events." + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules." + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["container"] + }, + { + "required": ["kubernetes"] + }, + { + "required": ["openshift"] + }, + { + "required": ["volume"] + }, + { + "required": ["image"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to deployment" + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to service" + } + }, + "additionalProperties": false, + "markdownDescription": "Annotations that should be added to specific resources for this container" + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image." + }, + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`" + }, + "image": { + "type": "string" + }, + "memoryLimit": { + "type": "string" + }, + "memoryRequest": { + "type": "string" + }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean", + "markdownDescription": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true." + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "markdownDescription": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used." + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files." + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`.", + "type": "string", + "markdownDescription": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/\u003cname\u003e`." + } + }, + "additionalProperties": false, + "markdownDescription": "Volume that should be mounted to a component container" + }, + "markdownDescription": "List of volumes mounts that should be mounted is this container." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows adding and configuring devworkspace-related containers" + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "oneOf": [ + { + "required": ["dockerfile"] + }, + { + "required": ["autoBuild"] + } + ], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the image should be built during startup.\n\nDefault value is `false`" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["devfileRegistry"] + }, + { + "required": ["git"] + } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The arguments to supply to the dockerfile build." + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container", + "type": "string", + "markdownDescription": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string", + "markdownDescription": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image." + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used.", + "type": "string", + "markdownDescription": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Devfile Registry source" + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string", + "markdownDescription": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile." + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Dockerfile's Git source" + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Specify if a privileged builder pod is required.\n\nDefault value is `false`" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string", + "markdownDescription": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying dockerfile type build" + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string", + "markdownDescription": "Name of the image for the resulting outerloop build" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of an image for outer loop builds" + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production." + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin." + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [ + { + "required": ["uri"] + }, + { + "required": ["inlined"] + } + ], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean", + "markdownDescription": "Defines if the component should be deployed during startup.\n\nDefault value is `false`" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Annotations to be added to Kubernetes Ingress or Openshift Route" + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\"," + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"], + "markdownDescription": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`" + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "Path of the endpoint URL", + "type": "string", + "markdownDescription": "Path of the endpoint URL" + }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"], + "markdownDescription": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`" + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean", + "markdownDescription": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`." + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer", + "markdownDescription": "Port number to be used within the container component. The same port cannot be used by two different container components." + } + }, + "additionalProperties": false + } + }, + "inlined": { + "description": "Inlined manifest", + "type": "string", + "markdownDescription": "Inlined manifest" + }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string", + "markdownDescription": "Location in a file fetched from a uri." + } + }, + "additionalProperties": false, + "markdownDescription": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production." + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean", + "markdownDescription": "Ephemeral volumes are not stored persistently across restarts. Defaults to false" + }, + "size": { + "description": "Size of the volume", + "type": "string", + "markdownDescription": "Size of the volume" + } + }, + "additionalProperties": false, + "markdownDescription": "Allows specifying the definition of a volume shared by several other components" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules." + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string", + "markdownDescription": "Id in a registry that contains a Devfile yaml file" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false, + "markdownDescription": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate" + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string", + "markdownDescription": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name." + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "registryUrl": { + "description": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the `registryUrl` when `id` is used.", + "type": "string", + "markdownDescription": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the `registryUrl` when `id` is used." + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "description": { + "description": "Description of a starter project", + "type": "string", + "markdownDescription": "Description of a starter project" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string", + "markdownDescription": "Sub-directory from a starter project to be used as root for starter project." + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "uri": { + "description": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI.", + "type": "string", + "markdownDescription": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI." + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "version": { + "description": "Specific stack/sample version to pull the parent devfile from, when using id in the parent reference. To specify `version`, `id` must be defined and used as the import reference source. `version` can be either a specific stack version, or `latest`. If no `version` specified, default version will be used.", + "type": "string", + "pattern": "^(latest)|(([1-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?)$", + "markdownDescription": "Specific stack/sample version to pull the parent devfile from, when using id in the parent reference. To specify `version`, `id` must be defined and used as the import reference source. `version` can be either a specific stack version, or `latest`. If no `version` specified, default version will be used." + } + }, + "additionalProperties": false, + "markdownDescription": "Parent devworkspace template" + }, + "projects": { + "description": "Projects worked on in the devworkspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string", + "markdownDescription": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name." + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "Projects worked on in the devworkspace, containing names and sources locations" + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^([2-9])\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$", + "markdownDescription": "Devfile schema version" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { + "required": ["git"] + }, + { + "required": ["zip"] + } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, + "description": { + "description": "Description of a starter project", + "type": "string", + "markdownDescription": "Description of a starter project" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string", + "markdownDescription": "The remote name should be used as init. Required if there are more than one remote configured" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string", + "markdownDescription": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found." + } + }, + "additionalProperties": false, + "markdownDescription": "Defines from what the project should be checked out. Required if there are more than one remote configured" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects \u0026 Image Component's Git source can only have at most one remote configured." + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Git source" + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "markdownDescription": "Project name" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string", + "markdownDescription": "Sub-directory from a starter project to be used as root for starter project." + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string", + "markdownDescription": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH" + } + }, + "additionalProperties": false, + "markdownDescription": "Project's Zip source" + } + }, + "additionalProperties": false + }, + "markdownDescription": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects" + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source\n\n - element identifiers, e.g. command id, component name, endpoint name, project name\n\n - references to identifiers, e.g. in events, a command's component, container's volume mount name\n\n - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Map of key-value variables used for string replacement in the devfile. Values can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source\n\n - element identifiers, e.g. command id, component name, endpoint name, project name\n\n - references to identifiers, e.g. in events, a command's component, container's volume mount name\n\n - string enums, e.g. command group kind, endpoint exposure" + } + }, + "additionalProperties": false, + "markdownDescription": "Devfile describes the structure of a cloud-native devworkspace and development environment.\n\nIDE-targeted variants of the schemas provide the following difference compared to the main schemas:\n- They contain additional non-standard `markdownDescription` attributes that are used by IDEs such a VSCode\nto provide markdown-rendered documentation hovers.\n- They don't contain `default` attributes, since this triggers unwanted addition of defaulted fields during completion in IDEs." +} diff --git a/packages/dashboard-backend/src/devfileSchemas/2.3.0/devfile.json b/packages/dashboard-backend/src/devfileSchemas/2.3.0/devfile.json new file mode 100644 index 0000000000..b35ffe31ca --- /dev/null +++ b/packages/dashboard-backend/src/devfileSchemas/2.3.0/devfile.json @@ -0,0 +1,1608 @@ +{ + "description": "Devfile describes the structure of a cloud-native devworkspace and development environment.", + "type": "object", + "title": "Devfile schema - Version 2.3.0", + "required": ["schemaVersion"], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Predefined, ready-to-use, devworkspace-related commands", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { "required": ["exec"] }, + { "required": ["apply"] }, + { "required": ["composite"] } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false.", + "type": "object", + "required": ["component"], + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { "type": "string" } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "required": ["commandLine", "component"], + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { "name": { "type": "string" }, "value": { "type": "string" } }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "required": ["kind"], + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Specify whether the command is restarted or not when the source code changes. If set to `true` the command won't be restarted. A *hotReloadCapable* `run` or `debug` command is expected to handle file changes on its own and won't be restarted. A *hotReloadCapable* `build` command is expected to be executed only once and won't be executed again. This field is taken into account only for commands `build`, `run` and `debug` with `isDefault` set to `true`.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "List of the devworkspace components, such as editor and plugins, user-provided containers, or other types of components", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { "required": ["container"] }, + { "required": ["kubernetes"] }, + { "required": ["openshift"] }, + { "required": ["volume"] }, + { "required": ["image"] } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "required": ["image"], + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { "type": "string" } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { "type": "string" } + }, + "cpuLimit": { "type": "string" }, + "cpuRequest": { "type": "string" }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { "description": "Path of the endpoint URL", "type": "string" }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { "name": { "type": "string" }, "value": { "type": "string" } }, + "additionalProperties": false + } + }, + "image": { "type": "string" }, + "memoryLimit": { "type": "string" }, + "memoryRequest": { "type": "string" }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string", + "default": "/projects" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "required": ["imageName"], + "oneOf": [{ "required": ["dockerfile"] }], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is `false`", + "type": "boolean" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { "required": ["uri"] }, + { "required": ["devfileRegistry"] }, + { "required": ["git"] } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { "type": "string" } + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container", + "type": "string" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string" + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used.", + "type": "string" + } + }, + "additionalProperties": false + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is `false`", + "type": "boolean" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string" + } + }, + "additionalProperties": false + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string" + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [{ "required": ["uri"] }, { "required": ["inlined"] }], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { "description": "Path of the endpoint URL", "type": "string" }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { "description": "Inlined manifest", "type": "string" }, + "uri": { "description": "Location in a file fetched from a uri.", "type": "string" } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [{ "required": ["uri"] }, { "required": ["inlined"] }], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "targetPort"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "default": "public", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { "description": "Path of the endpoint URL", "type": "string" }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "default": "http", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { "description": "Inlined manifest", "type": "string" }, + "uri": { "description": "Location in a file fetched from a uri.", "type": "string" } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { "description": "Size of the volume", "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "dependentProjects": { + "description": "Additional projects related to the main project in the devfile, contianing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [{ "required": ["git"] }, { "required": ["zip"] }], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "events": { + "description": "Bindings of commands to events. Each command is referred-to by its name.", + "type": "object", + "properties": { + "postStart": { + "description": "IDs of commands that should be executed after the devworkspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", + "type": "array", + "items": { "type": "string" } + }, + "postStop": { + "description": "IDs of commands that should be executed after stopping the devworkspace.", + "type": "array", + "items": { "type": "string" } + }, + "preStart": { + "description": "IDs of commands that should be executed before the devworkspace start. Kubernetes-wise, these commands would typically be executed in init containers of the devworkspace POD.", + "type": "array", + "items": { "type": "string" } + }, + "preStop": { + "description": "IDs of commands that should be executed before stopping the devworkspace.", + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, + "metadata": { + "description": "Optional metadata", + "type": "object", + "properties": { + "architectures": { + "description": "Optional list of processor architectures that the devfile supports, empty list suggests that the devfile can be used on any architecture", + "type": "array", + "uniqueItems": true, + "items": { + "description": "Architecture describes the architecture type", + "type": "string", + "enum": ["amd64", "arm64", "ppc64le", "s390x"] + } + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", + "type": "object", + "additionalProperties": true + }, + "description": { "description": "Optional devfile description", "type": "string" }, + "displayName": { "description": "Optional devfile display name", "type": "string" }, + "globalMemoryLimit": { + "description": "Optional devfile global memory limit", + "type": "string" + }, + "icon": { + "description": "Optional devfile icon, can be a URI or a relative path in the project", + "type": "string" + }, + "language": { "description": "Optional devfile language", "type": "string" }, + "name": { "description": "Optional devfile name", "type": "string" }, + "projectType": { "description": "Optional devfile project type", "type": "string" }, + "provider": { "description": "Optional devfile provider information", "type": "string" }, + "supportUrl": { + "description": "Optional link to a page that provides support information", + "type": "string" + }, + "tags": { + "description": "Optional devfile tags", + "type": "array", + "items": { "type": "string" } + }, + "version": { + "description": "Optional semver-compatible version", + "type": "string", + "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "website": { "description": "Optional devfile website", "type": "string" } + }, + "additionalProperties": true + }, + "parent": { + "description": "Parent devworkspace template", + "type": "object", + "oneOf": [{ "required": ["uri"] }, { "required": ["id"] }, { "required": ["kubernetes"] }], + "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, + "commands": { + "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["id"], + "oneOf": [ + { "required": ["exec"] }, + { "required": ["apply"] }, + { "required": ["composite"] } + ], + "properties": { + "apply": { + "description": "Command that consists in applying a given component definition, typically bound to a devworkspace event.\n\nFor example, when an `apply` command is bound to a `preStart` event, and references a `container` component, it will start the container as a K8S initContainer in the devworkspace POD, unless the component has its `dedicatedPod` field set to `true`.\n\nWhen no `apply` command exist for a given component, it is assumed the component will be applied at devworkspace start by default, unless `deployByDefault` for that component is set to false.", + "type": "object", + "properties": { + "component": { + "description": "Describes component that will be applied", + "type": "string" + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + } + }, + "additionalProperties": false + }, + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "composite": { + "description": "Composite command that allows executing several sub-commands either sequentially or concurrently", + "type": "object", + "properties": { + "commands": { + "description": "The commands that comprise this composite command", + "type": "array", + "items": { "type": "string" } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"] + } + }, + "additionalProperties": false + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "parallel": { + "description": "Indicates if the sub-commands should be executed concurrently", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "exec": { + "description": "CLI Command executed in an existing component container", + "type": "object", + "properties": { + "commandLine": { + "description": "The actual command-line string\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + }, + "component": { + "description": "Describes component to which given action relates", + "type": "string" + }, + "env": { + "description": "Optional list of environment variables that have to be set before running the command", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { "name": { "type": "string" }, "value": { "type": "string" } }, + "additionalProperties": false + } + }, + "group": { + "description": "Defines the group this command is part of", + "type": "object", + "properties": { + "isDefault": { + "description": "Identifies the default command for a given group kind", + "type": "boolean" + }, + "kind": { + "description": "Kind of group the command is part of", + "type": "string", + "enum": ["build", "run", "test", "debug", "deploy"] + } + }, + "additionalProperties": false + }, + "hotReloadCapable": { + "description": "Specify whether the command is restarted or not when the source code changes. If set to `true` the command won't be restarted. A *hotReloadCapable* `run` or `debug` command is expected to handle file changes on its own and won't be restarted. A *hotReloadCapable* `build` command is expected to be executed only once and won't be executed again. This field is taken into account only for commands `build`, `run` and `debug` with `isDefault` set to `true`.\n\nDefault value is `false`", + "type": "boolean" + }, + "label": { + "description": "Optional label that provides a label for this command to be used in Editor UI menus for example", + "type": "string" + }, + "workingDir": { + "description": "Working directory where the command should be executed\n\nSpecial variables that can be used:\n\n - `$PROJECTS_ROOT`: A path where projects sources are mounted as defined by container component's sourceMapping.\n\n - `$PROJECT_SOURCE`: A path to a project source ($PROJECTS_ROOT/). If there are multiple projects, this will point to the directory of the first one.", + "type": "string" + } + }, + "additionalProperties": false + }, + "id": { + "description": "Mandatory identifier that allows referencing this command in composite commands, from a parent, or in events.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + }, + "additionalProperties": false + } + }, + "components": { + "description": "Overrides of components encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [ + { "required": ["container"] }, + { "required": ["kubernetes"] }, + { "required": ["openshift"] }, + { "required": ["volume"] }, + { "required": ["image"] } + ], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "container": { + "description": "Allows adding and configuring devworkspace-related containers", + "type": "object", + "properties": { + "annotation": { + "description": "Annotations that should be added to specific resources for this container", + "type": "object", + "properties": { + "deployment": { + "description": "Annotations to be added to deployment", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "service": { + "description": "Annotations to be added to service", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "args": { + "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { "type": "string" } + }, + "command": { + "description": "The command to run in the dockerimage component instead of the default one provided in the image.\n\nDefaults to an empty array, meaning use whatever is defined in the image.", + "type": "array", + "items": { "type": "string" } + }, + "cpuLimit": { "type": "string" }, + "cpuRequest": { "type": "string" }, + "dedicatedPod": { + "description": "Specify if a container should run in its own separated pod, instead of running as part of the main development environment pod.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { "description": "Path of the endpoint URL", "type": "string" }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "env": { + "description": "Environment variables used in this container.\n\nThe following variables are reserved and cannot be overridden via env:\n\n - `$PROJECTS_ROOT`\n\n - `$PROJECT_SOURCE`", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { "name": { "type": "string" }, "value": { "type": "string" } }, + "additionalProperties": false + } + }, + "image": { "type": "string" }, + "memoryLimit": { "type": "string" }, + "memoryRequest": { "type": "string" }, + "mountSources": { + "description": "Toggles whether or not the project source code should be mounted in the component.\n\nDefaults to true for all component types except plugins and components that set `dedicatedPod` to true.", + "type": "boolean" + }, + "sourceMapping": { + "description": "Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the default value of /projects is used.", + "type": "string" + }, + "volumeMounts": { + "description": "List of volumes mounts that should be mounted is this container.", + "type": "array", + "items": { + "description": "Volume that should be mounted to a component container", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The volume mount name is the name of an existing `Volume` component. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { + "description": "The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/`.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "image": { + "description": "Allows specifying the definition of an image for outer loop builds", + "type": "object", + "oneOf": [{ "required": ["dockerfile"] }, { "required": ["autoBuild"] }], + "properties": { + "autoBuild": { + "description": "Defines if the image should be built during startup.\n\nDefault value is `false`", + "type": "boolean" + }, + "dockerfile": { + "description": "Allows specifying dockerfile type build", + "type": "object", + "oneOf": [ + { "required": ["uri"] }, + { "required": ["devfileRegistry"] }, + { "required": ["git"] } + ], + "properties": { + "args": { + "description": "The arguments to supply to the dockerfile build.", + "type": "array", + "items": { "type": "string" } + }, + "buildContext": { + "description": "Path of source directory to establish build context. Defaults to ${PROJECT_SOURCE} in the container", + "type": "string" + }, + "devfileRegistry": { + "description": "Dockerfile's Devfile Registry source", + "type": "object", + "properties": { + "id": { + "description": "Id in a devfile registry that contains a Dockerfile. The src in the OCI registry required for the Dockerfile build will be downloaded for building the image.", + "type": "string" + }, + "registryUrl": { + "description": "Devfile Registry URL to pull the Dockerfile from when using the Devfile Registry as Dockerfile src. To ensure the Dockerfile gets resolved consistently in different environments, it is recommended to always specify the `devfileRegistryUrl` when `Id` is used.", + "type": "string" + } + }, + "additionalProperties": false + }, + "git": { + "description": "Dockerfile's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "fileLocation": { + "description": "Location of the Dockerfile in the Git repository when using git as Dockerfile src. Defaults to Dockerfile.", + "type": "string" + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "rootRequired": { + "description": "Specify if a privileged builder pod is required.\n\nDefault value is `false`", + "type": "boolean" + }, + "uri": { + "description": "URI Reference of a Dockerfile. It can be a full URL or a relative URI from the current devfile as the base URI.", + "type": "string" + } + }, + "additionalProperties": false + }, + "imageName": { + "description": "Name of the image for the resulting outerloop build", + "type": "string" + } + }, + "additionalProperties": false + }, + "kubernetes": { + "description": "Allows importing into the devworkspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [{ "required": ["uri"] }, { "required": ["inlined"] }], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { "description": "Path of the endpoint URL", "type": "string" }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { "description": "Inlined manifest", "type": "string" }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "name": { + "description": "Mandatory name that allows referencing the component from other elements (such as commands) or from an external devfile that may reference this component through a parent or a plugin.", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "openshift": { + "description": "Allows importing into the devworkspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production.", + "type": "object", + "oneOf": [{ "required": ["uri"] }, { "required": ["inlined"] }], + "properties": { + "deployByDefault": { + "description": "Defines if the component should be deployed during startup.\n\nDefault value is `false`", + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "annotation": { + "description": "Annotations to be added to Kubernetes Ingress or Openshift Route", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "attributes": { + "description": "Map of implementation-dependant string-based free-form attributes.\n\nExamples of Che-specific attributes:\n- cookiesAuthEnabled: \"true\" / \"false\",\n- type: \"terminal\" / \"ide\" / \"ide-dev\",", + "type": "object", + "additionalProperties": true + }, + "exposure": { + "description": "Describes how the endpoint should be exposed on the network.\n- `public` means that the endpoint will be exposed on the public network, typically through a K8S ingress or an OpenShift route.\n- `internal` means that the endpoint will be exposed internally outside of the main devworkspace POD, typically by K8S services, to be consumed by other elements running on the same cloud internal network.\n- `none` means that the endpoint will not be exposed and will only be accessible inside the main devworkspace POD, on a local address.\n\nDefault value is `public`", + "type": "string", + "enum": ["public", "internal", "none"] + }, + "name": { + "type": "string", + "maxLength": 15, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "path": { "description": "Path of the endpoint URL", "type": "string" }, + "protocol": { + "description": "Describes the application and transport protocols of the traffic that will go through this endpoint.\n- `http`: Endpoint will have `http` traffic, typically on a TCP connection. It will be automaticaly promoted to `https` when the `secure` field is set to `true`.\n- `https`: Endpoint will have `https` traffic, typically on a TCP connection.\n- `ws`: Endpoint will have `ws` traffic, typically on a TCP connection. It will be automaticaly promoted to `wss` when the `secure` field is set to `true`.\n- `wss`: Endpoint will have `wss` traffic, typically on a TCP connection.\n- `tcp`: Endpoint will have traffic on a TCP connection, without specifying an application protocol.\n- `udp`: Endpoint will have traffic on an UDP connection, without specifying an application protocol.\n\nDefault value is `http`", + "type": "string", + "enum": ["http", "https", "ws", "wss", "tcp", "udp"] + }, + "secure": { + "description": "Describes whether the endpoint should be secured and protected by some authentication process. This requires a protocol of `https` or `wss`.", + "type": "boolean" + }, + "targetPort": { + "description": "Port number to be used within the container component. The same port cannot be used by two different container components.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "inlined": { "description": "Inlined manifest", "type": "string" }, + "uri": { + "description": "Location in a file fetched from a uri.", + "type": "string" + } + }, + "additionalProperties": false + }, + "volume": { + "description": "Allows specifying the definition of a volume shared by several other components", + "type": "object", + "properties": { + "ephemeral": { + "description": "Ephemeral volumes are not stored persistently across restarts. Defaults to false", + "type": "boolean" + }, + "size": { "description": "Size of the volume", "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "dependentProjects": { + "description": "Overrides of dependentProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [{ "required": ["git"] }, { "required": ["zip"] }], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "id": { + "description": "Id in a registry that contains a Devfile yaml file", + "type": "string" + }, + "kubernetes": { + "description": "Reference to a Kubernetes CRD of type DevWorkspaceTemplate", + "type": "object", + "required": ["name"], + "properties": { "name": { "type": "string" }, "namespace": { "type": "string" } }, + "additionalProperties": false + }, + "projects": { + "description": "Overrides of projects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [{ "required": ["git"] }, { "required": ["zip"] }], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "registryUrl": { + "description": "Registry URL to pull the parent devfile from when using id in the parent reference. To ensure the parent devfile gets resolved consistently in different environments, it is recommended to always specify the `registryUrl` when `id` is used.", + "type": "string" + }, + "starterProjects": { + "description": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [{ "required": ["git"] }, { "required": ["zip"] }], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { + "description": "Description of a starter project", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "uri": { + "description": "URI Reference of a parent devfile YAML file. It can be a full URL or a relative URI with the current devfile as the base URI.", + "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { "type": "string" } + }, + "version": { + "description": "Specific stack/sample version to pull the parent devfile from, when using id in the parent reference. To specify `version`, `id` must be defined and used as the import reference source. `version` can be either a specific stack version, or `latest`. If no `version` specified, default version will be used.", + "type": "string", + "pattern": "^(latest)|(([1-9])\\.([0-9]+)\\.([0-9]+)(-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?)$" + } + }, + "additionalProperties": false + }, + "projects": { + "description": "Projects worked on in the devworkspace, containing names and sources locations", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [{ "required": ["git"] }, { "required": ["zip"] }], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "clonePath": { + "description": "Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", + "type": "string" + }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "schemaVersion": { + "description": "Devfile schema version", + "type": "string", + "pattern": "^([2-9])\\.([0-9]+)\\.([0-9]+)(-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + }, + "starterProjects": { + "description": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects", + "type": "array", + "items": { + "type": "object", + "required": ["name"], + "oneOf": [{ "required": ["git"] }, { "required": ["zip"] }], + "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, + "description": { "description": "Description of a starter project", "type": "string" }, + "git": { + "description": "Project's Git source", + "type": "object", + "required": ["remotes"], + "properties": { + "checkoutFrom": { + "description": "Defines from what the project should be checked out. Required if there are more than one remote configured", + "type": "object", + "properties": { + "remote": { + "description": "The remote name should be used as init. Required if there are more than one remote configured", + "type": "string" + }, + "revision": { + "description": "The revision to checkout from. Should be branch name, tag or commit id. Default branch is used if missing or specified revision is not found.", + "type": "string" + } + }, + "additionalProperties": false + }, + "remotes": { + "description": "The remotes map which should be initialized in the git project. Projects must have at least one remote configured while StarterProjects & Image Component's Git source can only have at most one remote configured.", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + }, + "name": { + "description": "Project name", + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "subDir": { + "description": "Sub-directory from a starter project to be used as root for starter project.", + "type": "string" + }, + "zip": { + "description": "Project's Zip source", + "type": "object", + "properties": { + "location": { + "description": "Zip project's source location address. Should be file path of the archive, e.g. file://$FILE_PATH", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source\n\n - element identifiers, e.g. command id, component name, endpoint name, project name\n\n - references to identifiers, e.g. in events, a command's component, container's volume mount name\n\n - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false +} diff --git a/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts b/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts index 49ded318f5..c6a3ed8ad7 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts @@ -10,6 +10,8 @@ * Red Hat, Inc. - initial API and implementation */ +import { api } from '@eclipse-che/common'; + import { IAiProviderKeyApi, IAiRegistryApi, @@ -30,12 +32,17 @@ import { ISccPermissionApi, IServerConfigApi, IShhKeysApi, + IUserProfileApi, + IWatcherService, IWorkspacePreferencesApi, } from '@/devworkspaceClient/types'; import { IDevWorkspaceClient } from '..'; export class DevWorkspaceClient implements IDevWorkspaceClient { + get configMapWatchApi(): IWatcherService { + throw new Error('Method not implemented.'); + } get eventApi(): IEventApi { throw new Error('Method not implemented.'); } diff --git a/packages/dashboard-backend/src/devworkspaceClient/index.ts b/packages/dashboard-backend/src/devworkspaceClient/index.ts index 4854485f51..3212154b19 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/index.ts @@ -15,6 +15,7 @@ import * as k8s from '@kubernetes/client-node'; import { AiProviderKeyApiService } from '@/devworkspaceClient/services/aiProviderKeyApi'; import { AiRegistryApiService } from '@/devworkspaceClient/services/aiRegistryApi'; import { AirGapSampleApiService } from '@/devworkspaceClient/services/airGapSampleApi'; +import { ConfigMapWatchApiService } from '@/devworkspaceClient/services/configMapWatchApi'; import { DevWorkspaceApiService } from '@/devworkspaceClient/services/devWorkspaceApi'; import { DevWorkspaceClusterApiService } from '@/devworkspaceClient/services/devWorkspaceClusterApiService'; import { DevWorkspaceTemplateApiService } from '@/devworkspaceClient/services/devWorkspaceTemplateApi'; @@ -65,6 +66,10 @@ export class DevWorkspaceClient implements IDevWorkspaceClient { this.kubeConfig = kc; } + get configMapWatchApi(): ConfigMapWatchApiService { + return new ConfigMapWatchApiService(this.kubeConfig); + } + get eventApi(): IEventApi { return new EventApiService(this.kubeConfig); } diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/configMapWatchApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/configMapWatchApi.ts new file mode 100644 index 0000000000..718be26323 --- /dev/null +++ b/packages/dashboard-backend/src/devworkspaceClient/services/configMapWatchApi.ts @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { api } from '@eclipse-che/common'; +import * as k8s from '@kubernetes/client-node'; +import { V1ConfigMap, V1Status } from '@kubernetes/client-node'; + +import { prepareCustomObjectWatch } from '@/devworkspaceClient/services/helpers/prepareCustomObjectWatch'; +import { IWatcherService } from '@/devworkspaceClient/types'; +import { MessageListener } from '@/services/types/Observer'; +import { logger } from '@/utils/logger'; + +const CONFIGMAP_NAME = 'devfile-creator-storage'; + +/** + * Watches the devfile-creator-storage ConfigMap for changes. + * Uses a fieldSelector to only receive events for this specific ConfigMap. + */ +export class ConfigMapWatchApiService implements IWatcherService { + private readonly customObjectWatch: k8s.Watch; + private stopWatch?: () => void; + + constructor(kubeConfig: k8s.KubeConfig) { + this.customObjectWatch = prepareCustomObjectWatch(kubeConfig); + } + + public async watchInNamespace( + listener: MessageListener, + params: api.webSocket.SubscribeParams, + ): Promise { + const path = `/api/v1/namespaces/${params.namespace}/configmaps`; + const queryParams = { + watch: true, + resourceVersion: params.resourceVersion, + fieldSelector: `metadata.name=${CONFIGMAP_NAME}`, + }; + + this.stopWatching(); + + let abortController: AbortController | undefined; + try { + abortController = await this.customObjectWatch.watch( + path, + queryParams, + (eventPhase: string, apiObj: V1ConfigMap | V1Status) => + this.handleWatchMessage(eventPhase, apiObj, listener, params), + (error: unknown) => { + if (error instanceof Error && error.name === 'AbortError') { + return; + } + try { + logger.warn(error, `Stopped watching ${path}.`); + this.notifyWatchError(error, listener, params); + abortController?.abort(); + } catch (callbackError) { + logger.error(callbackError, `Error in ConfigMap watch done callback for ${path}.`); + } + }, + ); + } catch (error) { + logger.warn(error, `Failed to start watching ${path}.`); + this.notifyWatchError(error, listener, params); + return; + } + + this.stopWatch = () => abortController?.abort(); + } + + private handleWatchMessage( + eventPhase: string, + apiObj: V1ConfigMap | V1Status, + listener: MessageListener, + params: api.webSocket.SubscribeParams, + ): void { + switch (eventPhase) { + case api.webSocket.EventPhase.ADDED: + case api.webSocket.EventPhase.MODIFIED: + case api.webSocket.EventPhase.DELETED: { + const configMap = apiObj as V1ConfigMap; + listener({ eventPhase, configMap }); + break; + } + case api.webSocket.EventPhase.ERROR: { + const status = apiObj as V1Status; + listener({ eventPhase, status, params }); + break; + } + } + } + + private notifyWatchError( + error: unknown, + listener: MessageListener, + params: api.webSocket.SubscribeParams, + ): void { + const status: V1Status = { + kind: 'Status', + apiVersion: 'v1', + status: 'Failure', + message: error instanceof Error ? error.message : String(error), + code: (error as { statusCode?: number }).statusCode, + }; + listener({ eventPhase: api.webSocket.EventPhase.ERROR, status, params }); + } + + public stopWatching(): void { + this.stopWatch?.(); + this.stopWatch = undefined; + } +} diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/patchOptions.ts b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/patchOptions.ts index 26c28c1b2d..6620a5a124 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/patchOptions.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/patchOptions.ts @@ -49,3 +49,12 @@ export const STRATEGIC_MERGE_PATCH_OPTIONS: ConfigurationOptions = wrapOptions({ export const JSON_PATCH_OPTIONS: ConfigurationOptions = wrapOptions({ middleware: [createPatchMiddleware(PatchStrategy.JsonPatch)], })!; + +/** + * Configuration options for patch operations that need to delete keys from objects. + * Uses json-merge-patch (RFC 7396): setting a key to null removes it. + * Required for deleting keys from ConfigMap data (strategic merge patch can't delete keys). + */ +export const JSON_MERGE_PATCH_OPTIONS: ConfigurationOptions = wrapOptions({ + middleware: [createPatchMiddleware(PatchStrategy.MergePatch)], +})!; diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 6e8dc993df..d2619007b7 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -508,6 +508,7 @@ export interface IPersonalAccessTokenApi { export interface IDevWorkspaceClient { devWorkspaceTemplateApi: IDevWorkspaceTemplateApi; devworkspaceApi: IDevWorkspaceApi; + configMapWatchApi: IWatcherService; dockerConfigApi: IDockerConfigApi; eventApi: IEventApi; kubeConfigApi: IKubeConfigApi; diff --git a/packages/dashboard-backend/src/models/restParams.ts b/packages/dashboard-backend/src/models/restParams.ts index 2e855d110e..e282fdebb5 100644 --- a/packages/dashboard-backend/src/models/restParams.ts +++ b/packages/dashboard-backend/src/models/restParams.ts @@ -25,6 +25,10 @@ export interface IDevWorkspaceSpecParams { devworkspace: V1alpha2DevWorkspace; } +export interface IDevfileVersionParams { + version: string; +} + export interface IYamlResolverParams { url: string; } diff --git a/packages/dashboard-backend/src/routes/api/devfileSchema.ts b/packages/dashboard-backend/src/routes/api/devfileSchema.ts new file mode 100644 index 0000000000..6613477259 --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/devfileSchema.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; +import { JSONSchema7 } from 'json-schema'; + +import { baseApiPath } from '@/constants/config'; +import { devfileVersionSchema } from '@/constants/schemas'; +import * as devfileSchemaV200 from '@/devfileSchemas/2.0.0/devfile.json'; +import * as devfileSchemaV210 from '@/devfileSchemas/2.1.0/devfile.json'; +import * as devfileSchemaV220 from '@/devfileSchemas/2.2.0/devfile.json'; +import * as devfileSchemaV230 from '@/devfileSchemas/2.3.0/devfile.json'; +import { restParams } from '@/models'; +import { getSchema } from '@/services/helpers'; + +const tags = ['Devfile']; + +export function registerDevfileSchemaRoute(server: FastifyInstance) { + server.get( + `${baseApiPath}/devfile`, + getSchema({ tags, query: devfileVersionSchema }), + async function ( + request: FastifyRequest, + reply: FastifyReply, + ): Promise { + const { version } = request.query as restParams.IDevfileVersionParams; + switch (version) { + case '2.0.0': + return devfileSchemaV200 as unknown as JSONSchema7; + case '2.1.0': + return devfileSchemaV210 as unknown as JSONSchema7; + case '2.2.0': + return devfileSchemaV220 as unknown as JSONSchema7; + case 'latest': + case '2.3.0': + return devfileSchemaV230 as unknown as JSONSchema7; + } + reply.code(404); + }, + ); +} diff --git a/packages/dashboard-backend/src/routes/api/devfiles.ts b/packages/dashboard-backend/src/routes/api/devfiles.ts new file mode 100644 index 0000000000..98cad9ca72 --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/devfiles.ts @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; + +import { baseApiPath } from '@/constants/config'; +import { namespacedSchema } from '@/constants/schemas'; +import { + JSON_MERGE_PATCH_OPTIONS, + STRATEGIC_MERGE_PATCH_OPTIONS, +} from '@/devworkspaceClient/services/helpers/patchOptions'; +import { restParams } from '@/models'; +import { + CONFIGMAP_NAME, + DevfileIdParams, + generateDevfileId, + getCoreV1Api, + getOrCreateConfigMap, + MODIFIED_ANNOTATION_PREFIX, + parseEntries, +} from '@/routes/api/helpers/devfile'; +import { getToken } from '@/routes/api/helpers/getToken'; +import { getSchema } from '@/services/helpers'; + +const tags = ['Devfiles']; + +const namespacedIdSchema = { + type: 'object', + required: ['namespace', 'id'], + properties: { + namespace: { type: 'string', description: 'Kubernetes namespace of the user' }, + id: { type: 'string', description: 'UUID of the devfile entry in the ConfigMap' }, + }, +}; + +const MAX_DEVFILE_CONTENT_SIZE = 524288; // 512 KiB + +const devfileContentBodySchema = { + type: 'object', + required: ['content'], + properties: { + content: { + type: 'string', + minLength: 1, + maxLength: MAX_DEVFILE_CONTENT_SIZE, + description: 'YAML content of the devfile (max 512 KiB)', + }, + }, + additionalProperties: false, +}; + +export function registerDevfilesRoute(instance: FastifyInstance) { + instance.register(async server => { + // List all devfiles + server.get( + `${baseApiPath}/devfiles/namespace/:namespace`, + getSchema({ + tags, + summary: 'List all devfiles', + description: + 'Returns all devfile entries stored in the devfile-creator-storage ConfigMap ' + + 'in the given namespace. Each entry includes parsed metadata (name, description, ' + + 'project count) and the raw YAML content.', + params: namespacedSchema, + }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + const coreV1Api = getCoreV1Api(token); + + const configMap = await getOrCreateConfigMap(coreV1Api, namespace); + return { devfiles: parseEntries(configMap.data, configMap.metadata?.annotations) }; + }, + ); + + // Create new devfile + server.post( + `${baseApiPath}/devfiles/namespace/:namespace`, + getSchema({ + tags, + summary: 'Create a new devfile', + description: + 'Stores a new devfile YAML entry in the devfile-creator-storage ConfigMap. ' + + 'Assigns a random UUID as the entry key and records a last-modified annotation.', + params: namespacedSchema, + body: devfileContentBodySchema, + }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParams; + const body = request.body as { content: string }; + const token = getToken(request); + const coreV1Api = getCoreV1Api(token); + + const id = generateDevfileId(body.content); + const configMap = await getOrCreateConfigMap(coreV1Api, namespace); + const data = { ...configMap.data, [id]: body.content }; + const now = new Date().toISOString(); + + await coreV1Api.patchNamespacedConfigMap( + { + name: CONFIGMAP_NAME, + namespace, + body: { + metadata: { + annotations: { [`${MODIFIED_ANNOTATION_PREFIX}${id}`]: now }, + }, + data, + }, + }, + STRATEGIC_MERGE_PATCH_OPTIONS, + ); + + return { id }; + }, + ); + + // Update devfile + server.put( + `${baseApiPath}/devfiles/namespace/:namespace/:id`, + getSchema({ + tags, + summary: 'Update an existing devfile', + description: + 'Replaces the YAML content of an existing devfile entry identified by its UUID. ' + + 'Returns 404 if the entry does not exist.', + params: namespacedIdSchema, + body: devfileContentBodySchema, + }), + async function (request: FastifyRequest) { + const { namespace, id } = request.params as DevfileIdParams; + const body = request.body as { content: string }; + const token = getToken(request); + const coreV1Api = getCoreV1Api(token); + + const configMap = await getOrCreateConfigMap(coreV1Api, namespace); + if (!configMap.data?.[id]) { + throw new Error(`Devfile with id "${id}" not found`); + } + + const data: Record = { ...configMap.data, [id]: body.content }; + const annotations: Record = {}; + const now = new Date().toISOString(); + annotations[`${MODIFIED_ANNOTATION_PREFIX}${id}`] = now; + + await coreV1Api.patchNamespacedConfigMap( + { + name: CONFIGMAP_NAME, + namespace, + body: { metadata: { annotations }, data }, + }, + JSON_MERGE_PATCH_OPTIONS, + ); + + return { id }; + }, + ); + + // Delete devfile + server.delete( + `${baseApiPath}/devfiles/namespace/:namespace/:id`, + getSchema({ + tags, + summary: 'Delete a devfile', + description: + 'Removes a devfile entry from the devfile-creator-storage ConfigMap using ' + + 'a JSON merge patch (sets the key to null). Returns 204 on success.', + params: namespacedIdSchema, + }), + async function (request: FastifyRequest, reply: FastifyReply) { + const { namespace, id } = request.params as DevfileIdParams; + const token = getToken(request); + const coreV1Api = getCoreV1Api(token); + + const configMap = await getOrCreateConfigMap(coreV1Api, namespace); + if (configMap.data?.[id]) { + await coreV1Api.patchNamespacedConfigMap( + { + name: CONFIGMAP_NAME, + namespace, + body: { data: { [id]: null } }, + }, + JSON_MERGE_PATCH_OPTIONS, + ); + } + + reply.code(204).send(); + }, + ); + + // Raw devfile content + server.get( + `${baseApiPath}/devfiles/namespace/:namespace/:id/raw`, + getSchema({ + tags, + summary: 'Get raw devfile YAML', + description: + 'Returns the raw YAML content of a devfile entry as text/yaml. ' + + 'Used by the factory flow to create workspaces from stored devfiles.', + params: namespacedIdSchema, + }), + async function (request: FastifyRequest, reply: FastifyReply) { + const { namespace, id } = request.params as DevfileIdParams; + const token = getToken(request); + const coreV1Api = getCoreV1Api(token); + + const configMap = await getOrCreateConfigMap(coreV1Api, namespace); + const content = configMap.data?.[id]; + if (!content) { + return reply.status(404).send('Devfile not found'); + } + + reply.header('Content-Type', 'text/yaml; charset=utf-8'); + return reply.send(content); + }, + ); + }); +} diff --git a/packages/dashboard-backend/src/routes/api/devworkspaces.ts b/packages/dashboard-backend/src/routes/api/devworkspaces.ts index 3cbd6b4b67..97a3a2ade4 100644 --- a/packages/dashboard-backend/src/routes/api/devworkspaces.ts +++ b/packages/dashboard-backend/src/routes/api/devworkspaces.ts @@ -28,6 +28,12 @@ import { PostStartInjector } from '@/services/PostStartInjector'; const tags = ['Devworkspace']; +const RESOURCE_TYPE_LABEL = 'che.eclipse.org/type'; + +function isAgentWorkspace(labels: Record | undefined): boolean { + return labels?.[RESOURCE_TYPE_LABEL] === 'agent'; +} + export function registerDevworkspacesRoutes(instance: FastifyInstance) { instance.register(async server => { server.get( @@ -66,12 +72,13 @@ export function registerDevworkspacesRoutes(instance: FastifyInstance) { // Trigger server-side injection when workspace is created in started mode // (e.g. factory URL flow). Without this the watch would only be set up on // PATCH /spec/started=true (restart), missing first-time creation. + // Skip for agent workspaces — they don't need kubeconfig/podman injection. const workspaceName = devWorkspace.metadata?.name; - if (devworkspace.spec?.started === true && workspaceName) { - // Pass a fresh devworkspaceApi instance (DevWorkspaceClient.devworkspaceApi is a - // getter — each access returns a new DevWorkspaceApiService). PostStartInjector - // calls stopWatching() on this instance when done; passing the same instance that - // the route already used for .create() would cancel the route's own watch. + if ( + devworkspace.spec?.started === true && + workspaceName && + !isAgentWorkspace(devworkspace.metadata?.labels) + ) { PostStartInjector.watchAndInject( namespace, workspaceName, @@ -113,8 +120,7 @@ export function registerDevworkspacesRoutes(instance: FastifyInstance) { ); const isStarting = patch.some(p => p.path === '/spec/started' && p.value === true); - if (isStarting) { - // See comment above — pass a fresh devworkspaceApi instance. + if (isStarting && !isAgentWorkspace(devWorkspace.metadata?.labels)) { PostStartInjector.watchAndInject( namespace, workspaceName, diff --git a/packages/dashboard-backend/src/routes/api/helpers/devfile/index.ts b/packages/dashboard-backend/src/routes/api/helpers/devfile/index.ts new file mode 100644 index 0000000000..761e963bc4 --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/helpers/devfile/index.ts @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { randomBytes } from 'node:crypto'; + +import { helpers } from '@eclipse-che/common'; +import * as k8s from '@kubernetes/client-node'; +import { load } from 'js-yaml'; + +import { getKubeConfig } from '@/routes/api/helpers/getDevWorkspaceClient'; + +export const CONFIGMAP_NAME = 'devfile-creator-storage'; +const CONFIGMAP_LABELS = { + 'app.kubernetes.io/component': 'devfile-creator', + 'app.kubernetes.io/part-of': 'che.eclipse.org', +}; + +interface ParsedDevfile { + metadata?: { name?: string; generateName?: string; description?: string }; + projects?: Array<{ name?: string }>; +} + +export interface DevfileEntry { + id: string; + name: string; + description: string; + content: string; + projectCount: number; + lastModified: string; +} + +export interface DevfileIdParams { + namespace: string; + id: string; +} + +function parseDevfileYaml(content: string): ParsedDevfile { + try { + const parsed = load(content); + if (typeof parsed === 'object' && parsed !== null) { + return parsed as ParsedDevfile; + } + } catch { + // invalid YAML + } + return {}; +} + +export function extractDevfileName(content: string): string { + const parsed = parseDevfileYaml(content); + const name = parsed.metadata?.name; + if (typeof name === 'string' && name.length > 0) { + return name; + } + const generateName = parsed.metadata?.generateName; + if (typeof generateName === 'string' && generateName.length > 0) { + return generateName; + } + return 'untitled'; +} + +export function extractDevfileDescription(content: string): string { + const parsed = parseDevfileYaml(content); + const description = parsed.metadata?.description; + return typeof description === 'string' ? description : ''; +} + +export function generateDevfileId(content: string): string { + const name = extractDevfileName(content); + const sanitized = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 20); + const suffix = randomBytes(4).toString('hex').slice(0, 7); + return `${sanitized || 'devfile'}-${suffix}`; +} + +export function countProjects(content: string): number { + const parsed = parseDevfileYaml(content); + if (!Array.isArray(parsed.projects)) return 0; + return parsed.projects.filter(p => typeof p?.name === 'string').length; +} + +export const MODIFIED_ANNOTATION_PREFIX = 'che.eclipse.org/modified-'; + +export function parseEntries( + data: Record | undefined, + annotations?: Record, +): DevfileEntry[] { + if (!data) return []; + return Object.entries(data).map(([id, content]) => { + const parsed = parseDevfileYaml(content); + const name = parsed.metadata?.name; + const generateName = parsed.metadata?.generateName; + const description = parsed.metadata?.description; + const projects = Array.isArray(parsed.projects) ? parsed.projects : []; + const storedTimestamp = annotations?.[`${MODIFIED_ANNOTATION_PREFIX}${id}`]; + const displayName = + typeof name === 'string' && name.length > 0 + ? name + : typeof generateName === 'string' && generateName.length > 0 + ? generateName + : 'untitled'; + return { + id, + name: displayName, + description: typeof description === 'string' ? description : '', + content, + projectCount: projects.filter(p => typeof p?.name === 'string').length, + lastModified: storedTimestamp || new Date().toISOString(), + }; + }); +} + +export async function getOrCreateConfigMap( + coreV1Api: k8s.CoreV1Api, + namespace: string, +): Promise { + try { + const response = await coreV1Api.readNamespacedConfigMap({ + name: CONFIGMAP_NAME, + namespace, + }); + return response; + } catch (error: unknown) { + if (helpers.errors.isKubeClientError(error) && error.code === 404) { + const configMap: k8s.V1ConfigMap = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + name: CONFIGMAP_NAME, + namespace, + labels: CONFIGMAP_LABELS, + }, + data: {}, + }; + return coreV1Api.createNamespacedConfigMap({ + namespace, + body: configMap, + }); + } + throw error; + } +} + +export function getCoreV1Api(token: string): k8s.CoreV1Api { + const kubeConfig = getKubeConfig(token); + return kubeConfig.makeApiClient(k8s.CoreV1Api); +} From feb9db62fdb93c9aa9fb50f5dcdb45843b3fde94 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:11:11 +0300 Subject: [PATCH 04/16] feat(backend): add agent pod management and terminal proxy Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .../routes/api/__tests__/websocket.spec.ts | 28 + .../src/routes/api/agents.ts | 499 ++++++++++++++++ .../src/routes/api/helpers/agentPod.ts | 542 ++++++++++++++++++ .../src/routes/api/helpers/terminal/index.ts | 170 ++++++ .../src/routes/api/websocket.ts | 11 +- 5 files changed, 1246 insertions(+), 4 deletions(-) create mode 100644 packages/dashboard-backend/src/routes/api/agents.ts create mode 100644 packages/dashboard-backend/src/routes/api/helpers/agentPod.ts create mode 100644 packages/dashboard-backend/src/routes/api/helpers/terminal/index.ts diff --git a/packages/dashboard-backend/src/routes/api/__tests__/websocket.spec.ts b/packages/dashboard-backend/src/routes/api/__tests__/websocket.spec.ts index 19b2141e43..3aeb754456 100644 --- a/packages/dashboard-backend/src/routes/api/__tests__/websocket.spec.ts +++ b/packages/dashboard-backend/src/routes/api/__tests__/websocket.spec.ts @@ -56,6 +56,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockResolvedValue(undefined), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); @@ -106,6 +110,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockRejectedValue(watchError), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); @@ -150,6 +158,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockResolvedValue(undefined), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); @@ -186,6 +198,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockResolvedValue(undefined), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); @@ -220,6 +236,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockResolvedValue(undefined), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); @@ -253,6 +273,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockResolvedValue(undefined), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); @@ -282,6 +306,10 @@ describe('WebSocket handler', () => { watchInNamespace: jest.fn().mockResolvedValue(undefined), stopWatching: jest.fn(), }, + configMapWatchApi: { + watchInNamespace: jest.fn().mockResolvedValue(undefined), + stopWatching: jest.fn(), + }, }); webSocketHandler(mockWs as never, mockRequest as never); diff --git a/packages/dashboard-backend/src/routes/api/agents.ts b/packages/dashboard-backend/src/routes/api/agents.ts new file mode 100644 index 0000000000..c3d16a797e --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/agents.ts @@ -0,0 +1,499 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import http from 'node:http'; +import https from 'node:https'; + +import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; +import WebSocket from 'ws'; + +import { baseApiPath } from '@/constants/config'; +import { namespacedSchema } from '@/constants/schemas'; +import { isLocalRun } from '@/localRun'; +import { restParams } from '@/models'; +import { + AgentServiceAccess, + cleanupExpiredAgentPods, + createAgentPod, + deleteAgentPod, + getAgentPodStatus, + getAgentServiceAccess, + heartbeatAgentPod, +} from '@/routes/api/helpers/agentPod'; +import { getToken } from '@/routes/api/helpers/getToken'; +import { proxyToTerminal } from '@/routes/api/helpers/terminal'; +import { getSchema } from '@/services/helpers'; +import { logger } from '@/utils/logger'; + +const tags = ['Agent Pods']; + +const MIN_TERMINAL_PORT = 1024; +const MAX_TERMINAL_PORT = 65535; + +function validateTerminalPort(port: number): number { + if (!Number.isInteger(port) || port < MIN_TERMINAL_PORT || port > MAX_TERMINAL_PORT) { + throw new Error( + `terminalPort must be an integer between ${MIN_TERMINAL_PORT} and ${MAX_TERMINAL_PORT}`, + ); + } + return port; +} + +const terminalAccessCache = new Map(); +const TERMINAL_URL_CACHE_TTL = 30 * 1000; + +function terminalCacheKey(namespace: string, agentId: string): string { + return `${namespace}/${agentId}`; +} + +function getCachedTerminalAccess( + namespace: string, + agentId: string, +): AgentServiceAccess | undefined { + const key = terminalCacheKey(namespace, agentId); + const entry = terminalAccessCache.get(key); + if (!entry) return undefined; + if (Date.now() - entry.timestamp > TERMINAL_URL_CACHE_TTL) { + terminalAccessCache.delete(key); + return undefined; + } + return entry.access; +} + +function setCachedTerminalAccess( + namespace: string, + agentId: string, + access: AgentServiceAccess, +): void { + const key = terminalCacheKey(namespace, agentId); + terminalAccessCache.set(key, { access, timestamp: Date.now() }); +} + +function clearCachedTerminalAccess(namespace: string, agentId: string): void { + terminalAccessCache.delete(terminalCacheKey(namespace, agentId)); +} + +const namespacedAgentIdSchema = { + type: 'object', + required: ['namespace', 'agentId'], + properties: { + namespace: { type: 'string', description: 'Kubernetes namespace of the user' }, + agentId: { type: 'string', description: 'Unique identifier of the agent pod instance' }, + }, +}; + +const agentCreateBodySchema = { + type: 'object', + required: ['agentId', 'image', 'tag', 'memoryLimit', 'cpuLimit', 'terminalPort'], + properties: { + agentId: { + type: 'string', + minLength: 1, + maxLength: 128, + description: 'Unique identifier for the agent pod (e.g. "anthropic/claude-code-474")', + }, + image: { + type: 'string', + minLength: 1, + maxLength: 512, + description: 'Container image for the agent (e.g. "quay.io/oorel/dashboard-agent")', + }, + tag: { + type: 'string', + minLength: 1, + maxLength: 128, + description: 'Image tag (e.g. "v20")', + }, + memoryLimit: { + type: 'string', + pattern: '^[0-9]+(Mi|Gi)$', + maxLength: 10, + default: '4Gi', + description: 'Memory limit for the agent container (e.g. "4Gi", "512Mi")', + }, + cpuLimit: { + type: 'string', + pattern: '^[0-9]+(m)?$', + maxLength: 10, + default: '1', + description: 'CPU limit for the agent container (e.g. "1", "1000m")', + }, + terminalPort: { + type: 'integer', + minimum: MIN_TERMINAL_PORT, + maximum: MAX_TERMINAL_PORT, + description: 'Port on which the ttyd terminal server listens inside the agent container', + }, + env: { + type: 'array', + description: 'Additional environment variables to inject into the agent container', + items: { + type: 'object', + required: ['name', 'value'], + properties: { + name: { type: 'string', minLength: 1 }, + value: { type: 'string' }, + }, + additionalProperties: false, + }, + }, + }, + additionalProperties: false, +}; + +export function registerAgentsRoute(instance: FastifyInstance) { + instance.register(async server => { + // Create agent pod + service + server.post( + `${baseApiPath}/namespace/:namespace/agent`, + getSchema({ + tags, + summary: 'Create a new AI agent pod', + description: + 'Creates a new Kubernetes pod running an AI agent container with a ttyd terminal server. ' + + 'Also creates a ClusterIP service for terminal access. ' + + 'Maximum 3 active agent pods per user; returns 409 if the limit is exceeded.', + params: namespacedSchema, + body: agentCreateBodySchema, + }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + const body = request.body as { + agentId: string; + image: string; + tag: string; + memoryLimit: string; + cpuLimit: string; + terminalPort: number; + env: Array<{ name: string; value: string }>; + }; + + const status = await createAgentPod(token, namespace, body.agentId, { + image: body.image, + tag: body.tag, + memoryLimit: body.memoryLimit, + cpuLimit: body.cpuLimit, + terminalPort: validateTerminalPort(body.terminalPort), + env: body.env || [], + }); + + if (status.serviceUrl) { + setCachedTerminalAccess( + namespace, + body.agentId, + getAgentServiceAccess(namespace, body.agentId, validateTerminalPort(body.terminalPort)), + ); + } + + return status; + }, + ); + + // Get agent pod status + server.get( + `${baseApiPath}/namespace/:namespace/agent/:agentId`, + getSchema({ + tags, + summary: 'Get agent pod status', + description: + 'Returns the current status of an agent pod including its phase, readiness, ' + + 'and in-cluster service URL for terminal access.', + params: namespacedAgentIdSchema, + }), + async function (request: FastifyRequest, reply: FastifyReply) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + const { agentId } = request.params as { agentId: string }; + const query = request.query as Record; + const terminalPort = query.terminalPort + ? validateTerminalPort(parseInt(query.terminalPort, 10)) + : 8080; + + const status = await getAgentPodStatus(token, namespace, agentId, terminalPort); + if (!status) { + reply.code(404).send({ message: 'Agent pod not found' }); + return; + } + + if (status.serviceUrl) { + setCachedTerminalAccess( + namespace, + agentId, + getAgentServiceAccess(namespace, agentId, terminalPort), + ); + } + + return status; + }, + ); + + // Delete agent pod + server.delete( + `${baseApiPath}/namespace/:namespace/agent/:agentId`, + getSchema({ + tags, + summary: 'Stop and delete an agent pod', + description: + 'Deletes the agent pod, its ClusterIP service (via ownerReference), and ' + + 'the associated token secret. The pod is given a 5-second graceful termination period.', + params: namespacedAgentIdSchema, + }), + async function (request: FastifyRequest, reply: FastifyReply) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + const { agentId } = request.params as { agentId: string }; + + await deleteAgentPod(token, namespace, agentId); + clearCachedTerminalAccess(namespace, agentId); + reply.code(204).send(); + }, + ); + + // Agent heartbeat + server.post( + `${baseApiPath}/namespace/:namespace/agent/:agentId/heartbeat`, + getSchema({ + tags, + summary: 'Send agent pod heartbeat', + description: + 'Updates the last-heartbeat annotation on the agent pod to prevent TTL-based ' + + 'auto-cleanup. Also triggers a background cleanup of any expired agent pods ' + + 'in the namespace (pods with no heartbeat for more than 10 minutes).', + params: namespacedAgentIdSchema, + }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + const { agentId } = request.params as { agentId: string }; + + await heartbeatAgentPod(token, namespace, agentId); + cleanupExpiredAgentPods(token, namespace).catch(e => + logger.warn(e, 'Heartbeat-triggered cleanup failed'), + ); + return { ok: true }; + }, + ); + + // Check if agent terminal is reachable + server.get( + `${baseApiPath}/namespace/:namespace/agent-terminal-url`, + getSchema({ + tags, + summary: 'Check agent terminal reachability', + description: + 'Performs a health check against the agent ttyd terminal server via its in-cluster ' + + 'service URL. Returns the service URL if the terminal responds with a non-5xx status ' + + 'within 2 seconds.', + params: namespacedSchema, + }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + if (!token) { + throw new Error('Authentication required'); + } + const query = request.query as Record; + const agentId = query.agentId || 'anthropic/claude-code'; + const terminalPort = query.terminalPort + ? validateTerminalPort(parseInt(query.terminalPort, 10)) + : 8080; + + const access = getAgentServiceAccess(namespace, agentId, terminalPort); + setCachedTerminalAccess(namespace, agentId, access); + + await new Promise((resolve, reject) => { + const base = access.baseUrl.endsWith('/') ? access.baseUrl : access.baseUrl + '/'; + const url = new URL(base); + const isHttps = url.protocol === 'https:'; + const transport = isHttps ? https : http; + const options: http.RequestOptions = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + timeout: 2000, + headers: {}, + }; + if (isHttps && access.httpsOptions) { + (options as https.RequestOptions).rejectUnauthorized = + access.httpsOptions.rejectUnauthorized; + if (access.httpsOptions.ca) { + (options as https.RequestOptions).ca = access.httpsOptions.ca; + } + } + if (isHttps && token) { + options.headers = { Authorization: `Bearer ${token}` }; + } + const req = transport.get(options, res => { + res.resume(); + if (res.statusCode && res.statusCode < 500) { + resolve(); + } else { + reject(new Error('Terminal server not ready')); + } + }); + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error('Terminal server timeout')); + }); + }); + + return { url: access.baseUrl }; + }, + ); + + // WebSocket proxy for ttyd terminal + server.get( + `${baseApiPath}/namespace/:namespace/agent/t/ws`, + { + websocket: true, + schema: { + tags, + summary: 'WebSocket proxy for agent terminal', + description: + 'Relays binary WebSocket frames between the browser and the in-cluster ttyd server. ' + + 'Buffers client messages until the upstream connection is open.', + }, + }, + (socket: WebSocket, request: FastifyRequest) => { + const token = getToken(request); + if (!token) { + socket.close(1008, 'Authentication required'); + return; + } + const { namespace } = request.params as restParams.INamespacedParams; + const requestUrl = new URL(request.url, 'http://localhost'); + const agentId = requestUrl.searchParams.get('agentId') || 'anthropic/claude-code'; + const cachedAccess = getCachedTerminalAccess(namespace, agentId); + if (!cachedAccess) { + socket.close(1011, 'Terminal service URL not found'); + return; + } + + const CLUSTER_SVC_PATTERN = /^https?:\/\/[a-z0-9-]+\.[a-z0-9-]+\.svc:\d+$/; + if (!isLocalRun() && !CLUSTER_SVC_PATTERN.test(cachedAccess.baseUrl)) { + logger.warn( + 'Rejected terminal URL not matching cluster service pattern: %s', + cachedAccess.baseUrl, + ); + socket.close(1011, 'Invalid terminal service URL'); + return; + } + + const queryString = new URL(request.url, 'http://localhost').search; + const wsUrl = `${cachedAccess.baseUrl.replace(/^http/, 'ws')}/ws${queryString}`; + const wsOptions: WebSocket.ClientOptions = {}; + if (cachedAccess.httpsOptions) { + wsOptions.rejectUnauthorized = cachedAccess.httpsOptions.rejectUnauthorized; + if (cachedAccess.httpsOptions.ca) { + wsOptions.ca = cachedAccess.httpsOptions.ca; + } + wsOptions.headers = { Authorization: `Bearer ${token}` }; + } + const upstream = new WebSocket(wsUrl, ['tty'], wsOptions); + + const pendingMessages: Array<{ data: WebSocket.RawData; isBinary: boolean }> = []; + + upstream.on('open', () => { + logger.info('Terminal WebSocket upstream connected: %s', wsUrl); + for (const msg of pendingMessages) { + upstream.send(msg.data, { binary: msg.isBinary }); + } + pendingMessages.length = 0; + }); + + socket.on('message', (data: WebSocket.RawData, isBinary: boolean) => { + if (upstream.readyState === WebSocket.OPEN) { + upstream.send(data, { binary: isBinary }); + } else if (upstream.readyState === WebSocket.CONNECTING) { + pendingMessages.push({ data, isBinary }); + } + }); + + upstream.on('message', (data: WebSocket.RawData, isBinary: boolean) => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(data, { binary: isBinary }); + } + }); + + socket.on('close', () => { + if (upstream.readyState <= WebSocket.OPEN) upstream.close(); + }); + upstream.on('close', () => { + if (socket.readyState <= WebSocket.OPEN) socket.close(); + }); + + socket.on('error', err => { + logger.error(err, 'Terminal WebSocket client error'); + upstream.close(); + }); + upstream.on('error', err => { + logger.error(err, 'Terminal WebSocket upstream error'); + socket.close(); + }); + }, + ); + + // HTTP reverse proxy for ttyd terminal assets + const terminalProxyHandler = async function (request: FastifyRequest, reply: FastifyReply) { + const token = getToken(request); + if (!token) { + reply.code(401).send({ message: 'Authentication required' }); + return; + } + const { namespace } = request.params as restParams.INamespacedParams; + const requestUrl = new URL(request.url, 'http://localhost'); + const agentId = requestUrl.searchParams.get('agentId') || 'anthropic/claude-code'; + const terminalPort = requestUrl.searchParams.get('terminalPort') + ? validateTerminalPort(parseInt(requestUrl.searchParams.get('terminalPort') as string, 10)) + : 8080; + + let access: AgentServiceAccess; + const cachedAccess = getCachedTerminalAccess(namespace, agentId); + if (cachedAccess) { + access = cachedAccess; + } else { + access = getAgentServiceAccess(namespace, agentId, terminalPort); + setCachedTerminalAccess(namespace, agentId, access); + } + + const prefix = `${baseApiPath}/namespace/${namespace}/agent/t`; + const subPath = request.url.slice(prefix.length) || '/'; + + logger.info( + 'Terminal proxy: %s %s -> %s%s', + request.method, + request.url, + access.baseUrl, + subPath, + ); + return proxyToTerminal(access, subPath, prefix, token, request, reply); + }; + + server.get( + `${baseApiPath}/namespace/:namespace/agent/t/*`, + getSchema({ + tags, + summary: 'Proxy agent terminal assets', + description: + 'HTTP reverse proxy for the ttyd terminal server. Serves HTML, JS, CSS, and other ' + + 'static assets through the dashboard origin so the terminal iframe can load them. ' + + 'Injects a bridge script for theme synchronization and init command execution.', + params: namespacedSchema, + }), + terminalProxyHandler, + ); + }); +} diff --git a/packages/dashboard-backend/src/routes/api/helpers/agentPod.ts b/packages/dashboard-backend/src/routes/api/helpers/agentPod.ts new file mode 100644 index 0000000000..e1cb487bec --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/helpers/agentPod.ts @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { helpers } from '@eclipse-che/common'; +import * as k8s from '@kubernetes/client-node'; + +import { STRATEGIC_MERGE_PATCH_OPTIONS } from '@/devworkspaceClient/services/helpers/patchOptions'; +import { isLocalRun } from '@/localRun'; +import { getKubeConfig } from '@/routes/api/helpers/getDevWorkspaceClient'; +import { KubeConfigProvider } from '@/services/kubeclient/kubeConfigProvider'; +import { logger } from '@/utils/logger'; + +const AGENT_LABEL_COMPONENT = 'app.kubernetes.io/component'; +const AGENT_LABEL_PART_OF = 'app.kubernetes.io/part-of'; +const AGENT_LABEL_AGENT_ID = 'che.eclipse.org/agent-id'; +const HEARTBEAT_ANNOTATION = 'che.eclipse.org/last-heartbeat'; +const AGENT_TTL_MINUTES = 20; +export const MAX_AGENT_PODS_PER_USER = 3; +const TOKEN_SECRET_VOLUME = 'che-user-token'; +const TOKEN_MOUNT_PATH = '/var/run/secrets/che/token'; + +export interface AgentPodStatus { + agentId: string; + name: string; + phase: string; + ready: boolean; + serviceUrl: string | undefined; +} + +interface AgentConfig { + image: string; + tag: string; + memoryLimit: string; + cpuLimit: string; + terminalPort: number; + env: Array<{ name: string; value: string }>; +} + +function normalizeAgentId(agentId: string): string { + return agentId.replace(/\//g, '-').replace(/[^a-z0-9-]/g, ''); +} + +function agentPodName(agentId: string): string { + return `agent-${normalizeAgentId(agentId)}`; +} + +function agentServiceName(agentId: string): string { + return `agent-${normalizeAgentId(agentId)}-svc`; +} + +function agentTokenSecretName(agentId: string): string { + return `agent-${normalizeAgentId(agentId)}-token`; +} + +function agentLabels(agentId: string): Record { + return { + [AGENT_LABEL_COMPONENT]: 'ai-agent', + [AGENT_LABEL_PART_OF]: 'che.eclipse.org', + [AGENT_LABEL_AGENT_ID]: normalizeAgentId(agentId), + }; +} + +const DW_MOUNT_LABEL = 'controller.devfile.io/mount-to-devworkspace'; +const DW_MOUNT_AS = 'controller.devfile.io/mount-as'; +const DW_MOUNT_PATH = 'controller.devfile.io/mount-path'; + +interface DevWorkspaceMounts { + volumes: k8s.V1Volume[]; + volumeMounts: k8s.V1VolumeMount[]; + envFrom: k8s.V1EnvFromSource[]; +} + +function getCoreV1Api(token: string): k8s.CoreV1Api { + const kubeConfig = getKubeConfig(token); + return kubeConfig.makeApiClient(k8s.CoreV1Api); +} + +async function discoverDevWorkspaceMounts( + coreV1Api: k8s.CoreV1Api, + namespace: string, +): Promise { + const volumes: k8s.V1Volume[] = []; + const volumeMounts: k8s.V1VolumeMount[] = []; + const envFrom: k8s.V1EnvFromSource[] = []; + + try { + const secrets = await coreV1Api.listNamespacedSecret({ + namespace, + labelSelector: `${DW_MOUNT_LABEL}=true`, + }); + + for (const secret of secrets.items) { + const name = secret.metadata?.name; + if (!name) continue; + + const mountAs = secret.metadata?.annotations?.[DW_MOUNT_AS]; + const mountPath = secret.metadata?.annotations?.[DW_MOUNT_PATH]; + + if (mountAs === 'file' && mountPath) { + const volumeName = `secret-${name}`; + volumes.push({ name: volumeName, secret: { secretName: name } }); + volumeMounts.push({ name: volumeName, mountPath, readOnly: true }); + } else if (mountAs === 'env') { + envFrom.push({ secretRef: { name } }); + } + } + } catch (e) { + logger.warn(e, 'Failed to discover DevWorkspace-mountable secrets'); + } + + try { + const configMaps = await coreV1Api.listNamespacedConfigMap({ + namespace, + labelSelector: `${DW_MOUNT_LABEL}=true`, + }); + + for (const cm of configMaps.items) { + const name = cm.metadata?.name; + if (!name) continue; + + const mountAs = cm.metadata?.annotations?.[DW_MOUNT_AS]; + const mountPath = cm.metadata?.annotations?.[DW_MOUNT_PATH]; + + if (mountAs === 'env') { + envFrom.push({ configMapRef: { name } }); + } else if (mountAs === 'file' && mountPath) { + const volumeName = `cm-${name}`; + volumes.push({ name: volumeName, configMap: { name } }); + volumeMounts.push({ name: volumeName, mountPath, readOnly: true }); + } + } + } catch (e) { + logger.warn(e, 'Failed to discover DevWorkspace-mountable configmaps'); + } + + return { volumes, volumeMounts, envFrom }; +} + +export async function createAgentPod( + token: string, + namespace: string, + agentId: string, + config: AgentConfig, +): Promise { + const coreV1Api = getCoreV1Api(token); + const podName = agentPodName(agentId); + const svcName = agentServiceName(agentId); + const secretName = agentTokenSecretName(agentId); + const labels = agentLabels(agentId); + + // Clean up expired pods before checking limits + await cleanupExpiredAgentPods(token, namespace); + + const existing = await findAgentPod(coreV1Api, namespace, agentId); + if (existing) { + const phase = existing.status?.phase || 'Unknown'; + if (phase === 'Failed' || phase === 'Succeeded') { + await coreV1Api.deleteNamespacedPod({ name: existing.metadata!.name!, namespace }); + } else { + return podToStatus(existing, agentId, namespace, config.terminalPort); + } + } + + // Check active pod count against limit + const activePods = await coreV1Api.listNamespacedPod({ + namespace, + labelSelector: `${AGENT_LABEL_COMPONENT}=ai-agent`, + }); + const runningCount = activePods.items.filter( + p => + !p.metadata?.deletionTimestamp && + p.status?.phase !== 'Failed' && + p.status?.phase !== 'Succeeded', + ).length; + if (runningCount >= MAX_AGENT_PODS_PER_USER) { + throw Object.assign( + new Error( + `Maximum number of agent pods (${MAX_AGENT_PODS_PER_USER}) reached. Stop an existing agent before starting a new one.`, + ), + { statusCode: 409 }, + ); + } + + const dwMounts = await discoverDevWorkspaceMounts(coreV1Api, namespace); + const now = new Date().toISOString(); + + // Resolve the Kubernetes API server URL for the agent env var + const kubeConfig = getKubeConfig(token); + const kubeApiUrl = kubeConfig.getCurrentCluster()?.server || ''; + + // Create the token Secret before the Pod so the volume mount can reference it + await createOrReplaceTokenSecret(coreV1Api, namespace, secretName, token, labels); + + const allVolumes: k8s.V1Volume[] = [ + ...dwMounts.volumes, + { name: TOKEN_SECRET_VOLUME, secret: { secretName } }, + { name: 'tmp-dir', emptyDir: {} }, + ]; + const allVolumeMounts: k8s.V1VolumeMount[] = [ + ...dwMounts.volumeMounts, + { name: TOKEN_SECRET_VOLUME, mountPath: TOKEN_MOUNT_PATH, readOnly: true }, + { name: 'tmp-dir', mountPath: '/tmp' }, + ]; + + const pod = await coreV1Api.createNamespacedPod({ + namespace, + body: { + metadata: { + name: podName, + namespace, + labels, + annotations: { + 'che.eclipse.org/ai-agent-id': agentId, + [HEARTBEAT_ANNOTATION]: now, + }, + }, + spec: { + automountServiceAccountToken: false, + volumes: allVolumes, + containers: [ + { + name: 'agent', + image: `${config.image}:${config.tag}`, + imagePullPolicy: 'IfNotPresent', + ports: [ + { + containerPort: config.terminalPort, + name: 'terminal', + protocol: 'TCP', + }, + ], + resources: { + limits: { + memory: config.memoryLimit, + cpu: config.cpuLimit, + }, + requests: { + memory: '128Mi', + cpu: '100m', + }, + }, + env: [ + ...config.env, + { name: 'AGENT_NAMESPACE', value: namespace }, + { name: 'KUBERNETES_API_URL', value: kubeApiUrl }, + { name: 'CHE_USER_TOKEN_FILE', value: `${TOKEN_MOUNT_PATH}/token` }, + ], + envFrom: dwMounts.envFrom.length > 0 ? dwMounts.envFrom : undefined, + volumeMounts: allVolumeMounts, + securityContext: { + allowPrivilegeEscalation: false, + runAsUser: 1001, + runAsNonRoot: true, + readOnlyRootFilesystem: true, + capabilities: { drop: ['ALL'] }, + }, + }, + ], + restartPolicy: 'Never', + terminationGracePeriodSeconds: 5, + }, + }, + }); + + const podUid = pod.metadata!.uid!; + + // Set ownerReference on the Secret so it is garbage-collected with the Pod + await setSecretOwnerReference(coreV1Api, namespace, secretName, podName, podUid); + + await coreV1Api.createNamespacedService({ + namespace, + body: { + metadata: { + name: svcName, + namespace, + labels, + ownerReferences: [ + { + apiVersion: 'v1', + kind: 'Pod', + name: podName, + uid: podUid, + }, + ], + }, + spec: { + type: 'ClusterIP', + selector: { [AGENT_LABEL_AGENT_ID]: normalizeAgentId(agentId) }, + ports: [ + { + name: 'terminal', + port: config.terminalPort, + targetPort: config.terminalPort, + protocol: 'TCP', + }, + ], + }, + }, + }); + + return podToStatus(pod, agentId, namespace, config.terminalPort); +} + +export async function getAgentPodStatus( + token: string, + namespace: string, + agentId: string, + terminalPort = 8080, +): Promise { + const coreV1Api = getCoreV1Api(token); + const pod = await findAgentPod(coreV1Api, namespace, agentId); + if (!pod) return undefined; + return podToStatus(pod, agentId, namespace, terminalPort); +} + +export async function deleteAgentPod( + token: string, + namespace: string, + agentId: string, +): Promise { + const coreV1Api = getCoreV1Api(token); + const pod = await findAgentPod(coreV1Api, namespace, agentId); + if (pod?.metadata?.name) { + await coreV1Api.deleteNamespacedPod({ + name: pod.metadata.name, + namespace, + body: { gracePeriodSeconds: 5 }, + }); + } + + // Best-effort cleanup of the token Secret (may already be gone via ownerReference) + const secretName = agentTokenSecretName(agentId); + try { + await coreV1Api.deleteNamespacedSecret({ name: secretName, namespace }); + } catch (e: unknown) { + if (helpers.errors.isKubeClientError(e) && e.code === 404) { + // Already deleted — nothing to do + } else { + logger.warn(e, 'Failed to delete agent token secret: %s', secretName); + } + } +} + +export async function heartbeatAgentPod( + token: string, + namespace: string, + agentId: string, +): Promise { + const coreV1Api = getCoreV1Api(token); + const pod = await findAgentPod(coreV1Api, namespace, agentId); + if (!pod?.metadata?.name) return; + + const now = new Date().toISOString(); + await coreV1Api.patchNamespacedPod( + { + name: pod.metadata.name, + namespace, + body: { metadata: { annotations: { [HEARTBEAT_ANNOTATION]: now } } }, + }, + STRATEGIC_MERGE_PATCH_OPTIONS, + ); +} + +export async function cleanupExpiredAgentPods(token: string, namespace: string): Promise { + const coreV1Api = getCoreV1Api(token); + const pods = await coreV1Api.listNamespacedPod({ + namespace, + labelSelector: `${AGENT_LABEL_COMPONENT}=ai-agent`, + }); + + const now = Date.now(); + const ttlMs = AGENT_TTL_MINUTES * 60 * 1000; + const deleted: string[] = []; + + for (const pod of pods.items) { + const heartbeat = pod.metadata?.annotations?.[HEARTBEAT_ANNOTATION]; + if (!heartbeat) continue; + + const lastBeat = new Date(heartbeat).getTime(); + if (now - lastBeat > ttlMs) { + const name = pod.metadata!.name!; + logger.info('Cleaning up expired agent pod: %s (last heartbeat: %s)', name, heartbeat); + try { + await coreV1Api.deleteNamespacedPod({ + name, + namespace, + body: { gracePeriodSeconds: 5 }, + }); + deleted.push(name); + } catch (e) { + logger.error(e, 'Failed to delete expired agent pod: %s', name); + } + } + } + return deleted; +} + +export function getAgentServiceUrl( + namespace: string, + agentId: string, + terminalPort = 8080, +): string { + const svcName = agentServiceName(agentId); + return `http://${svcName}.${namespace}.svc:${terminalPort}`; +} + +export interface AgentServiceAccess { + baseUrl: string; + httpsOptions?: { + rejectUnauthorized: boolean; + ca?: string; + }; +} + +export function getAgentServiceAccess( + namespace: string, + agentId: string, + terminalPort: number, +): AgentServiceAccess { + if (isLocalRun()) { + const kc = new KubeConfigProvider().getSAKubeConfig(); + const cluster = kc.getCurrentCluster(); + if (!cluster) { + throw new Error('No cluster found in kubeconfig'); + } + const svcName = agentServiceName(agentId); + const baseUrl = `${cluster.server}/api/v1/namespaces/${namespace}/services/${svcName}:${terminalPort}/proxy`; + return { + baseUrl, + httpsOptions: { + rejectUnauthorized: !cluster.skipTLSVerify, + ca: cluster.caData ? Buffer.from(cluster.caData, 'base64').toString() : undefined, + }, + }; + } + return { + baseUrl: getAgentServiceUrl(namespace, agentId, terminalPort), + }; +} + +async function findAgentPod( + coreV1Api: k8s.CoreV1Api, + namespace: string, + agentId: string, +): Promise { + const normalized = normalizeAgentId(agentId); + const pods = await coreV1Api.listNamespacedPod({ + namespace, + labelSelector: `${AGENT_LABEL_AGENT_ID}=${normalized}`, + }); + return pods.items.find(pod => !pod.metadata?.deletionTimestamp); +} + +function podToStatus( + pod: k8s.V1Pod, + agentId: string, + namespace: string, + terminalPort: number, +): AgentPodStatus { + const phase = pod.status?.phase || 'Unknown'; + const containerReady = pod.status?.containerStatuses?.some(c => c.ready === true) || false; + const isRunning = phase === 'Running' && containerReady; + + return { + agentId, + name: pod.metadata?.name || '', + phase, + ready: isRunning, + serviceUrl: isRunning ? getAgentServiceUrl(namespace, agentId, terminalPort) : undefined, + }; +} + +async function createOrReplaceTokenSecret( + coreV1Api: k8s.CoreV1Api, + namespace: string, + secretName: string, + userToken: string, + labels: Record, +): Promise { + const secretBody: k8s.V1Secret = { + metadata: { + name: secretName, + namespace, + labels, + }, + stringData: { token: userToken }, + }; + + try { + await coreV1Api.createNamespacedSecret({ namespace, body: secretBody }); + } catch (e: unknown) { + if (helpers.errors.isKubeClientError(e) && e.code === 409) { + // Secret already exists — replace its data + await coreV1Api.replaceNamespacedSecret({ + name: secretName, + namespace, + body: secretBody, + }); + } else { + throw e; + } + } +} + +async function setSecretOwnerReference( + coreV1Api: k8s.CoreV1Api, + namespace: string, + secretName: string, + podName: string, + podUid: string, +): Promise { + await coreV1Api.patchNamespacedSecret( + { + name: secretName, + namespace, + body: { + metadata: { + ownerReferences: [ + { + apiVersion: 'v1', + kind: 'Pod', + name: podName, + uid: podUid, + }, + ], + }, + }, + }, + STRATEGIC_MERGE_PATCH_OPTIONS, + ); +} diff --git a/packages/dashboard-backend/src/routes/api/helpers/terminal/index.ts b/packages/dashboard-backend/src/routes/api/helpers/terminal/index.ts new file mode 100644 index 0000000000..a5c6e051fe --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/helpers/terminal/index.ts @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import http from 'node:http'; +import https from 'node:https'; + +import { FastifyReply, FastifyRequest } from 'fastify'; + +import { AGENT_TERMINAL_THEMES } from '@/constants/terminal-themes'; +import { AgentServiceAccess } from '@/routes/api/helpers/agentPod'; + +function buildBridgeScript(darkThemeJson: string, lightThemeJson: string): string { + return ``; +} + +export function proxyToTerminal( + access: AgentServiceAccess, + path: string, + proxyPrefix: string, + token: string, + request: FastifyRequest, + reply: FastifyReply, +): Promise { + return new Promise((resolve, reject) => { + const base = access.baseUrl.endsWith('/') ? access.baseUrl : access.baseUrl + '/'; + const relativePath = path.startsWith('/') ? path.slice(1) : path; + const url = new URL(relativePath, base); + const isHttps = url.protocol === 'https:'; + const transport = isHttps ? https : http; + const options: http.RequestOptions = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method: request.method, + headers: { + host: `${url.hostname}:${url.port || (isHttps ? 443 : 80)}`, + }, + }; + if (isHttps && access.httpsOptions) { + (options as https.RequestOptions).rejectUnauthorized = access.httpsOptions.rejectUnauthorized; + if (access.httpsOptions.ca) { + (options as https.RequestOptions).ca = access.httpsOptions.ca; + } + options.headers = { ...options.headers, Authorization: `Bearer ${token}` }; + } + + const darkThemeJson = JSON.stringify(AGENT_TERMINAL_THEMES.dark); + const lightThemeJson = JSON.stringify(AGENT_TERMINAL_THEMES.light); + + const proxyReq = transport.request(options, proxyRes => { + reply.status(proxyRes.statusCode || 200); + const ct = proxyRes.headers['content-type']; + if (ct) { + reply.header('content-type', ct); + } + + if (ct && ct.includes('text/html')) { + const chunks: Buffer[] = []; + proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk)); + proxyRes.on('end', () => { + let html = Buffer.concat(chunks).toString('utf-8'); + html = html.replace(/href="\//g, `href="${proxyPrefix}/`); + html = html.replace(/src="\//g, `src="${proxyPrefix}/`); + html = html.replace( + '', + `${buildBridgeScript(darkThemeJson, lightThemeJson)}\n`, + ); + reply.header('content-length', Buffer.byteLength(html)); + reply.send(html); + resolve(); + }); + } else { + reply.send(proxyRes); + resolve(); + } + }); + + proxyReq.on('error', err => { + reject(err); + }); + + if (request.raw.readable && !request.body) { + request.raw.pipe(proxyReq); + } else { + proxyReq.end(); + } + }); +} diff --git a/packages/dashboard-backend/src/routes/api/websocket.ts b/packages/dashboard-backend/src/routes/api/websocket.ts index b9cdb57a0b..79bccb4167 100644 --- a/packages/dashboard-backend/src/routes/api/websocket.ts +++ b/packages/dashboard-backend/src/routes/api/websocket.ts @@ -32,7 +32,8 @@ export function webSocketHandler(ws: WebSocket, request: FastifyRequest): void { const subscriptionManager = new SubscriptionManager(ws); const token = getToken(request); - const { eventApi, devworkspaceApi, logsApi, podApi } = getDevWorkspaceClient(token); + const { eventApi, devworkspaceApi, logsApi, podApi, configMapWatchApi } = + getDevWorkspaceClient(token); const channel = api.webSocket.Channel; const watchers = { @@ -40,6 +41,7 @@ export function webSocketHandler(ws: WebSocket, request: FastifyRequest): void { [channel.EVENT]: new ObjectsWatcher(eventApi, channel.EVENT), [channel.LOGS]: new ObjectsWatcher(logsApi, channel.LOGS), [channel.POD]: new ObjectsWatcher(podApi, channel.POD), + [channel.CONFIGMAP]: new ObjectsWatcher(configMapWatchApi, channel.CONFIGMAP), }; function notifyWatchStartError( @@ -67,7 +69,8 @@ export function webSocketHandler(ws: WebSocket, request: FastifyRequest): void { switch (message.channel) { case channel.DEV_WORKSPACE: case channel.EVENT: - case channel.POD: { + case channel.POD: + case channel.CONFIGMAP: { const watcher = watchers[message.channel]; watcher.attach(subscriptionManager); try { @@ -97,8 +100,8 @@ export function webSocketHandler(ws: WebSocket, request: FastifyRequest): void { watcher.stop(); } function handleUnsubscribeAll() { - [channel.DEV_WORKSPACE, channel.EVENT, channel.LOGS, channel.POD].forEach(channel => - handleChannelUnsubscribe(channel), + [channel.DEV_WORKSPACE, channel.EVENT, channel.LOGS, channel.POD, channel.CONFIGMAP].forEach( + channel => handleChannelUnsubscribe(channel), ); } From 8002ee02e881b4a4ea55a249453dfd9117df0303 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:11:17 +0300 Subject: [PATCH 05/16] feat(backend): add AI agent registry API Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .../src/routes/api/aiAgentRegistry.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 packages/dashboard-backend/src/routes/api/aiAgentRegistry.ts diff --git a/packages/dashboard-backend/src/routes/api/aiAgentRegistry.ts b/packages/dashboard-backend/src/routes/api/aiAgentRegistry.ts new file mode 100644 index 0000000000..04885a8335 --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/aiAgentRegistry.ts @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { api } from '@eclipse-che/common'; +import * as k8s from '@kubernetes/client-node'; +import { FastifyInstance } from 'fastify'; + +import { baseApiPath } from '@/constants/config'; +import { getServiceAccountToken } from '@/routes/api/helpers/getServiceAccountToken'; +import { getSchema } from '@/services/helpers'; +import { KubeConfigProvider } from '@/services/kubeclient/kubeConfigProvider'; +import { logger } from '@/utils/logger'; + +const tags = ['AI Agent Registry']; + +const rateLimitConfig = { + config: { + rateLimit: { + max: 30, + timeWindow: '1 minute', + }, + }, +}; + +const AI_AGENT_REGISTRY_LABEL_SELECTOR = + 'app.kubernetes.io/component=ai-agent-registry,app.kubernetes.io/part-of=che.eclipse.org'; + +const EMPTY_REGISTRY: api.IAiAgentRegistry = { agents: [], defaultAgentId: '' }; + +const REGISTRY_CACHE_TTL_MS = 60 * 1000; +let registryCache: { data: api.IAiAgentRegistry; timestamp: number } | undefined; + +function isValidAgent(obj: unknown): obj is api.AiAgentDefinition { + if (typeof obj !== 'object' || obj === null) return false; + const agent = obj as Record; + return ( + typeof agent.id === 'string' && + typeof agent.name === 'string' && + typeof agent.image === 'string' && + typeof agent.tag === 'string' && + typeof agent.terminalPort === 'number' + ); +} + +function parseRegistryJson(raw: string): api.IAiAgentRegistry { + const parsed = JSON.parse(raw) as Record; + const agents = Array.isArray(parsed.agents) ? parsed.agents.filter(isValidAgent) : []; + const defaultAgentId = typeof parsed.defaultAgentId === 'string' ? parsed.defaultAgentId : ''; + return { agents, defaultAgentId }; +} + +export function registerAiAgentRegistryRoute(instance: FastifyInstance) { + instance.register(async server => { + server.get( + `${baseApiPath}/ai-agent-registry`, + Object.assign({}, rateLimitConfig, getSchema({ tags })), + async function (): Promise { + if (registryCache && Date.now() - registryCache.timestamp < REGISTRY_CACHE_TTL_MS) { + return registryCache.data; + } + + const cheNamespace = process.env.CHECLUSTER_CR_NAMESPACE; + if (!cheNamespace) { + logger.warn('CHECLUSTER_CR_NAMESPACE not set, returning empty AI agent registry'); + return EMPTY_REGISTRY; + } + + try { + const token = getServiceAccountToken(); + const provider = new KubeConfigProvider(); + const kc = provider.getKubeConfig(token); + const coreV1Api = kc.makeApiClient(k8s.CoreV1Api); + + const response = await coreV1Api.listNamespacedConfigMap({ + namespace: cheNamespace, + labelSelector: AI_AGENT_REGISTRY_LABEL_SELECTOR, + }); + + const configMaps = response.items; + if (configMaps.length === 0) { + registryCache = { data: EMPTY_REGISTRY, timestamp: Date.now() }; + return EMPTY_REGISTRY; + } + + const data = configMaps[0].data; + if (!data) { + registryCache = { data: EMPTY_REGISTRY, timestamp: Date.now() }; + return EMPTY_REGISTRY; + } + + const registryJson = data['registry.json']; + if (!registryJson) { + registryCache = { data: EMPTY_REGISTRY, timestamp: Date.now() }; + return EMPTY_REGISTRY; + } + + const result = parseRegistryJson(registryJson); + registryCache = { data: result, timestamp: Date.now() }; + return result; + } catch (error) { + logger.error(error, 'Failed to read AI agent registry ConfigMap'); + return EMPTY_REGISTRY; + } + }, + ); + }); +} From fde64229041e7eb45e08d319bfd89aae16d43636 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:11:24 +0300 Subject: [PATCH 06/16] feat(store): add devfile, schema, and agent registry stores Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .../backend-client/aiAgentRegistryApi.ts | 27 + .../backend-client/devfileSchemaApi.ts | 33 ++ .../backend-client/websocketClient/index.ts | 10 + .../websocketClient/subscriptionsManager.ts | 3 +- .../bootstrap/__tests__/index.spec.tsx | 11 +- .../src/services/bootstrap/index.ts | 27 + .../src/services/helpers/types.ts | 1 + .../devworkspace/devWorkspaceClient.ts | 1 + .../src/store/AiAgentRegistry/actions.ts | 41 ++ .../src/store/AiAgentRegistry/index.ts | 20 + .../src/store/AiAgentRegistry/reducer.ts | 53 ++ .../src/store/AiAgentRegistry/selectors.ts | 37 ++ .../src/store/DevfileSchema/actions.ts | 46 ++ .../src/store/DevfileSchema/index.ts | 20 + .../src/store/DevfileSchema/reducer.ts | 48 ++ .../src/store/DevfileSchema/selectors.ts | 23 + .../LocalDevfiles/__tests__/helpers.spec.ts | 124 ++++ .../src/store/LocalDevfiles/index.ts | 536 ++++++++++++++++++ .../src/store/LocalDevfiles/selectors.ts | 50 ++ .../src/store/rootReducer.ts | 6 + 20 files changed, 1109 insertions(+), 8 deletions(-) create mode 100644 packages/dashboard-frontend/src/services/backend-client/aiAgentRegistryApi.ts create mode 100644 packages/dashboard-frontend/src/services/backend-client/devfileSchemaApi.ts create mode 100644 packages/dashboard-frontend/src/store/AiAgentRegistry/actions.ts create mode 100644 packages/dashboard-frontend/src/store/AiAgentRegistry/index.ts create mode 100644 packages/dashboard-frontend/src/store/AiAgentRegistry/reducer.ts create mode 100644 packages/dashboard-frontend/src/store/AiAgentRegistry/selectors.ts create mode 100644 packages/dashboard-frontend/src/store/DevfileSchema/actions.ts create mode 100644 packages/dashboard-frontend/src/store/DevfileSchema/index.ts create mode 100644 packages/dashboard-frontend/src/store/DevfileSchema/reducer.ts create mode 100644 packages/dashboard-frontend/src/store/DevfileSchema/selectors.ts create mode 100644 packages/dashboard-frontend/src/store/LocalDevfiles/__tests__/helpers.spec.ts create mode 100644 packages/dashboard-frontend/src/store/LocalDevfiles/index.ts create mode 100644 packages/dashboard-frontend/src/store/LocalDevfiles/selectors.ts diff --git a/packages/dashboard-frontend/src/services/backend-client/aiAgentRegistryApi.ts b/packages/dashboard-frontend/src/services/backend-client/aiAgentRegistryApi.ts new file mode 100644 index 0000000000..fdc6cbc24d --- /dev/null +++ b/packages/dashboard-frontend/src/services/backend-client/aiAgentRegistryApi.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import common, { api } from '@eclipse-che/common'; +import axios from 'axios'; + +import { dashboardBackendPrefix } from '@/services/backend-client/const'; + +export async function fetchAiAgentRegistry(): Promise { + try { + const response = await axios.get(`${dashboardBackendPrefix}/ai-agent-registry`); + return response.data; + } catch (e) { + throw new Error(`Failed to fetch AI agent registry. ${common.helpers.errors.getMessage(e)}`); + } +} diff --git a/packages/dashboard-frontend/src/services/backend-client/devfileSchemaApi.ts b/packages/dashboard-frontend/src/services/backend-client/devfileSchemaApi.ts new file mode 100644 index 0000000000..4c6700f6e8 --- /dev/null +++ b/packages/dashboard-frontend/src/services/backend-client/devfileSchemaApi.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import common from '@eclipse-che/common'; +import axios from 'axios'; + +import { dashboardBackendPrefix } from '@/services/backend-client/const'; + +export interface DevfileSchema { + definitions?: Record; + properties?: Record; + [key: string]: unknown; +} + +export async function fetchDevfileSchema(version: string): Promise { + try { + const response = await axios.get(`${dashboardBackendPrefix}/devfile`, { + params: { version }, + }); + return response.data as DevfileSchema; + } catch (e) { + throw new Error(`Failed to fetch devfile schema. ${common.helpers.errors.getMessage(e)}`); + } +} diff --git a/packages/dashboard-frontend/src/services/backend-client/websocketClient/index.ts b/packages/dashboard-frontend/src/services/backend-client/websocketClient/index.ts index 930478e9e9..a5d2ffc8db 100644 --- a/packages/dashboard-frontend/src/services/backend-client/websocketClient/index.ts +++ b/packages/dashboard-frontend/src/services/backend-client/websocketClient/index.ts @@ -189,6 +189,16 @@ export class WebsocketClient { this.messageHandler.addListener(channel, listener); } + /** + * Removes a data message listener for the given channel. + */ + public removeChannelMessageListener( + channel: api.webSocket.Channel, + listener: ChannelListener, + ): void { + this.messageHandler.removeListener(channel, listener); + } + /** * Returns true if the given channel has a listener. */ diff --git a/packages/dashboard-frontend/src/services/backend-client/websocketClient/subscriptionsManager.ts b/packages/dashboard-frontend/src/services/backend-client/websocketClient/subscriptionsManager.ts index b5e334f902..7177da3211 100644 --- a/packages/dashboard-frontend/src/services/backend-client/websocketClient/subscriptionsManager.ts +++ b/packages/dashboard-frontend/src/services/backend-client/websocketClient/subscriptionsManager.ts @@ -20,7 +20,8 @@ export type SubscriptionArgs = channel: | api.webSocket.Channel.DEV_WORKSPACE | api.webSocket.Channel.EVENT - | api.webSocket.Channel.POD, + | api.webSocket.Channel.POD + | api.webSocket.Channel.CONFIGMAP, namespace: string, options: { getResourceVersion: ReturnResourceVersion }, ] diff --git a/packages/dashboard-frontend/src/services/bootstrap/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/services/bootstrap/__tests__/index.spec.tsx index d7ed199d41..dc8fcbb775 100644 --- a/packages/dashboard-frontend/src/services/bootstrap/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/services/bootstrap/__tests__/index.spec.tsx @@ -153,19 +153,16 @@ describe('Dashboard bootstrap', () => { expect(mockWebsocketClient.connect).toHaveBeenCalledTimes(3); - expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenCalledTimes(3); - expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenNthCalledWith( - 1, + expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenCalledTimes(4); + expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenCalledWith( 'devWorkspace', expect.any(Function), ); - expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenNthCalledWith( - 2, + expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenCalledWith( 'event', expect.any(Function), ); - expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenNthCalledWith( - 3, + expect(mockWebsocketClient.addChannelMessageListener).toHaveBeenCalledWith( 'pod', expect.any(Function), ); diff --git a/packages/dashboard-frontend/src/services/bootstrap/index.ts b/packages/dashboard-frontend/src/services/bootstrap/index.ts index 354ac86d13..0cc1d20ce0 100644 --- a/packages/dashboard-frontend/src/services/bootstrap/index.ts +++ b/packages/dashboard-frontend/src/services/bootstrap/index.ts @@ -35,6 +35,7 @@ import { ResourceFetcherService } from '@/services/resource-fetcher'; import { Workspace } from '@/services/workspace-adapter'; import { hasLoginPage, isForbidden, isUnauthorized } from '@/services/workspace-client/helpers'; import { RootState } from '@/store'; +import { aiAgentRegistryActionCreators } from '@/store/AiAgentRegistry'; import { aiConfigActionCreators } from '@/store/AiConfig'; import { selectAiConfigEnabled } from '@/store/AiConfig/selectors'; import { bannerAlertActionCreators } from '@/store/BannerAlert'; @@ -51,6 +52,7 @@ import { infrastructureNamespacesActionCreators, selectDefaultNamespace, } from '@/store/InfrastructureNamespaces'; +import { actionCreators as localDevfilesActionCreators } from '@/store/LocalDevfiles'; import { chePluginsActionCreators } from '@/store/Plugins/chePlugins'; import { devWorkspacePluginsActionCreators } from '@/store/Plugins/devWorkspacePlugins'; import { podsActionCreators, selectPodsResourceVersion } from '@/store/Pods'; @@ -122,9 +124,12 @@ export default class Bootstrap { }), this.fetchPods().then(() => { this.watchWebSocketPods(); + this.subscribeToAgentPodChanges(); }), this.fetchSshKeys(), this.fetchWorkspacePreferences(), + this.fetchLocalDevfiles(), + this.fetchAiAgentRegistry(), this.fetchAiRegistry().then(() => this.fetchAiProviderKeyStatus()), ]); @@ -278,6 +283,11 @@ export default class Bootstrap { }); } + private async subscribeToAgentPodChanges(): Promise { + const { subscribeToAgentPodChanges } = localDevfilesActionCreators; + await subscribeToAgentPodChanges()(this.store.dispatch, this.store.getState, undefined); + } + private async fetchWorkspaces(): Promise { const { requestWorkspaces } = workspacesActionCreators; await requestWorkspaces()(this.store.dispatch, this.store.getState, undefined); @@ -487,4 +497,21 @@ export default class Bootstrap { undefined, ); } + + private async fetchLocalDevfiles(): Promise { + await localDevfilesActionCreators.requestDevfiles()( + this.store.dispatch, + this.store.getState, + undefined, + ); + } + + private async fetchAiAgentRegistry(): Promise { + const { requestAiAgentRegistry } = aiAgentRegistryActionCreators; + try { + await requestAiAgentRegistry()(this.store.dispatch, this.store.getState, undefined); + } catch (e) { + console.warn('Unable to fetch AI agent registry.', e); + } + } } diff --git a/packages/dashboard-frontend/src/services/helpers/types.ts b/packages/dashboard-frontend/src/services/helpers/types.ts index c5bcaa6f98..62eb676ca7 100644 --- a/packages/dashboard-frontend/src/services/helpers/types.ts +++ b/packages/dashboard-frontend/src/services/helpers/types.ts @@ -102,6 +102,7 @@ export enum LoaderTab { Progress = 'Progress', Logs = 'Logs', Events = 'Events', + AiAgent = 'AI Agent', } export enum WorkspaceDetailsTab { diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts index 9d901ee0c8..fba01d15f0 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts @@ -52,6 +52,7 @@ export const COMPONENT_UPDATE_POLICY = 'che.eclipse.org/components-update-policy export const REGISTRY_URL = 'che.eclipse.org/plugin-registry-url'; export const DEVWORKSPACE_LABEL_METADATA_NAME = 'kubernetes.io/metadata.name'; +export const DEVWORKSPACE_LABEL_RESOURCE_TYPE = 'che.eclipse.org/type'; export const DEVWORKSPACE_NEXT_START_ANNOTATION = 'che.eclipse.org/next-start-cfg'; export const DEVWORKSPACE_DEVFILE_SOURCE = 'che.eclipse.org/devfile-source'; diff --git a/packages/dashboard-frontend/src/store/AiAgentRegistry/actions.ts b/packages/dashboard-frontend/src/store/AiAgentRegistry/actions.ts new file mode 100644 index 0000000000..0768f25949 --- /dev/null +++ b/packages/dashboard-frontend/src/store/AiAgentRegistry/actions.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import common, { api } from '@eclipse-che/common'; +import { createAction } from '@reduxjs/toolkit'; + +import { fetchAiAgentRegistry } from '@/services/backend-client/aiAgentRegistryApi'; +import { AppThunk } from '@/store'; + +export const aiAgentRegistryRequestAction = createAction('aiAgentRegistry/request'); +export const aiAgentRegistryReceiveAction = + createAction('aiAgentRegistry/receive'); +export const aiAgentRegistryErrorAction = createAction('aiAgentRegistry/error'); + +export const actionCreators = { + requestAiAgentRegistry: + (): AppThunk => + async (dispatch): Promise => { + try { + dispatch(aiAgentRegistryRequestAction()); + + const registry = await fetchAiAgentRegistry(); + dispatch(aiAgentRegistryReceiveAction(registry)); + } catch (e) { + const errorMessage = + 'Failed to fetch AI agent registry, reason: ' + common.helpers.errors.getMessage(e); + dispatch(aiAgentRegistryErrorAction(errorMessage)); + } + }, +}; diff --git a/packages/dashboard-frontend/src/store/AiAgentRegistry/index.ts b/packages/dashboard-frontend/src/store/AiAgentRegistry/index.ts new file mode 100644 index 0000000000..f846ae2627 --- /dev/null +++ b/packages/dashboard-frontend/src/store/AiAgentRegistry/index.ts @@ -0,0 +1,20 @@ +/* c8 ignore start */ + +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +export { actionCreators as aiAgentRegistryActionCreators } from '@/store/AiAgentRegistry/actions'; +export { + reducer as aiAgentRegistryReducer, + State as AiAgentRegistryState, +} from '@/store/AiAgentRegistry/reducer'; +export * from '@/store/AiAgentRegistry/selectors'; diff --git a/packages/dashboard-frontend/src/store/AiAgentRegistry/reducer.ts b/packages/dashboard-frontend/src/store/AiAgentRegistry/reducer.ts new file mode 100644 index 0000000000..f973d14abe --- /dev/null +++ b/packages/dashboard-frontend/src/store/AiAgentRegistry/reducer.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { api } from '@eclipse-che/common'; +import { createReducer } from '@reduxjs/toolkit'; + +import { + aiAgentRegistryErrorAction, + aiAgentRegistryReceiveAction, + aiAgentRegistryRequestAction, +} from '@/store/AiAgentRegistry/actions'; + +export interface State { + isLoading: boolean; + agents: api.AiAgentDefinition[]; + defaultAgentId: string; + error?: string; +} + +export const unloadedState: State = { + isLoading: false, + agents: [], + defaultAgentId: '', +}; + +export const reducer = createReducer(unloadedState, builder => + builder + .addCase(aiAgentRegistryRequestAction, state => { + state.isLoading = true; + state.error = undefined; + }) + .addCase(aiAgentRegistryReceiveAction, (state, action) => { + state.isLoading = false; + state.agents = action.payload.agents; + state.defaultAgentId = action.payload.defaultAgentId; + }) + .addCase(aiAgentRegistryErrorAction, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addDefaultCase(state => state), +); diff --git a/packages/dashboard-frontend/src/store/AiAgentRegistry/selectors.ts b/packages/dashboard-frontend/src/store/AiAgentRegistry/selectors.ts new file mode 100644 index 0000000000..89b26f6f02 --- /dev/null +++ b/packages/dashboard-frontend/src/store/AiAgentRegistry/selectors.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { createSelector } from '@reduxjs/toolkit'; + +import { RootState } from '@/store'; + +const selectState = (state: RootState) => state.aiAgentRegistry; + +export const selectAiAgents = createSelector(selectState, state => state.agents); + +export const selectDefaultAgentId = createSelector(selectState, state => state.defaultAgentId); + +export const selectDefaultAgent = createSelector(selectState, state => { + if (state.agents.length === 0) return undefined; + return state.agents.find(a => a.id === state.defaultAgentId) || state.agents[0]; +}); + +export const selectAiAgentRegistryEnabled = createSelector( + selectState, + state => state.agents.length > 0, +); + +export const selectAiAgentRegistryIsLoading = createSelector(selectState, state => state.isLoading); + +export const selectAiAgentRegistryError = createSelector(selectState, state => state.error); diff --git a/packages/dashboard-frontend/src/store/DevfileSchema/actions.ts b/packages/dashboard-frontend/src/store/DevfileSchema/actions.ts new file mode 100644 index 0000000000..e21070b391 --- /dev/null +++ b/packages/dashboard-frontend/src/store/DevfileSchema/actions.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import common from '@eclipse-che/common'; +import { createAction } from '@reduxjs/toolkit'; + +import { DevfileSchema, fetchDevfileSchema } from '@/services/backend-client/devfileSchemaApi'; +import { AppThunk } from '@/store'; + +export const devfileSchemaRequestAction = createAction('devfileSchema/request'); +export const devfileSchemaReceiveAction = createAction('devfileSchema/receive'); +export const devfileSchemaErrorAction = createAction('devfileSchema/error'); + +const DEFAULT_VERSION = 'latest'; + +export const actionCreators = { + requestDevfileSchema: + (): AppThunk => + async (dispatch, getState): Promise => { + const state = getState().devfileSchema; + if (state.isLoading || state.schema !== undefined) { + return; + } + + try { + dispatch(devfileSchemaRequestAction()); + + const schema = await fetchDevfileSchema(DEFAULT_VERSION); + dispatch(devfileSchemaReceiveAction(schema)); + } catch (e) { + const errorMessage = + 'Failed to fetch devfile schema, reason: ' + common.helpers.errors.getMessage(e); + dispatch(devfileSchemaErrorAction(errorMessage)); + throw e; + } + }, +}; diff --git a/packages/dashboard-frontend/src/store/DevfileSchema/index.ts b/packages/dashboard-frontend/src/store/DevfileSchema/index.ts new file mode 100644 index 0000000000..092b3b0b01 --- /dev/null +++ b/packages/dashboard-frontend/src/store/DevfileSchema/index.ts @@ -0,0 +1,20 @@ +/* c8 ignore start */ + +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +export { actionCreators as devfileSchemaActionCreators } from '@/store/DevfileSchema/actions'; +export { + reducer as devfileSchemaReducer, + State as DevfileSchemaState, +} from '@/store/DevfileSchema/reducer'; +export * from '@/store/DevfileSchema/selectors'; diff --git a/packages/dashboard-frontend/src/store/DevfileSchema/reducer.ts b/packages/dashboard-frontend/src/store/DevfileSchema/reducer.ts new file mode 100644 index 0000000000..6bb2fdb177 --- /dev/null +++ b/packages/dashboard-frontend/src/store/DevfileSchema/reducer.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { createReducer } from '@reduxjs/toolkit'; + +import { DevfileSchema } from '@/services/backend-client/devfileSchemaApi'; +import { + devfileSchemaErrorAction, + devfileSchemaReceiveAction, + devfileSchemaRequestAction, +} from '@/store/DevfileSchema/actions'; + +export interface State { + isLoading: boolean; + schema: DevfileSchema | undefined; + error?: string; +} + +export const unloadedState: State = { + isLoading: false, + schema: undefined, +}; + +export const reducer = createReducer(unloadedState, builder => + builder + .addCase(devfileSchemaRequestAction, state => { + state.isLoading = true; + state.error = undefined; + }) + .addCase(devfileSchemaReceiveAction, (state, action) => { + state.isLoading = false; + state.schema = action.payload; + }) + .addCase(devfileSchemaErrorAction, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addDefaultCase(state => state), +); diff --git a/packages/dashboard-frontend/src/store/DevfileSchema/selectors.ts b/packages/dashboard-frontend/src/store/DevfileSchema/selectors.ts new file mode 100644 index 0000000000..bd6f74e723 --- /dev/null +++ b/packages/dashboard-frontend/src/store/DevfileSchema/selectors.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { createSelector } from '@reduxjs/toolkit'; + +import { RootState } from '@/store'; + +const selectState = (state: RootState) => state.devfileSchema; + +export const selectDevfileSchema = createSelector(selectState, state => state.schema); + +export const selectDevfileSchemaIsLoading = createSelector(selectState, state => state.isLoading); + +export const selectDevfileSchemaError = createSelector(selectState, state => state.error); diff --git a/packages/dashboard-frontend/src/store/LocalDevfiles/__tests__/helpers.spec.ts b/packages/dashboard-frontend/src/store/LocalDevfiles/__tests__/helpers.spec.ts new file mode 100644 index 0000000000..1b2a8fdf8f --- /dev/null +++ b/packages/dashboard-frontend/src/store/LocalDevfiles/__tests__/helpers.spec.ts @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { extractProjectNames } from '@/store/LocalDevfiles'; + +describe('extractProjectNames', () => { + test('returns empty array when no projects section exists', () => { + const content = `schemaVersion: 2.2.2 +metadata: + name: my-project +components: + - name: tools + container: + image: node:18`; + + expect(extractProjectNames(content)).toEqual([]); + }); + + test('returns project names from projects section', () => { + const content = `schemaVersion: 2.2.2 +metadata: + name: my-project +projects: + - name: frontend + git: + remotes: + origin: https://github.com/user/frontend.git + - name: backend + git: + remotes: + origin: https://github.com/user/backend.git`; + + expect(extractProjectNames(content)).toEqual(['frontend', 'backend']); + }); + + test('returns single project name', () => { + const content = `schemaVersion: 2.2.2 +projects: + - name: my-app + git: + remotes: + origin: https://github.com/user/app.git`; + + expect(extractProjectNames(content)).toEqual(['my-app']); + }); + + test('does not include component names as project names', () => { + const content = `schemaVersion: 2.2.2 +components: + - name: tools + container: + image: node:18 +projects: + - name: my-app + git: + remotes: + origin: https://github.com/user/app.git`; + + const result = extractProjectNames(content); + expect(result).toEqual(['my-app']); + expect(result).not.toContain('tools'); + }); + + test('returns empty array when projects section has no entries', () => { + const content = `schemaVersion: 2.2.2 +metadata: + name: my-project +projects: []`; + + expect(extractProjectNames(content)).toEqual([]); + }); + + test('does not include component or endpoint names when projects is before components', () => { + const content = `schemaVersion: 2.2.2 +metadata: + name: go-web-server +projects: + - name: gritty + git: + remotes: + origin: https://github.com/cloudcmd/gritty.git +components: + - name: tools + container: + image: registry.access.redhat.com/ubi9/nodejs-20:latest + endpoints: + - name: http + targetPort: 1337 + - name: npm-cache + volume: + size: 1Gi`; + + const result = extractProjectNames(content); + expect(result).toEqual(['gritty']); + }); + + test('handles multiple projects with various whitespace', () => { + const content = `schemaVersion: 2.2.2 +projects: + - name: app-one + git: + remotes: + origin: https://github.com/user/one.git + - name: app-two + git: + remotes: + origin: https://github.com/user/two.git + - name: app-three + git: + remotes: + origin: https://github.com/user/three.git`; + + expect(extractProjectNames(content)).toEqual(['app-one', 'app-two', 'app-three']); + }); +}); diff --git a/packages/dashboard-frontend/src/store/LocalDevfiles/index.ts b/packages/dashboard-frontend/src/store/LocalDevfiles/index.ts new file mode 100644 index 0000000000..9c0dfd7502 --- /dev/null +++ b/packages/dashboard-frontend/src/store/LocalDevfiles/index.ts @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { api } from '@eclipse-che/common'; +import { createAction, createReducer } from '@reduxjs/toolkit'; +import { load } from 'js-yaml'; + +import { container } from '@/inversify.config'; +import { WebsocketClient } from '@/services/backend-client/websocketClient'; +import { ChannelListener } from '@/services/backend-client/websocketClient/messageHandler'; +import { AppThunk } from '@/store'; +import { selectDefaultNamespace } from '@/store/InfrastructureNamespaces/selectors'; + +export interface LocalDevfile { + id: string; + name: string; + description: string; + content: string; + projectNames: string[]; + lastModified: string; +} + +export enum AgentPodPhase { + PENDING = 'Pending', + RUNNING = 'Running', + SUCCEEDED = 'Succeeded', + FAILED = 'Failed', + UNKNOWN = 'Unknown', +} + +export interface AgentPodStatus { + agentId: string; + name: string; + phase: AgentPodPhase; + ready: boolean; + serviceUrl: string | undefined; +} + +export interface LocalDevfilesState { + devfiles: LocalDevfile[]; + isLoading: boolean; + error: string | undefined; + agentTerminalUrl: string | undefined; + agentPodStatuses: AgentPodStatus[]; + configMapResourceVersion: string; +} + +const initialState: LocalDevfilesState = { + devfiles: [], + isLoading: false, + error: undefined, + agentTerminalUrl: undefined, + agentPodStatuses: [], + configMapResourceVersion: '', +}; + +const requestStart = createAction('localDevfiles/requestStart'); +const requestSuccess = createAction('localDevfiles/requestSuccess'); +const requestError = createAction('localDevfiles/requestError'); +const addDevfile = createAction('localDevfiles/addDevfile'); +const updateDevfileAction = createAction('localDevfiles/updateDevfile'); +const removeDevfile = createAction('localDevfiles/removeDevfile'); +const setAgentTerminalUrl = createAction('localDevfiles/setAgentTerminalUrl'); +const upsertAgentPodStatus = createAction('localDevfiles/upsertAgentPodStatus'); +const removeAgentPodStatus = createAction('localDevfiles/removeAgentPodStatus'); +const setConfigMapResourceVersion = createAction( + 'localDevfiles/setConfigMapResourceVersion', +); + +export const localDevfilesReducer = createReducer(initialState, builder => + builder + .addCase(requestStart, state => { + state.isLoading = true; + state.error = undefined; + }) + .addCase(requestSuccess, (state, action) => { + state.devfiles = action.payload; + state.isLoading = false; + }) + .addCase(requestError, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addCase(addDevfile, (state, action) => { + state.devfiles.push(action.payload); + }) + .addCase(updateDevfileAction, (state, action) => { + const index = state.devfiles.findIndex(d => d.id === action.payload.id); + if (index !== -1) { + state.devfiles[index] = action.payload; + } + }) + .addCase(removeDevfile, (state, action) => { + state.devfiles = state.devfiles.filter(d => d.id !== action.payload); + }) + .addCase(setAgentTerminalUrl, (state, action) => { + state.agentTerminalUrl = action.payload; + }) + .addCase(upsertAgentPodStatus, (state, action) => { + const idx = state.agentPodStatuses.findIndex(s => s.agentId === action.payload.agentId); + if (idx >= 0) { + state.agentPodStatuses[idx] = action.payload; + } else { + state.agentPodStatuses.push(action.payload); + } + }) + .addCase(removeAgentPodStatus, (state, action) => { + state.agentPodStatuses = state.agentPodStatuses.filter(s => s.agentId !== action.payload); + }) + .addCase(setConfigMapResourceVersion, (state, action) => { + state.configMapResourceVersion = action.payload; + }) + .addDefaultCase(state => state), +); + +const DEVFILES_API_BASE = '/dashboard/api/devfiles/namespace'; +const AGENTS_API_BASE = '/dashboard/api/namespace'; + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function parseRetryAfter(response: Response): Promise { + const header = response.headers.get('Retry-After'); + if (header) { + const seconds = parseInt(header, 10); + if (!isNaN(seconds)) { + return seconds * 1000; + } + } + try { + const body = (await response.clone().json()) as { message?: string }; + const match = body.message?.match(/retry in (\d+)/i); + if (match) { + return parseInt(match[1], 10) * 1000; + } + } catch { + // ignore + } + return 10_000; +} + +export const clearAgentTerminalUrl = (): AppThunk => async dispatch => { + dispatch(setAgentTerminalUrl(undefined)); +}; + +const AGENT_LABEL_COMPONENT = 'app.kubernetes.io/component'; + +let configMapListener: ChannelListener | undefined; +let agentPodListener: ChannelListener | undefined; + +function handleAgentPodWebSocketMessage( + dispatch: (action: unknown) => void, + message: api.webSocket.NotificationMessage, +): void { + if (!api.webSocket.isPodMessage(message)) return; + + const { pod, eventPhase } = message; + const labels = pod.metadata?.labels || {}; + if (labels[AGENT_LABEL_COMPONENT] !== 'ai-agent') return; + + const podAgentId = pod.metadata?.annotations?.['che.eclipse.org/ai-agent-id'] || ''; + + if (eventPhase === api.webSocket.EventPhase.DELETED || pod.metadata?.deletionTimestamp) { + dispatch(removeAgentPodStatus(podAgentId)); + return; + } + + const phase = (pod.status?.phase as AgentPodPhase) || AgentPodPhase.UNKNOWN; + + if (phase === AgentPodPhase.SUCCEEDED || phase === AgentPodPhase.FAILED) { + dispatch(removeAgentPodStatus(podAgentId)); + return; + } + + const containerReady = pod.status?.containerStatuses?.some(c => c.ready === true) || false; + + dispatch( + upsertAgentPodStatus({ + agentId: podAgentId, + name: pod.metadata?.name || '', + phase, + ready: phase === AgentPodPhase.RUNNING && containerReady, + serviceUrl: undefined, + }), + ); +} + +const MODIFIED_ANNOTATION_PREFIX = 'che.eclipse.org/modified-'; + +function handleConfigMapWebSocketMessage( + dispatch: (action: unknown) => void, + message: api.webSocket.NotificationMessage, +): void { + if (api.webSocket.isConfigMapMessage(message)) { + const { configMap } = message; + const resourceVersion = configMap.metadata?.resourceVersion; + if (resourceVersion) { + dispatch(setConfigMapResourceVersion(resourceVersion)); + } + const devfiles = parseConfigMapData(configMap.data, configMap.metadata?.annotations); + dispatch(requestSuccess(devfiles)); + } +} + +function parseConfigMapData( + data: Record | undefined, + annotations?: Record, +): LocalDevfile[] { + if (!data) return []; + return Object.entries(data).map(([id, content]) => { + const storedTimestamp = annotations?.[`${MODIFIED_ANNOTATION_PREFIX}${id}`]; + return { + id, + name: extractName(content), + description: extractDescription(content), + content, + projectNames: extractProjectNames(content), + lastModified: storedTimestamp || new Date().toISOString(), + }; + }); +} + +export const actionCreators = { + requestDevfiles: (): AppThunk => async (dispatch, getState) => { + dispatch(requestStart()); + try { + const namespace = selectDefaultNamespace(getState()).name; + const response = await fetch(`${DEVFILES_API_BASE}/${namespace}`); + if (!response.ok) { + throw new Error(`Failed to fetch devfiles: ${response.statusText}`); + } + const data = (await response.json()) as { devfiles: LocalDevfile[] }; + const devfiles = data.devfiles.map(d => ({ + ...d, + projectNames: d.content ? extractProjectNames(d.content) : [], + })); + dispatch(requestSuccess(devfiles)); + } catch (e) { + const message = e instanceof Error ? e.message : 'Unknown error'; + dispatch(requestError(message)); + } + }, + + createDevfile: + (name: string, description: string, content: string): AppThunk> => + async (dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + const response = await fetch(`${DEVFILES_API_BASE}/${namespace}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }), + }); + if (!response.ok) { + throw new Error(`Failed to create devfile: ${response.statusText}`); + } + const data = (await response.json()) as { id: string }; + dispatch( + addDevfile({ + id: data.id, + name, + description, + content, + projectNames: extractProjectNames(content), + lastModified: new Date().toISOString(), + }), + ); + return data.id; + }, + + saveDevfile: + (id: string, content: string): AppThunk => + async (dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + const response = await fetch(`${DEVFILES_API_BASE}/${namespace}/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }), + }); + if (!response.ok) { + throw new Error(`Failed to save devfile: ${response.statusText}`); + } + dispatch( + updateDevfileAction({ + id, + name: extractName(content), + description: extractDescription(content), + content, + projectNames: extractProjectNames(content), + lastModified: new Date().toISOString(), + }), + ); + }, + + deleteDevfile: + (id: string): AppThunk => + async (dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + const response = await fetch(`${DEVFILES_API_BASE}/${namespace}/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error(`Failed to delete devfile: ${response.statusText}`); + } + dispatch(removeDevfile(id)); + }, + + startAgent: + (agent: api.AiAgentDefinition, instanceId?: string): AppThunk => + async (dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + const MAX_RETRIES = 3; + const agentId = instanceId || agent.id; + + dispatch( + upsertAgentPodStatus({ + agentId, + name: '', + phase: AgentPodPhase.PENDING, + ready: false, + serviceUrl: undefined, + }), + ); + + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + const response = await fetch(`${AGENTS_API_BASE}/${namespace}/agent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + agentId, + image: agent.image, + tag: agent.tag, + memoryLimit: agent.memoryLimit, + cpuLimit: agent.cpuLimit, + terminalPort: agent.terminalPort, + env: agent.env, + }), + }); + + if (response.status === 429 && attempt < MAX_RETRIES) { + const retryAfter = await parseRetryAfter(response); + await delay(retryAfter); + continue; + } + + if (!response.ok) { + const text = await response.text(); + dispatch(removeAgentPodStatus(agentId)); + throw new Error(`Failed to start agent: ${text}`); + } + + const status = (await response.json()) as AgentPodStatus; + dispatch(upsertAgentPodStatus(status)); + return; + } + }, + + stopAgent: + (agentId: string): AppThunk => + async (dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + dispatch(setAgentTerminalUrl(undefined)); + dispatch(removeAgentPodStatus(agentId)); + + await fetch(`${AGENTS_API_BASE}/${namespace}/agent/${encodeURIComponent(agentId)}`, { + method: 'DELETE', + }); + }, + + fetchAgentStatus: + (agentId: string, terminalPort = 8080): AppThunk => + async (dispatch, getState) => { + try { + const namespace = selectDefaultNamespace(getState()).name; + const params = new URLSearchParams({ terminalPort: String(terminalPort) }); + const response = await fetch( + `${AGENTS_API_BASE}/${namespace}/agent/${encodeURIComponent(agentId)}?${params.toString()}`, + ); + if (!response.ok) { + return; + } + const status = (await response.json()) as AgentPodStatus; + dispatch(upsertAgentPodStatus(status)); + } catch { + // network error — keep current status + } + }, + + sendHeartbeat: + (agentId: string): AppThunk => + async (_dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + const response = await fetch( + `${AGENTS_API_BASE}/${namespace}/agent/${encodeURIComponent(agentId)}/heartbeat`, + { + method: 'POST', + }, + ); + if (!response.ok) { + throw new Error(`Heartbeat failed: ${response.status}`); + } + }, + + fetchAgentTerminalUrl: + (agentId = 'anthropic/claude-code', terminalPort = 8080): AppThunk => + async (dispatch, getState) => { + const namespace = selectDefaultNamespace(getState()).name; + const params = new URLSearchParams({ + agentId, + terminalPort: String(terminalPort), + }); + const response = await fetch( + `${AGENTS_API_BASE}/${namespace}/agent-terminal-url?${params.toString()}`, + ); + if (!response.ok) { + dispatch(setAgentTerminalUrl(undefined)); + throw new Error(`Terminal URL fetch failed: ${response.status}`); + } + const terminalUrl = `${AGENTS_API_BASE}/${namespace}/agent/t/?${params.toString()}`; + dispatch(setAgentTerminalUrl(terminalUrl)); + }, + + subscribeToConfigMapChanges: (): AppThunk => async (dispatch, getState) => { + if (configMapListener) return; + + const websocketClient = container.get(WebsocketClient); + const namespace = selectDefaultNamespace(getState()).name; + + const listener: ChannelListener = message => { + handleConfigMapWebSocketMessage(dispatch, message); + }; + websocketClient.addChannelMessageListener(api.webSocket.Channel.CONFIGMAP, listener); + configMapListener = listener; + + const resourceVersion = getState().localDevfiles.configMapResourceVersion; + websocketClient.subscribeToChannel(api.webSocket.Channel.CONFIGMAP, namespace, { + getResourceVersion: () => resourceVersion || '', + }); + }, + + unsubscribeFromConfigMapChanges: (): AppThunk => async () => { + if (!configMapListener) return; + + const websocketClient = container.get(WebsocketClient); + websocketClient.removeChannelMessageListener( + api.webSocket.Channel.CONFIGMAP, + configMapListener, + ); + websocketClient.unsubscribeFromChannel(api.webSocket.Channel.CONFIGMAP); + configMapListener = undefined; + }, + + subscribeToAgentPodChanges: (): AppThunk => async (dispatch, getState) => { + if (agentPodListener) return; + + const websocketClient = container.get(WebsocketClient); + + const listener: ChannelListener = message => { + handleAgentPodWebSocketMessage(dispatch, message); + }; + websocketClient.addChannelMessageListener(api.webSocket.Channel.POD, listener); + agentPodListener = listener; + + const pods = getState().pods.pods; + for (const pod of pods) { + const labels = pod.metadata?.labels || {}; + if (labels[AGENT_LABEL_COMPONENT] !== 'ai-agent') continue; + if (pod.metadata?.deletionTimestamp) continue; + + const phase = (pod.status?.phase as AgentPodPhase) || AgentPodPhase.UNKNOWN; + if (phase === AgentPodPhase.SUCCEEDED || phase === AgentPodPhase.FAILED) continue; + + const agentId = pod.metadata?.annotations?.['che.eclipse.org/ai-agent-id'] || ''; + const containerReady = pod.status?.containerStatuses?.some(c => c.ready === true) || false; + dispatch( + upsertAgentPodStatus({ + agentId, + name: pod.metadata?.name || '', + phase, + ready: phase === AgentPodPhase.RUNNING && containerReady, + serviceUrl: undefined, + }), + ); + } + }, +}; + +function extractName(content: string): string { + const nameMatch = content.match(/^\s*name:\s*(.+)$/m); + if (nameMatch) { + return nameMatch[1].trim(); + } + const generateNameMatch = content.match(/^\s*generateName:\s*(.+)$/m); + if (generateNameMatch) { + return generateNameMatch[1].trim(); + } + return 'untitled'; +} + +function extractDescription(content: string): string { + const match = content.match(/^\s*description:\s*(.+)$/m); + return match ? match[1].trim() : ''; +} + +interface DevfileYaml { + projects?: Array<{ name?: string }>; +} + +export function buildAgentWorkspaceName(agentId: string): string { + return 'agent-' + agentId.replace(/\//g, '-').replace(/[^a-z0-9-]/g, ''); +} + +export function extractProjectNames(content: string): string[] { + try { + const parsed = load(content) as DevfileYaml | null; + if (!parsed?.projects) { + return []; + } + return parsed.projects + .map(p => p.name) + .filter((name): name is string => typeof name === 'string' && name.length > 0); + } catch { + return []; + } +} diff --git a/packages/dashboard-frontend/src/store/LocalDevfiles/selectors.ts b/packages/dashboard-frontend/src/store/LocalDevfiles/selectors.ts new file mode 100644 index 0000000000..f78420055d --- /dev/null +++ b/packages/dashboard-frontend/src/store/LocalDevfiles/selectors.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { createSelector } from '@reduxjs/toolkit'; + +import { RootState } from '@/store'; + +const selectLocalDevfilesState = (state: RootState) => state.localDevfiles; + +export const selectLocalDevfiles = createSelector( + selectLocalDevfilesState, + state => state.devfiles, +); + +export const selectLocalDevfilesIsLoading = createSelector( + selectLocalDevfilesState, + state => state.isLoading, +); + +export const selectLocalDevfilesError = createSelector( + selectLocalDevfilesState, + state => state.error, +); + +export const selectLocalDevfileById = (id: string) => + createSelector(selectLocalDevfiles, devfiles => devfiles.find(d => d.id === id)); + +export const selectLocalDevfileByName = (name: string) => + createSelector(selectLocalDevfiles, devfiles => devfiles.find(d => d.name === name)); + +export const selectAgentTerminalUrl = createSelector( + selectLocalDevfilesState, + state => state.agentTerminalUrl, +); + +export const selectAgentPodStatuses = createSelector( + selectLocalDevfilesState, + state => state.agentPodStatuses, +); diff --git a/packages/dashboard-frontend/src/store/rootReducer.ts b/packages/dashboard-frontend/src/store/rootReducer.ts index 11df18a5ea..b8314fba62 100644 --- a/packages/dashboard-frontend/src/store/rootReducer.ts +++ b/packages/dashboard-frontend/src/store/rootReducer.ts @@ -10,6 +10,7 @@ * Red Hat, Inc. - initial API and implementation */ +import { aiAgentRegistryReducer } from '@/store/AiAgentRegistry'; import { aiConfigReducer } from '@/store/AiConfig'; import { backupsReducer } from '@/store/Backups'; import { bannerAlertReducer } from '@/store/BannerAlert'; @@ -17,6 +18,7 @@ import { brandingReducer } from '@/store/Branding'; import { clusterConfigReducer } from '@/store/ClusterConfig'; import { clusterInfoReducer } from '@/store/ClusterInfo'; import { devfileRegistriesReducer } from '@/store/DevfileRegistries'; +import { devfileSchemaReducer } from '@/store/DevfileSchema'; import { devWorkspacesClusterReducer } from '@/store/DevWorkspacesCluster'; import { dockerConfigReducer } from '@/store/DockerConfig'; import { eventsReducer } from '@/store/Events'; @@ -24,6 +26,7 @@ import { factoryResolverReducer } from '@/store/FactoryResolver'; import { gitConfigReducer } from '@/store/GitConfig'; import { gitOauthConfigReducer } from '@/store/GitOauthConfig'; import { infrastructureNamespacesReducer } from '@/store/InfrastructureNamespaces'; +import { localDevfilesReducer } from '@/store/LocalDevfiles'; import { personalAccessTokenReducer } from '@/store/PersonalAccessTokens'; import { chePluginsReducer } from '@/store/Plugins/chePlugins'; import { devWorkspacePluginsReducer } from '@/store/Plugins/devWorkspacePlugins'; @@ -39,6 +42,7 @@ import { devWorkspacesReducer } from '@/store/Workspaces/devWorkspaces'; import { workspacePreferencesReducer } from '@/store/Workspaces/Preferences'; export const rootReducer = { + aiAgentRegistry: aiAgentRegistryReducer, aiConfig: aiConfigReducer, backups: backupsReducer, bannerAlert: bannerAlertReducer, @@ -46,6 +50,7 @@ export const rootReducer = { clusterConfig: clusterConfigReducer, clusterInfo: clusterInfoReducer, devfileRegistries: devfileRegistriesReducer, + devfileSchema: devfileSchemaReducer, devWorkspaces: devWorkspacesReducer, devWorkspacesCluster: devWorkspacesClusterReducer, dockerConfig: dockerConfigReducer, @@ -56,6 +61,7 @@ export const rootReducer = { gitConfig: gitConfigReducer, gitOauthConfig: gitOauthConfigReducer, infrastructureNamespaces: infrastructureNamespacesReducer, + localDevfiles: localDevfilesReducer, logs: podLogsReducer, personalAccessToken: personalAccessTokenReducer, plugins: chePluginsReducer, From 373d3fa1eb88174cad907ddedc9e0925540ae866 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:11:28 +0300 Subject: [PATCH 07/16] feat(editor): add CodeMirror YAML editor with schema validation Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .../BasicViewer/__tests__/index.spec.tsx | 17 +- .../components/BasicViewer/index.module.css | 2 +- .../src/components/BasicViewer/index.tsx | 81 ++++- .../__snapshots__/index.spec.tsx.snap | 8 + .../DevfileEditor/__tests__/index.spec.tsx | 45 +++ .../__tests__/yamlSchemaLinter.spec.ts | 45 +++ .../components/DevfileEditor/index.module.css | 56 ++++ .../src/components/DevfileEditor/index.tsx | 266 ++++++++++++++++ .../DevfileEditor/yamlSchemaCompletion.ts | 290 ++++++++++++++++++ .../DevfileEditor/yamlSchemaLinter.ts | 196 ++++++++++++ .../DevfileEditorTools/index.module.css | 48 +++ .../components/DevfileEditorTools/index.tsx | 147 +++++++++ .../src/components/DevfileViewer/index.tsx | 155 ++++++---- .../components/EditorIcon/index.module.css | 2 +- .../components/EditorTools/index.module.css | 1 - 15 files changed, 1263 insertions(+), 96 deletions(-) create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/__tests__/__snapshots__/index.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/__tests__/index.spec.tsx create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/__tests__/yamlSchemaLinter.spec.ts create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/index.module.css create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/index.tsx create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaCompletion.ts create mode 100644 packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaLinter.ts create mode 100644 packages/dashboard-frontend/src/components/DevfileEditorTools/index.module.css create mode 100644 packages/dashboard-frontend/src/components/DevfileEditorTools/index.tsx diff --git a/packages/dashboard-frontend/src/components/BasicViewer/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/BasicViewer/__tests__/index.spec.tsx index 0d10638322..be630f8524 100644 --- a/packages/dashboard-frontend/src/components/BasicViewer/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/BasicViewer/__tests__/index.spec.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { BasicViewer } from '@/components/BasicViewer'; -import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); @@ -23,18 +23,11 @@ describe('BasicViewer', () => { expect(snapshot.toJSON()).toMatchSnapshot(); }); - test('handle content change', () => { - const { reRenderComponent } = renderComponent('line 1\nline 2\nline 3'); + test('renders container with id', () => { + renderComponent('line 1\nline 2\nline 3'); - const textbox = screen.getByRole('textbox'); - - // no new line character - expect(textbox).toHaveTextContent('line 1 line 2 line 3'); - - reRenderComponent('line 4\nline 5\nline 6'); - - // no new line character - expect(textbox).toHaveTextContent('line 4 line 5 line 6'); + const container = document.getElementById('basic-viewer-id'); + expect(container).toBeTruthy(); }); }); diff --git a/packages/dashboard-frontend/src/components/BasicViewer/index.module.css b/packages/dashboard-frontend/src/components/BasicViewer/index.module.css index fc96dd2b86..a653808e38 100644 --- a/packages/dashboard-frontend/src/components/BasicViewer/index.module.css +++ b/packages/dashboard-frontend/src/components/BasicViewer/index.module.css @@ -16,7 +16,7 @@ height: 100%; } -.basicViewer > div { +.basicViewer > * { overflow: auto; height: 100%; border: 1px solid var(--pf-t--global--border--color--default); diff --git a/packages/dashboard-frontend/src/components/BasicViewer/index.tsx b/packages/dashboard-frontend/src/components/BasicViewer/index.tsx index 6452243d49..3cd9f54fd5 100644 --- a/packages/dashboard-frontend/src/components/BasicViewer/index.tsx +++ b/packages/dashboard-frontend/src/components/BasicViewer/index.tsx @@ -10,9 +10,13 @@ * Red Hat, Inc. - initial API and implementation */ -import { githubDark, githubLight } from '@uiw/codemirror-theme-github'; -import CodeMirror from '@uiw/react-codemirror'; -import React from 'react'; +// Generated by Claude Opus 4.6 + +import { Compartment, EditorState } from '@codemirror/state'; +import { oneDark } from '@codemirror/theme-one-dark'; +import { EditorView } from '@codemirror/view'; +import { basicSetup } from 'codemirror'; +import React, { useCallback, useEffect, useRef } from 'react'; import styles from '@/components/BasicViewer/index.module.css'; import { useTheme } from '@/contexts/ThemeContext'; @@ -22,18 +26,69 @@ export type Props = { id: string; }; +const themeCompartment = new Compartment(); + +const lightTheme = EditorView.theme({ + '&': { fontSize: '13px' }, + '.cm-gutters': { display: 'none' }, + '.cm-activeLine': { backgroundColor: 'transparent' }, +}); + export const BasicViewer: React.FC = ({ value, id }) => { const { isDarkTheme } = useTheme(); + const viewRef = useRef(null); + const containerRef = useRef(null); + + useEffect(() => { + if (viewRef.current) { + viewRef.current.dispatch({ + effects: themeCompartment.reconfigure(isDarkTheme ? oneDark : lightTheme), + }); + } + }, [isDarkTheme]); + + useEffect(() => { + if (viewRef.current) { + const currentContent = viewRef.current.state.doc.toString(); + if (value !== currentContent) { + viewRef.current.dispatch({ + changes: { from: 0, to: currentContent.length, insert: value }, + }); + } + } + }, [value]); - return ( -
- -
+ const containerRefCallback = useCallback( + (node: HTMLDivElement | null) => { + if (containerRef.current === node) { + return; + } + containerRef.current = node; + + if (viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } + + if (!node) { + return; + } + + const state = EditorState.create({ + doc: value, + extensions: [ + basicSetup, + themeCompartment.of(isDarkTheme ? oneDark : lightTheme), + EditorState.readOnly.of(true), + EditorView.lineWrapping, + ], + }); + + viewRef.current = new EditorView({ state, parent: node }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], ); + + return
; }; diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..52ee1426ba --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`DevfileEditor snapshot 1`] = ` +
+`; diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/index.spec.tsx new file mode 100644 index 0000000000..dcda366c45 --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/index.spec.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; + +import { DevfileEditor } from '@/components/DevfileEditor'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +describe('DevfileEditor', () => { + test('snapshot', () => { + const snapshot = createSnapshot(); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('renders container with id', () => { + renderComponent(); + + const container = document.getElementById('test-editor'); + expect(container).toBeTruthy(); + }); + + test('renders with the devfileEditor class', () => { + renderComponent(); + + const container = document.getElementById('test-editor'); + expect(container?.className).toContain('devfileEditor'); + }); +}); + +function getComponent(): React.ReactElement { + return ( + + ); +} diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/yamlSchemaLinter.spec.ts b/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/yamlSchemaLinter.spec.ts new file mode 100644 index 0000000000..3678ebb94f --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/__tests__/yamlSchemaLinter.spec.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { EditorState } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; + +import { createDevfileSchemaLinter } from '@/components/DevfileEditor/yamlSchemaLinter'; + +describe('createDevfileSchemaLinter', () => { + test('creates an extension without schema', () => { + const linter = createDevfileSchemaLinter(undefined); + expect(linter).toBeDefined(); + }); + + test('can be added to editor state without errors', () => { + const linter = createDevfileSchemaLinter(undefined); + const state = EditorState.create({ + doc: 'schemaVersion: 2.2.2\n', + extensions: [linter], + }); + expect(state).toBeDefined(); + expect(state.doc.toString()).toBe('schemaVersion: 2.2.2\n'); + }); + + test('can be used with EditorView', () => { + const container = document.createElement('div'); + const linter = createDevfileSchemaLinter(undefined); + const state = EditorState.create({ + doc: 'schemaVersion: 2.2.2\n', + extensions: [linter], + }); + const view = new EditorView({ state, parent: container }); + expect(view).toBeDefined(); + view.destroy(); + }); +}); diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/index.module.css b/packages/dashboard-frontend/src/components/DevfileEditor/index.module.css new file mode 100644 index 0000000000..2eaa193f3c --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/index.module.css @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +.devfileEditor { + overflow: auto; + + width: 100%; + height: 100%; + min-height: 50vh; + + border: 1px solid var(--pf-t--global--border--color--default); +} + +.devfileEditor > * { + height: inherit; +} + +.error { + overflow: hidden; + + padding: 4px 8px; + + font-size: 13px; + line-height: 1.4; + color: var(--pf-t--global--color--status--danger--default); + text-overflow: ellipsis; + white-space: nowrap; +} + +.errorMessage :global(.pf-v6-c-helper-text__item.pf-m-error) { + overflow: hidden; + display: inline-block; + + color: var(--pf-v6-c-helper-text__item-icon--m-error--Color); + text-overflow: ellipsis; + white-space: nowrap; +} + +.docsLink { + float: right; + display: block; + + padding: 4px 8px; + + font-size: 13px; + color: var(--pf-t--global--text--color--link--default); +} diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/index.tsx b/packages/dashboard-frontend/src/components/DevfileEditor/index.tsx new file mode 100644 index 0000000000..6bd2f7cd33 --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/index.tsx @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { indentWithTab } from '@codemirror/commands'; +import { yaml } from '@codemirror/lang-yaml'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { diagnosticCount, forEachDiagnostic } from '@codemirror/lint'; +import { Compartment, EditorState, Extension } from '@codemirror/state'; +import { oneDark } from '@codemirror/theme-one-dark'; +import { EditorView, keymap } from '@codemirror/view'; +import { tags as t } from '@lezer/highlight'; +import { FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { basicSetup } from 'codemirror'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import styles from '@/components/DevfileEditor/index.module.css'; +import { createDevfileSchemaCompletion } from '@/components/DevfileEditor/yamlSchemaCompletion'; +import { createDevfileSchemaLinter } from '@/components/DevfileEditor/yamlSchemaLinter'; +import { useTheme } from '@/contexts/ThemeContext'; +import { DevfileSchema } from '@/services/backend-client/devfileSchemaApi'; + +export type Props = { + value: string; + id: string; + readOnly?: boolean; + schema?: DevfileSchema; + onChange?: (value: string) => void; + onValidation?: (errorMessage: string) => void; + onFocusChange?: (focused: boolean) => void; +}; + +const cheLightTheme = EditorView.theme( + { + '&': { + color: '#2e3440', + backgroundColor: '#fff', + fontSize: '13px', + }, + '.cm-activeLine': { + backgroundColor: 'inherit', + }, + '.cm-gutters': { + backgroundColor: '#f7f7f7', + color: '#999', + border: 'none', + }, + '.cm-activeLineGutter': { + backgroundColor: '#f7f7f7', + }, + '.cm-tooltip': { + backgroundColor: '#ffffff', + border: '1px solid var(--pf-t--global--border--color--default, #ddd)', + borderRadius: '4px', + boxShadow: '0 2px 8px rgba(0,0,0,0.15)', + }, + '.cm-diagnostic-error': { + borderLeft: '3px solid var(--pf-t--global--color--status--danger--default, #dc2626)', + }, + '.cm-diagnostic-warning': { + borderLeft: '3px solid var(--pf-t--global--color--status--warning--default, #f59e0b)', + }, + }, + { dark: false }, +); + +const cheLightHighlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: '#5e81ac' }, + { tag: [t.string], color: '#5e81ac' }, + { tag: [t.variableName], color: '#008080' }, + { + tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], + color: '#008080', + }, +]); + +const cheLight = [cheLightTheme, syntaxHighlighting(cheLightHighlightStyle)]; + +interface EditorCallbackRefs { + onValidation: React.MutableRefObject<((errorMessage: string) => void) | undefined>; + onChange: React.MutableRefObject<((value: string) => void) | undefined>; + onFocusChange: React.MutableRefObject<((focused: boolean) => void) | undefined>; + setError: React.MutableRefObject<((msg: string) => void) | undefined>; + isProgrammaticChange: React.MutableRefObject; +} + +function diagnosticsReporter(refs: EditorCallbackRefs) { + return EditorView.updateListener.of(update => { + if (!update.transactions.some(tr => tr.effects.length > 0) && !update.docChanged) { + return; + } + + const count = diagnosticCount(update.state); + if (count > 0) { + let firstError = ''; + forEachDiagnostic(update.state, diag => { + if (firstError) { + return; + } + if (diag.severity === 'error') { + const line = update.state.doc.lineAt(diag.from); + const col = diag.from - line.from + 1; + firstError = `Error on line ${line.number}, column ${col}: ${diag.message}`; + } + }); + refs.onValidation.current?.(firstError || ''); + refs.setError.current?.(firstError || ''); + } else { + refs.onValidation.current?.(''); + refs.setError.current?.(''); + } + }); +} + +function changeListener(refs: EditorCallbackRefs) { + return EditorView.updateListener.of(update => { + if (update.docChanged && refs.onChange.current && !refs.isProgrammaticChange.current) { + refs.onChange.current(update.state.doc.toString()); + } + }); +} + +function focusChangeListener(refs: EditorCallbackRefs) { + return EditorView.updateListener.of(update => { + if (update.focusChanged && refs.onFocusChange.current) { + refs.onFocusChange.current(update.view.hasFocus); + } + }); +} + +export const DevfileEditor: React.FC = ({ + value, + id, + readOnly = false, + schema, + onChange, + onValidation, + onFocusChange, +}) => { + const { isDarkTheme } = useTheme(); + const containerRef = useRef(null); + const viewRef = useRef(null); + const prevValueRef = useRef(value); + const [errorMessage, setErrorMessage] = useState(''); + + const themeCompartmentRef = useRef(new Compartment()); + const readOnlyCompartmentRef = useRef(new Compartment()); + + const callbackRefs = useRef({ + onValidation: { current: undefined }, + onChange: { current: undefined }, + onFocusChange: { current: undefined }, + setError: { current: undefined }, + isProgrammaticChange: { current: false }, + }); + + callbackRefs.current.onValidation.current = readOnly ? undefined : onValidation; + callbackRefs.current.onChange.current = onChange; + callbackRefs.current.onFocusChange.current = onFocusChange; + callbackRefs.current.setError.current = readOnly ? undefined : setErrorMessage; + + useEffect(() => { + if (viewRef.current) { + viewRef.current.dispatch({ + effects: themeCompartmentRef.current.reconfigure(isDarkTheme ? oneDark : cheLight), + }); + } + }, [isDarkTheme]); + + useEffect(() => { + if (viewRef.current) { + viewRef.current.dispatch({ + effects: readOnlyCompartmentRef.current.reconfigure(EditorState.readOnly.of(readOnly)), + }); + } + }, [readOnly]); + + useEffect(() => { + if (viewRef.current && value !== prevValueRef.current) { + const currentContent = viewRef.current.state.doc.toString(); + if (value !== currentContent) { + callbackRefs.current.isProgrammaticChange.current = true; + viewRef.current.dispatch({ + changes: { from: 0, to: currentContent.length, insert: value }, + }); + callbackRefs.current.isProgrammaticChange.current = false; + } + prevValueRef.current = value; + } + }, [value]); + + const containerRefCallback = useCallback( + (node: HTMLDivElement | null) => { + if ((containerRef as React.MutableRefObject).current === node) { + return; + } + (containerRef as React.MutableRefObject).current = node; + + if (viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } + + if (!node) { + return; + } + + const extensions: Extension[] = [ + basicSetup, + yaml(), + keymap.of([indentWithTab]), + themeCompartmentRef.current.of(isDarkTheme ? oneDark : cheLight), + readOnlyCompartmentRef.current.of(EditorState.readOnly.of(readOnly)), + changeListener(callbackRefs.current), + focusChangeListener(callbackRefs.current), + EditorView.lineWrapping, + ]; + + if (!readOnly) { + extensions.push(createDevfileSchemaLinter(schema)); + extensions.push(createDevfileSchemaCompletion(schema)); + extensions.push(diagnosticsReporter(callbackRefs.current)); + } + + const state = EditorState.create({ + doc: value, + extensions, + }); + + const view = new EditorView({ + state, + parent: node, + }); + viewRef.current = view; + prevValueRef.current = value; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [schema], + ); + + return ( + +
+ {!readOnly && errorMessage && ( + + + } variant="error"> + {errorMessage} + + + + )} + + ); +}; diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaCompletion.ts b/packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaCompletion.ts new file mode 100644 index 0000000000..26f78db2cc --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaCompletion.ts @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; +import { autocompletion } from '@codemirror/autocomplete'; +import type { Extension } from '@codemirror/state'; +import type { Scalar as YamlScalar, YAMLMap } from 'yaml'; +import type { Node as YamlNode } from 'yaml'; +import { isMap, isSeq, parseDocument } from 'yaml'; + +import { DevfileSchema } from '@/services/backend-client/devfileSchemaApi'; + +interface JsonSchemaProperty { + description?: string; + type?: string; + enum?: string[]; + properties?: Record; + items?: JsonSchemaProperty; + $ref?: string; + oneOf?: JsonSchemaProperty[]; + anyOf?: JsonSchemaProperty[]; + required?: string[]; + additionalProperties?: boolean | JsonSchemaProperty; + [key: string]: unknown; +} + +interface JsonSchema extends JsonSchemaProperty { + definitions?: Record; +} + +function resolveRef(schema: JsonSchema, ref: string): JsonSchemaProperty | undefined { + if (!ref.startsWith('#/')) { + return undefined; + } + const path = ref.substring(2).split('/'); + let current: JsonSchemaProperty = schema; + for (const segment of path) { + if (current && typeof current === 'object' && segment in current) { + current = current[segment] as JsonSchemaProperty; + } else { + return undefined; + } + } + return current; +} + +function resolveSchema(root: JsonSchema, s: JsonSchemaProperty): JsonSchemaProperty { + if (s.$ref) { + const resolved = resolveRef(root, s.$ref); + return resolved ? resolveSchema(root, resolved) : s; + } + return s; +} + +function getSchemaAtPath(root: JsonSchema, path: string[]): JsonSchemaProperty | undefined { + let current: JsonSchemaProperty = root; + + for (const segment of path) { + current = resolveSchema(root, current); + + if (current.properties && segment in current.properties) { + current = current.properties[segment]; + } else if (current.items) { + const resolved = resolveSchema(root, current.items); + if (resolved.properties && segment in resolved.properties) { + current = resolved.properties[segment]; + } else if (resolved.oneOf) { + let found = false; + for (const option of resolved.oneOf) { + const resolvedOption = resolveSchema(root, option); + if (resolvedOption.properties && segment in resolvedOption.properties) { + current = resolvedOption.properties[segment]; + found = true; + break; + } + } + if (!found) { + return undefined; + } + } else { + return undefined; + } + } else if (current.oneOf) { + let found = false; + for (const option of current.oneOf) { + const resolvedOption = resolveSchema(root, option); + if (resolvedOption.properties && segment in resolvedOption.properties) { + current = resolvedOption.properties[segment]; + found = true; + break; + } + } + if (!found) { + return undefined; + } + } else { + return undefined; + } + } + + return resolveSchema(root, current); +} + +function getCompletionProperties(root: JsonSchema, s: JsonSchemaProperty): Completion[] { + const resolved = resolveSchema(root, s); + const completions: Completion[] = []; + const seen = new Set(); + + const collect = (sch: JsonSchemaProperty) => { + const r = resolveSchema(root, sch); + if (r.properties) { + for (const [key, value] of Object.entries(r.properties)) { + if (seen.has(key)) { + continue; + } + seen.add(key); + const resolvedValue = resolveSchema(root, value); + completions.push({ + label: key, + type: 'property', + detail: resolvedValue.type || '', + info: resolvedValue.description || undefined, + boost: r.required?.includes(key) ? 1 : 0, + }); + } + } + if (r.oneOf) { + for (const option of r.oneOf) { + collect(option); + } + } + if (r.items) { + collect(r.items); + } + }; + + collect(resolved); + return completions; +} + +function getValueCompletions(root: JsonSchema, s: JsonSchemaProperty): Completion[] { + const resolved = resolveSchema(root, s); + if (resolved.enum) { + return resolved.enum.map(v => ({ + label: String(v), + type: 'value', + })); + } + if (resolved.type === 'boolean') { + return [ + { label: 'true', type: 'value' }, + { label: 'false', type: 'value' }, + ]; + } + return []; +} + +function getYamlPath(text: string, offset: number): { path: string[]; isValue: boolean } { + try { + const doc = parseDocument(text, { keepSourceTokens: true }); + if (!doc.contents) { + return { path: [], isValue: false }; + } + + const path: string[] = []; + let isValue = false; + + const findPath = (node: YamlNode | undefined, targetOffset: number): boolean => { + if (!node) { + return false; + } + + if (isMap(node)) { + for (const pair of (node as YAMLMap).items) { + const key = pair.key as YamlScalar; + const value = pair.value as YamlNode | null; + + if (key && key.range) { + const keyStart = key.range[0]; + const valueEnd = value?.range ? value.range[1] : key.range[1]; + + if (targetOffset >= keyStart && targetOffset <= valueEnd + 1) { + path.push(String(key.value)); + if (value && targetOffset > key.range[1]) { + isValue = true; + if (isMap(value) || isSeq(value)) { + isValue = false; + return findPath(value, targetOffset); + } + return true; + } + return true; + } + } + } + return false; + } + + if (isSeq(node)) { + for (let i = 0; i < node.items.length; i++) { + const item = node.items[i] as YamlNode; + if (item?.range && targetOffset >= item.range[0] && targetOffset <= item.range[1] + 1) { + isValue = false; + return findPath(item, targetOffset); + } + } + return false; + } + + return false; + }; + + findPath(doc.contents as YamlNode, offset); + return { path, isValue }; + } catch { + return { path: [], isValue: false }; + } +} + +function createCompletionSource(schema: JsonSchema) { + return function devfileCompletionSource(context: CompletionContext): CompletionResult | null { + const line = context.state.doc.lineAt(context.pos); + const lineText = line.text; + const lineOffset = context.pos - line.from; + const beforeCursor = lineText.substring(0, lineOffset); + + const keyMatch = beforeCursor.match(/^(\s*(?:-\s*)?)(\w*)$/); + const valueMatch = beforeCursor.match(/:\s*(\w*)$/); + + const text = context.state.doc.toString(); + const { path: yamlPath, isValue } = getYamlPath(text, context.pos); + + if (valueMatch && isValue && yamlPath.length > 0) { + const fieldSchema = getSchemaAtPath(schema, yamlPath); + if (fieldSchema) { + const completions = getValueCompletions(schema, fieldSchema); + if (completions.length > 0) { + return { + from: context.pos - valueMatch[1].length, + options: completions, + validFor: /^\w*$/, + }; + } + } + } + + if (keyMatch) { + const word = keyMatch[2]; + const parentPath = isValue ? yamlPath : yamlPath.slice(0, -1); + const parentSchema = parentPath.length === 0 ? schema : getSchemaAtPath(schema, parentPath); + if (!parentSchema) { + return null; + } + + const completions = getCompletionProperties(schema, parentSchema); + if (completions.length === 0) { + return null; + } + + return { + from: context.pos - word.length, + options: completions, + validFor: /^\w*$/, + }; + } + + return null; + }; +} + +export function createDevfileSchemaCompletion(schema: DevfileSchema | undefined): Extension { + if (!schema) { + return autocompletion({ override: [], activateOnTyping: false }); + } + return autocompletion({ + override: [createCompletionSource(schema as JsonSchema)], + activateOnTyping: true, + }); +} diff --git a/packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaLinter.ts b/packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaLinter.ts new file mode 100644 index 0000000000..9948931c15 --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditor/yamlSchemaLinter.ts @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import type { Diagnostic } from '@codemirror/lint'; +import { linter } from '@codemirror/lint'; +import type { Extension } from '@codemirror/state'; +import type { EditorView } from '@codemirror/view'; +import Ajv, { ValidateFunction } from 'ajv'; +import { + type Document as YamlDocument, + isMap, + isPair, + isScalar, + isSeq, + type Node as YamlNode, + type Pair as YamlPair, + parseDocument, + type Scalar as YamlScalar, + type YAMLMap, +} from 'yaml'; + +import { DevfileSchema } from '@/services/backend-client/devfileSchemaApi'; + +const ajv = new Ajv({ + allErrors: true, + strict: false, + validateFormats: false, + unicodeRegExp: false, +}); + +const validateCache = new WeakMap(); + +function getValidateFunction(schema: DevfileSchema): ValidateFunction { + let validate = validateCache.get(schema); + if (!validate) { + validate = ajv.compile(schema as Record); + validateCache.set(schema, validate); + } + return validate; +} + +function findNodeByPath(doc: YamlDocument, jsonPath: string): YamlNode | YamlPair | undefined { + const segments = jsonPath + .split('/') + .filter(s => s.length > 0) + .map(s => (s.match(/^\d+$/) ? parseInt(s, 10) : s)); + + let current: YamlNode | undefined = doc.contents as YamlNode | undefined; + + for (const segment of segments) { + if (!current) { + return undefined; + } + + if (typeof segment === 'number' && isSeq(current)) { + current = current.items[segment] as YamlNode | undefined; + } else if (typeof segment === 'string' && isMap(current)) { + const pair = (current as YAMLMap).items.find( + (p: YamlPair) => isScalar(p.key) && (p.key as YamlScalar).value === segment, + ); + if (pair) { + return pair; + } + return undefined; + } else { + return undefined; + } + } + + return current; +} + +function getNodeRange( + node: YamlNode | YamlPair | undefined, + docLength: number, +): { from: number; to: number } { + if (!node) { + return { from: 0, to: Math.min(1, docLength) }; + } + + if (isPair(node)) { + const key = node.key as YamlNode; + if (key && key.range) { + return { from: key.range[0], to: key.range[1] }; + } + } + + if ('range' in node && Array.isArray(node.range)) { + return { from: node.range[0], to: node.range[1] }; + } + + return { from: 0, to: Math.min(1, docLength) }; +} + +function createLintSource(schema: DevfileSchema | undefined) { + return function yamlSchemaLintSource(view: EditorView): Diagnostic[] { + const text = view.state.doc.toString(); + if (text.trim().length === 0) { + return []; + } + + const diagnostics: Diagnostic[] = []; + const doc = parseDocument(text, { keepSourceTokens: true }); + + for (const error of doc.errors) { + const pos = error.pos ? error.pos[0] : 0; + const endPos = error.pos ? error.pos[1] : Math.min(pos + 1, text.length); + diagnostics.push({ + from: Math.min(pos, text.length), + to: Math.min(endPos, text.length), + severity: 'error', + message: error.message, + source: 'yaml', + }); + } + + for (const warning of doc.warnings) { + const pos = warning.pos ? warning.pos[0] : 0; + const endPos = warning.pos ? warning.pos[1] : Math.min(pos + 1, text.length); + diagnostics.push({ + from: Math.min(pos, text.length), + to: Math.min(endPos, text.length), + severity: 'warning', + message: warning.message, + source: 'yaml', + }); + } + + if (doc.errors.length > 0) { + return diagnostics; + } + + if (!schema) { + return diagnostics; + } + + const validate = getValidateFunction(schema); + const jsonValue: unknown = doc.toJSON(); + if (jsonValue === null || jsonValue === undefined || typeof jsonValue !== 'object') { + return diagnostics; + } + + const valid = validate(jsonValue); + if (!valid && validate.errors) { + for (const error of validate.errors) { + const instancePath = error.instancePath || ''; + const node = findNodeByPath(doc, instancePath); + const range = getNodeRange(node, text.length); + + let message = error.message || 'Schema validation error'; + if (error.keyword === 'additionalProperties') { + const params = error.params as { additionalProperty?: string }; + message = `Unknown property '${params.additionalProperty || ''}'`; + } else if (error.keyword === 'required') { + const params = error.params as { missingProperty?: string }; + message = `Missing required property '${params.missingProperty || ''}'`; + } else if (error.keyword === 'enum') { + const params = error.params as { allowedValues?: unknown[] }; + message = `Value must be one of: ${(params.allowedValues || []).join(', ')}`; + } else if (error.keyword === 'type') { + const params = error.params as { type?: string }; + message = `Expected type '${params.type || ''}'`; + } + + if (instancePath) { + message = `${instancePath}: ${message}`; + } + + diagnostics.push({ + from: range.from, + to: range.to, + severity: error.keyword === 'oneOf' ? 'info' : 'error', + message, + source: 'devfile-schema', + }); + } + } + + return diagnostics; + }; +} + +export function createDevfileSchemaLinter(schema: DevfileSchema | undefined): Extension { + return linter(createLintSource(schema), { delay: 500 }); +} diff --git a/packages/dashboard-frontend/src/components/DevfileEditorTools/index.module.css b/packages/dashboard-frontend/src/components/DevfileEditorTools/index.module.css new file mode 100644 index 0000000000..1e8fb9fbaa --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditorTools/index.module.css @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +.editorTools { + height: 33px; +} + +.editorTools > * { + user-select: none; + + float: right; + + height: inherit; + margin: 0; + + font-size: small; +} + +.editorTools > * > div { + margin: inherit; +} + +.editorTools > * > hr { + margin: 0 5px; +} + +.editorTools .button { + padding: 6px; + font-size: inherit; + outline: none; +} + +.editorTools .button a { + text-decoration: none; +} + +.editorTools .button svg { + margin-right: 8px; +} diff --git a/packages/dashboard-frontend/src/components/DevfileEditorTools/index.tsx b/packages/dashboard-frontend/src/components/DevfileEditorTools/index.tsx new file mode 100644 index 0000000000..32f28e8ac9 --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileEditorTools/index.tsx @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { helpers } from '@eclipse-che/common'; +import { AlertVariant, Button, Divider, Flex, FlexItem } from '@patternfly/react-core'; +import { CompressIcon, CopyIcon, DownloadIcon, ExpandIcon } from '@patternfly/react-icons'; +import React from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +import styles from '@/components/DevfileEditorTools/index.module.css'; +import { lazyInject } from '@/inversify.config'; +import { AppAlerts } from '@/services/alerts/appAlerts'; +import { AlertItem } from '@/services/helpers/types'; + +export interface Props { + contentText: string; + workspaceName: string; + isExpanded: boolean; + readOnly?: boolean; + onToggleExpand: () => void; +} + +interface State { + copied?: boolean; + contentText: string; + contentBlobUrl: string; +} + +export default class DevfileEditorTools extends React.PureComponent { + private copiedTimer: number | undefined; + + @lazyInject(AppAlerts) + private readonly appAlerts: AppAlerts; + + constructor(props: Props) { + super(props); + + this.state = { + contentText: '', + contentBlobUrl: '', + }; + } + + public componentDidMount(): void { + this.init(); + } + + public componentDidUpdate(): void { + this.init(); + } + + private init() { + const { contentText } = this.props; + try { + if (contentText !== this.state.contentText) { + const contentBlobUrl = URL.createObjectURL( + new Blob([contentText], { type: 'application/x-yaml' }), + ); + this.setState({ contentText, contentBlobUrl }); + } + } catch (e) { + this.showAlert({ + key: 'devfile-editor-tools-create-blob-url-fails', + variant: AlertVariant.danger, + title: helpers.errors.getMessage(e), + }); + } + } + + private showAlert(alert: AlertItem): void { + this.appAlerts.showAlert(alert); + } + + private onCopyToClipboard(): void { + this.setState({ copied: true }); + if (this.copiedTimer) { + clearTimeout(this.copiedTimer); + } + this.copiedTimer = window.setTimeout(() => { + this.setState({ copied: false }); + }, 3000); + } + + public render(): React.ReactElement { + const { workspaceName, isExpanded, readOnly, onToggleExpand } = this.props; + const { contentText, contentBlobUrl, copied } = this.state; + + return ( +
+ + {!readOnly && ( + + + this.onCopyToClipboard()}> + + + + + + + + + + )} + + + + +
+ ); + } +} diff --git a/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx b/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx index 52f2cc16e1..fd12b19de3 100644 --- a/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx +++ b/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx @@ -12,11 +12,12 @@ import { yaml } from '@codemirror/lang-yaml'; import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { Compartment, EditorState } from '@codemirror/state'; +import { oneDark } from '@codemirror/theme-one-dark'; import { EditorView } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; -import { githubDark } from '@uiw/codemirror-theme-github'; -import CodeMirror from '@uiw/react-codemirror'; -import React, { useMemo } from 'react'; +import { basicSetup } from 'codemirror'; +import React, { useCallback, useEffect, useRef } from 'react'; import styles from '@/components/DevfileViewer/index.module.css'; import { useTheme } from '@/contexts/ThemeContext'; @@ -28,82 +29,100 @@ export type Props = { id: string; }; -const createLightTheme = () => { - return EditorView.theme( - { - '&': { - color: '#2e3440', - backgroundColor: '#fff', - }, - '.cm-activeLine': { - backgroundColor: 'inherit', - }, - '.cm-gutters': { - backgroundColor: '#f7f7f7', - color: '#636363', - }, - '.cm-activeLineGutter': { - backgroundColor: '#f7f7f7', - }, - }, - { dark: false }, - ); -}; +const themeCompartment = new Compartment(); -const createDarkGutterTheme = () => { - return EditorView.theme( - { - '.cm-gutters': { - backgroundColor: '#161b22', - color: '#8b949e', - }, - '.cm-activeLineGutter': { - backgroundColor: '#161b22', - }, +const lightTheme = EditorView.theme( + { + '&': { + color: '#2e3440', + backgroundColor: '#fff', }, - { dark: true }, - ); -}; - -const createLightHighlightStyle = () => { - return HighlightStyle.define([ - { tag: t.keyword, color: '#4c7399' }, - { tag: [t.string], color: '#4c7399' }, - { tag: [t.variableName], color: '#008080' }, - { - tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], - color: '#008080', + '.cm-activeLine': { + backgroundColor: 'inherit', }, - ]); -}; + '.cm-gutters': { + backgroundColor: '#f7f7f7', + color: '#999', + }, + '.cm-activeLineGutter': { + backgroundColor: '#f7f7f7', + }, + }, + { dark: false }, +); + +const lightHighlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: '#5e81ac' }, + { tag: [t.string], color: '#5e81ac' }, + { tag: [t.variableName], color: '#008080' }, + { + tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], + color: '#008080', + }, +]); + +const cheLight = [lightTheme, syntaxHighlighting(lightHighlightStyle)]; export const DevfileViewer: React.FC = ({ value, id }) => { const { isDarkTheme } = useTheme(); + const viewRef = useRef(null); + const containerRef = useRef(null); + + useEffect(() => { + if (viewRef.current) { + viewRef.current.dispatch({ + effects: themeCompartment.reconfigure(isDarkTheme ? oneDark : cheLight), + }); + } + }, [isDarkTheme]); + + useEffect(() => { + if (viewRef.current) { + const currentContent = viewRef.current.state.doc.toString(); + if (value !== currentContent) { + viewRef.current.dispatch({ + changes: { from: 0, to: currentContent.length, insert: value }, + }); + } + } + }, [value]); + + const containerRefCallback = useCallback( + (node: HTMLDivElement | null) => { + if (containerRef.current === node) { + return; + } + containerRef.current = node; - const lightTheme = useMemo(() => createLightTheme(), []); - const darkGutterTheme = useMemo(() => createDarkGutterTheme(), []); - const lightHighlightStyle = useMemo(() => createLightHighlightStyle(), []); + if (viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } - const extensions = useMemo(() => { - const baseExtensions = [ - EditorView.contentAttributes.of({ 'aria-label': 'Devfile content' }), - yaml(), - ]; - return isDarkTheme - ? [darkGutterTheme, ...baseExtensions] - : [lightTheme, syntaxHighlighting(lightHighlightStyle), ...baseExtensions]; - }, [isDarkTheme, darkGutterTheme, lightTheme, lightHighlightStyle]); + if (!node) { + return; + } + + const state = EditorState.create({ + doc: value, + extensions: [ + basicSetup, + yaml(), + themeCompartment.of(isDarkTheme ? oneDark : cheLight), + EditorState.readOnly.of(true), + EditorView.lineWrapping, + ], + }); + + viewRef.current = new EditorView({ state, parent: node }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); return (
- +
); }; diff --git a/packages/dashboard-frontend/src/components/EditorIcon/index.module.css b/packages/dashboard-frontend/src/components/EditorIcon/index.module.css index 7d0e06d141..d03ed8f62c 100644 --- a/packages/dashboard-frontend/src/components/EditorIcon/index.module.css +++ b/packages/dashboard-frontend/src/components/EditorIcon/index.module.css @@ -29,7 +29,7 @@ } .name { - white-space: nowrap; + white-space: break-spaces; } .placeholder { diff --git a/packages/dashboard-frontend/src/components/EditorTools/index.module.css b/packages/dashboard-frontend/src/components/EditorTools/index.module.css index a59dea00c2..1e8fb9fbaa 100644 --- a/packages/dashboard-frontend/src/components/EditorTools/index.module.css +++ b/packages/dashboard-frontend/src/components/EditorTools/index.module.css @@ -12,7 +12,6 @@ .editorTools { height: 33px; - margin: 5px 0; } .editorTools > * { From 45217b1f2b78227c0882144ec404ae60835fc1b5 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 24 Apr 2026 06:11:33 +0300 Subject: [PATCH 08/16] feat(terminal): add agent terminal and pod events widgets Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel --- .../src/components/AgentPodEvents/index.tsx | 115 ++++++++++++++ .../StatusConnecting/index.module.css | 18 +++ .../AgentTerminal/StatusConnecting/index.tsx | 23 +++ .../StatusError/index.module.css | 18 +++ .../AgentTerminal/StatusError/index.tsx | 23 +++ .../components/AgentTerminal/index.module.css | 32 ++++ .../src/components/AgentTerminal/index.tsx | 142 ++++++++++++++++++ .../src/components/AgentTerminal/themes.ts | 31 ++++ .../components/TerminalTools/index.module.css | 34 +++++ .../src/components/TerminalTools/index.tsx | 52 +++++++ 10 files changed, 488 insertions(+) create mode 100644 packages/dashboard-frontend/src/components/AgentPodEvents/index.tsx create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.module.css create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.tsx create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.module.css create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.tsx create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/index.module.css create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/index.tsx create mode 100644 packages/dashboard-frontend/src/components/AgentTerminal/themes.ts create mode 100644 packages/dashboard-frontend/src/components/TerminalTools/index.module.css create mode 100644 packages/dashboard-frontend/src/components/TerminalTools/index.tsx diff --git a/packages/dashboard-frontend/src/components/AgentPodEvents/index.tsx b/packages/dashboard-frontend/src/components/AgentPodEvents/index.tsx new file mode 100644 index 0000000000..9de8247604 --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentPodEvents/index.tsx @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import { CoreV1Event } from '@kubernetes/client-node'; +import { + Content, + ContentVariants, + EmptyState, + EmptyStateBody, + Flex, + FlexItem, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { FileIcon } from '@patternfly/react-icons'; +import React from 'react'; +import Pluralize from 'react-pluralize'; +import { connect, ConnectedProps } from 'react-redux'; + +import compareEventTime from '@/components/WorkspaceEvents/compareEventTime'; +import styles from '@/components/WorkspaceEvents/index.module.css'; +import { WorkspaceEventsItem } from '@/components/WorkspaceEvents/Item'; +import { RootState } from '@/store'; +import { selectAllEvents } from '@/store/Events/selectors'; + +export type Props = { + podName: string | undefined; + className?: string; +} & MappedProps; + +class AgentPodEvents extends React.PureComponent { + private getEventItems(events: CoreV1Event[]): React.ReactNode[] { + return events + .filter(event => event.message) + .sort(compareEventTime) + .map((event, index) => { + return ( + +
+ +
+
+ ); + }); + } + + render() { + const { podName, allEvents } = this.props; + + if (!podName) { + return ( + + Events will be streamed for a starting agent pod. + + ); + } + + const podEvents = allEvents.filter( + event => event.involvedObject.name === podName && event.involvedObject.kind === 'Pod', + ); + + const eventItems = this.getEventItems(podEvents); + + if (eventItems.length === 0) { + return ( + + Events will be streamed for the agent pod. + + ); + } + + return ( +
+ + + + + Streaming events... + + + + Showing + + + + + {eventItems} + + Older events are not stored. + + +
+ ); + } +} + +const mapStateToProps = (state: RootState) => ({ + allEvents: selectAllEvents(state), +}); + +const connector = connect(mapStateToProps); + +type MappedProps = ConnectedProps; +export default connector(AgentPodEvents); diff --git a/packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.module.css b/packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.module.css new file mode 100644 index 0000000000..37acaaece5 --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.module.css @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +.statusMessage { + padding: 8px 12px; + font-size: 12px; + color: var(--pf-t--global--color--status--warning--default); + background-color: var(--pf-t--global--background--color--primary--default); +} diff --git a/packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.tsx b/packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.tsx new file mode 100644 index 0000000000..972a5f40da --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentTerminal/StatusConnecting/index.tsx @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import React from 'react'; + +import styles from '@/components/AgentTerminal/StatusConnecting/index.module.css'; + +const StatusConnecting: React.FC = () => ( +
Connecting to agent...
+); + +export default StatusConnecting; diff --git a/packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.module.css b/packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.module.css new file mode 100644 index 0000000000..edeb08cce7 --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.module.css @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +.statusMessage { + padding: 8px 12px; + font-size: 12px; + color: var(--pf-t--global--color--status--danger--default); + background-color: var(--pf-t--global--background--color--primary--default); +} diff --git a/packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.tsx b/packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.tsx new file mode 100644 index 0000000000..d53ba34fc5 --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentTerminal/StatusError/index.tsx @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import React from 'react'; + +import styles from '@/components/AgentTerminal/StatusError/index.module.css'; + +const StatusError: React.FC = () => ( +
Failed to load terminal
+); + +export default StatusError; diff --git a/packages/dashboard-frontend/src/components/AgentTerminal/index.module.css b/packages/dashboard-frontend/src/components/AgentTerminal/index.module.css new file mode 100644 index 0000000000..ba485620b4 --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentTerminal/index.module.css @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +.terminalContainer :global(.xterm) { + margin: 0 5px; +} + +.wrapper { + display: flex; + flex-direction: column; + height: calc(100vh - 350px); + border: 1px solid var(--pf-t--global--border--color--default); +} + +.iframe { + flex: 1; + + width: 100%; + min-height: 0; + + background-color: var(--pf-t--global--background--color--primary--default); + border: none; +} diff --git a/packages/dashboard-frontend/src/components/AgentTerminal/index.tsx b/packages/dashboard-frontend/src/components/AgentTerminal/index.tsx new file mode 100644 index 0000000000..8a2c8e192c --- /dev/null +++ b/packages/dashboard-frontend/src/components/AgentTerminal/index.tsx @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2018-2025 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +// Generated by Claude Opus 4.6 + +import React from 'react'; + +import styles from '@/components/AgentTerminal/index.module.css'; +import StatusConnecting from '@/components/AgentTerminal/StatusConnecting'; +import StatusError from '@/components/AgentTerminal/StatusError'; +import { AGENT_TERMINAL_THEMES } from '@/components/AgentTerminal/themes'; + +const READY_TIMEOUT_MS = 30_000; + +export interface Props { + terminalUrl: string; + namespace: string; + devfileName: string; + devfileId?: string; + isDarkTheme: boolean; + initCommand?: string; +} + +interface State { + connectionStatus: 'connecting' | 'connected' | 'error'; +} + +export default class AgentTerminal extends React.PureComponent { + private iframeRef = React.createRef(); + private readyTimer: ReturnType | undefined; + private messageHandler: ((event: MessageEvent) => void) | undefined; + private readonly initialSrc: string; + + constructor(props: Props) { + super(props); + this.state = { + connectionStatus: 'connecting', + }; + this.initialSrc = props.terminalUrl; + } + + componentDidMount() { + this.messageHandler = (event: MessageEvent) => { + if (event.data?.type === 'ttyd-ready') { + this.onReady(); + } + }; + window.addEventListener('message', this.messageHandler); + + this.readyTimer = setTimeout(() => { + if (this.state.connectionStatus === 'connecting') { + this.setState({ connectionStatus: 'error' }); + } + }, READY_TIMEOUT_MS); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.isDarkTheme !== this.props.isDarkTheme) { + this.sendThemeUpdate(); + } + } + + componentWillUnmount() { + if (this.readyTimer) { + clearTimeout(this.readyTimer); + } + if (this.messageHandler) { + window.removeEventListener('message', this.messageHandler); + } + } + + private onReady() { + if (this.state.connectionStatus === 'connected') return; + if (this.readyTimer) { + clearTimeout(this.readyTimer); + } + this.setState({ connectionStatus: 'connected' }); + this.sendInitCommand(); + } + + private postToIframe(message: Record) { + const iframe = this.iframeRef.current; + if (!iframe?.contentWindow) return; + iframe.contentWindow.postMessage(message, window.location.origin); + } + + private sendThemeUpdate() { + const theme = this.props.isDarkTheme ? AGENT_TERMINAL_THEMES.dark : AGENT_TERMINAL_THEMES.light; + this.postToIframe({ type: 'terminal-theme', theme }); + } + + private buildInitCommand(): string { + const { namespace, devfileName, devfileId, initCommand } = this.props; + if (!initCommand) { + return ''; + } + let context = + `'You are working in namespace "${namespace}" on a devfile named "${devfileName}". ` + + `It is stored in a Kubernetes ConfigMap named "devfile-creator-storage" in namespace "${namespace}".`; + if (devfileId) { + context += ` The entry key is "${devfileId}".`; + } + context += `'`; + return `${initCommand} ${context}\r`; + } + + private sendInitCommand() { + const command = this.buildInitCommand(); + this.postToIframe({ type: 'terminal-input', data: command }); + } + + private handleError = () => { + this.setState({ connectionStatus: 'error' }); + }; + + render(): React.ReactElement { + const { connectionStatus } = this.state; + + return ( +
+ {connectionStatus === 'connecting' && } + {connectionStatus === 'error' && } +