From f361c31dbf5378c7250bc6591a4bed9e6aa8e86f Mon Sep 17 00:00:00 2001 From: Mario Loriedo Date: Mon, 31 Dec 2018 13:13:36 +0000 Subject: [PATCH] Support creating workspaces with a workspace config JSON (#34) * Updates to support workspaceconfig.json * Fix issue with URL regex * Fix trailing whitespace * Fixup after Artem review --- README.md | 7 +- src/commands/workspace/start.ts | 11 +- src/helpers/che.ts | 61 +++++++--- test/helpers/che.test.ts | 37 +++--- ...-workspace-from-valid-workspaceconfig.json | 109 ++++++++++++++++++ test/helpers/requests/workspaceconfig.valid | 96 +++++++++++++++ 6 files changed, 282 insertions(+), 39 deletions(-) create mode 100644 test/helpers/replies/create-workspace-from-valid-workspaceconfig.json create mode 100644 test/helpers/requests/workspaceconfig.valid diff --git a/README.md b/README.md index 93932cc0c..c4e193750 100644 --- a/README.md +++ b/README.md @@ -183,9 +183,10 @@ USAGE $ chectl workspace:start OPTIONS - -f, --devfile=devfile (required) path to a valid devfile - -h, --help show CLI help - -n, --chenamespace=chenamespace [default: kube-che] kubernetes namespace where Che server is deployed + -f, --devfile=devfile path to a valid devfile + -h, --help show CLI help + -n, --chenamespace=chenamespace [default: kube-che] kubernetes namespace where Che server is deployed + -w, --workspaceconfig=workspaceconfig path to a valid workspace configuration json file ``` _See code: [src/commands/workspace/start.ts](https://github.com/che-incubator/chectl/blob/v0.0.2/src/commands/workspace/start.ts)_ diff --git a/src/commands/workspace/start.ts b/src/commands/workspace/start.ts index 2c1b42aaa..9c2d4f6af 100644 --- a/src/commands/workspace/start.ts +++ b/src/commands/workspace/start.ts @@ -21,7 +21,13 @@ export default class Start extends Command { char: 'f', description: 'path to a valid devfile', env: 'DEVFILE_PATH', - required: true, + required: false, + }), + workspaceconfig: string({ + char: 'w', + description: 'path to a valid workspace configuration json file', + env: 'WORKSPACE_CONFIG_JSON_PATH', + required: false, }), } @@ -32,7 +38,8 @@ export default class Start extends Command { const che = new CheHelper() const tasks = new Listr([ { title: 'Verify if Che server is running', task: async () => { if (!await che.isCheServerReady(flags.chenamespace)) { this.error(`E_SRV_NOT_RUNNING - Che Server is not running.\nChe Server cannot be found in Kubernetes Namespace "${flags.chenamespace}". Have you already start it?\nFix with: start Che server: chectl server:start\nhttps://github.com/eclipse/che`, { code: 'E_SRV_NOT_RUNNNG'}) } } }, - { title: `Create Workspaces from Devfile ${flags.devfile}`, task: async (ctx: any) => { ctx.workspaceIdeURL = await che.createWorkspaceFromDevfile(flags.chenamespace, flags.devfile) } }, + { title: `Create workspace from Devfile ${flags.devfile}`, enabled: () => flags.devfile !== undefined, task: async (ctx: any) => { ctx.workspaceIdeURL = await che.createWorkspaceFromDevfile(flags.chenamespace, flags.devfile) } }, + { title: `Create workspace from Workspace Config ${flags.workspaceconfig}`, enabled: () => flags.workspaceconfig !== undefined, task: async (ctx: any) => { ctx.workspaceIdeURL = await che.createWorkspaceFromWorkspaceConfig(flags.chenamespace, flags.workspaceconfig) } }, ]) try { diff --git a/src/helpers/che.ts b/src/helpers/che.ts index 62c944692..44d7818e1 100644 --- a/src/helpers/che.ts +++ b/src/helpers/che.ts @@ -136,37 +136,62 @@ export class CheHelper { } let devfile - let url = await this.cheURL(namespace) try { + let url = await this.cheURL(namespace) devfile = fs.readFileSync(devfilePath, 'utf8') let response = await axios.post(`${url}/api/devfile`, devfile, {headers: {'Content-Type': 'text/yaml'}}) if (response && response.data && response.data.links && response.data.links.ide) { - // console.log(response.data) - // console.log(response.status) - // console.log(response.statusText) - // console.log(response.headers) - // console.log(response.config) let ideURL = response.data.links.ide - return ideURL.replace(/\/\w*\/\w*$/g, '\/dashboard\/#\/ide$&') + return this.buildDashboardURL(ideURL) } else { throw new Error('E_BAD_RESP_CHE_SERVER') } } catch (error) { - if (!devfile) { throw new Error(`E_NOT_FOUND_DEFILE - ${devfilePath} - ${error.message}`) } - if (error.response && error.response.status && error.response.status === 400) { - // console.log(error.response.data) - // console.log(error.response.status) - // console.log(error.response.headers) - throw new Error(`E_BAD_DEFILE_FORMAT - Message: ${error.response.data.message}`) + if (!devfile) { throw new Error(`E_NOT_FOUND_DEVFILE - ${devfilePath} - ${error.message}`) } + if (error.response && error.response.status === 400) { + throw new Error(`E_BAD_DEVFILE_FORMAT - Message: ${error.response.data.message}`) + } + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + throw new Error(`E_CHE_SERVER_UNKNOWN_ERROR - Status: ${error.response.status}`) + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + throw new Error(`E_CHE_SERVER_NO_RESPONSE - ${error.message}`) + } else { + // Something happened in setting up the request that triggered an Error + throw new Error(`E_CHECTL_UNKNOWN_ERROR - Message: ${error.message}`) } + } + } + async createWorkspaceFromWorkspaceConfig(namespace: string | undefined, workspaceConfigPath: string | undefined = ''): Promise { + if (!await this.cheNamespaceExist(namespace)) { + throw new Error('E_BAD_NS') + } + + let workspaceConfig + try { + let url = await this.cheURL(namespace) + let workspaceConfig = fs.readFileSync(workspaceConfigPath, 'utf8') + let response = await axios.post(`${url}/api/workspace`, workspaceConfig, {headers: {'Content-Type': 'application/json'}}) + if (response && response.data && response.data.links && response.data.links.ide) { + let ideURL = response.data.links.ide + return this.buildDashboardURL(ideURL) + } else { + throw new Error('E_BAD_RESP_CHE_SERVER') + } + } catch (error) { + if (!workspaceConfig) { throw new Error(`E_NOT_FOUND_WORKSPACE_CONFIG_FILE - ${workspaceConfigPath} - ${error.message}`) } + if (error.response && error.response.status === 400) { + throw new Error(`E_BAD_WORKSPACE_CONFIG_FORMAT - Message: ${error.response.data.message}`) + } if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - // console.log(error.response.data) - // console.log(error.response.status) - // console.log(error.response.headers) throw new Error(`E_CHE_SERVER_UNKNOWN_ERROR - Status: ${error.response.status}`) } else if (error.request) { // The request was made but no response was received @@ -179,4 +204,8 @@ export class CheHelper { } } } + + async buildDashboardURL(ideURL: string): Promise { + return ideURL.replace(/\/[^/|.]*\/[^/|.]*$/g, '\/dashboard\/#\/ide$&') + } } diff --git a/test/helpers/che.test.ts b/test/helpers/che.test.ts index 09b43bdeb..d66e5db11 100644 --- a/test/helpers/che.test.ts +++ b/test/helpers/che.test.ts @@ -123,30 +123,31 @@ describe('Che helper', () => { 'Content-Type': 'application/json' })) .do(() => ch.createWorkspaceFromDevfile(namespace, __dirname + '/requests/devfile.invalid')) - .catch(/E_BAD_DEFILE_FORMAT/) + .catch(/E_BAD_DEVFILE_FORMAT/) .it('fails creating a workspace from an invalid devfile') fancy .stub(ch, 'cheNamespaceExist', () => true) .stub(ch, 'cheURL', () => cheURL) .do(() => ch.createWorkspaceFromDevfile(namespace, __dirname + '/requests/devfile.inexistent')) - .catch(/E_NOT_FOUND_DEFILE/) + .catch(/E_NOT_FOUND_DEVFILE/) .it('fails creating a workspace from a non-existing devfile') - // fancy - // .stub(ch, 'cheNamespaceExist', () => true) - // .stub(ch, 'cheURL', () => cheURL) - // .nock(cheURL, api => api - // .post('/api/devfile') - // .replyWithFile(400, __dirname + '/replies/create-workspace-from-valid-devfile', { 'Content-Type': 'application/json' })) - // // .get('/api/system/state') - // // .reply(404) - // // .get('/api/system/state') - // // .reply(404) - // // .get('/api/system/state') - // // .reply(503)) - // .it('fails creating a workspace from an invalid devfile', async () => { - // const res = await ch.createWorkspaceFromDevfile(namespace, __dirname + '/requests/devfile.valid') - // expect(res).to.equal('http://che-kube-che.192.168.64.39.nip.io/che/chectl') - // }) + fancy + .stub(ch, 'cheNamespaceExist', () => true) + .stub(ch, 'cheURL', () => cheURL) + .nock(cheURL, api => api + .post('/api/workspace') + .replyWithFile(201, __dirname + '/replies/create-workspace-from-valid-devfile.json', { 'Content-Type': 'application/json' })) + .it('succeds creating a workspace from a valid workspaceconfig', async () => { + const res = await ch.createWorkspaceFromWorkspaceConfig(namespace, __dirname + '/requests/workspaceconfig.valid') + expect(res).to.equal('https://che-kube-che.192.168.64.39.nip.io/dashboard/#/ide/che/chectl') + }) + fancy + .it('builds the Dashboard URL of a workspace given the IDE link', async () => { + let ideURL = 'https://che-kube-che.192.168.64.40.nip.io/che/name-with-dashes' + let dashboardURL = 'https://che-kube-che.192.168.64.40.nip.io/dashboard/#/ide/che/name-with-dashes' + let res = await ch.buildDashboardURL(ideURL) + expect(res).to.equal(dashboardURL) + }) describe('getWorkspacePod', () => { fancy .stub(kc, 'makeApiClient', () => k8sApi) diff --git a/test/helpers/replies/create-workspace-from-valid-workspaceconfig.json b/test/helpers/replies/create-workspace-from-valid-workspaceconfig.json new file mode 100644 index 000000000..70ca094dd --- /dev/null +++ b/test/helpers/replies/create-workspace-from-valid-workspaceconfig.json @@ -0,0 +1,109 @@ +{ + "links": { + "self": "http://che-kube-che.192.168.64.40.nip.io/api/workspace/workspace1ioy0lx0zg7j0jio", + "ide": "http://che-kube-che.192.168.64.40.nip.io/che/inner-loop-buildah" + }, + "attributes": { + "created": "1545089353884" + }, + "namespace": "che", + "temporary": false, + "id": "workspace1ioy0lx0zg7j0jio", + "status": "STOPPED", + "config": { + "defaultEnv": "default", + "environments": { + "default": { + "machines": { + "ws/dev": { + "attributes": { + "memoryLimitBytes": "536870912" + }, + "servers": {}, + "volumes": { + "projects": { + "path": "/projects" + } + }, + "installers": [], + "env": { + "": "" + } + } + }, + "recipe": { + "type": "kubernetes", + "content": "kind: List\nitems:\n - \n apiVersion: v1\n kind: Pod\n metadata:\n name: ws\n spec:\n containers:\n - \n image: mariolet/che-plugin-dev-tooling:jug\n name: dev\n resources:\n limits:\n memory: 512Mi\n", + "contentType": "application/x-yaml" + } + } + }, + "projects": [ + { + "links": [], + "name": "spring-petclinic", + "attributes": {}, + "source": { + "location": "https://github.com/sleshchenko/spring-petclinic.git", + "type": "git", + "parameters": {} + }, + "path": "/spring-petclinic", + "description": "", + "mixins": [], + "problems": [] + } + ], + "name": "inner-loop-buildah", + "attributes": { + "editor": "org.eclipse.che.editor.theia:1.0.0", + "sidecar.theia-ide.memory_limit": "512Mi", + "plugins": "che-machine-exec-plugin:0.0.1" + }, + "commands": [ + { + "commandLine": "cd /projects/spring-petclinic && kubectl apply -f app4jug.yaml", + "name": "Run Kube App", + "attributes": { + "machineName": "ws/dev", + "goal": "Run" + }, + "type": "che" + }, + { + "commandLine": "cd /projects/spring-petclinic && ./mvnw package", + "name": "Build Java", + "attributes": { + "machineName": "ws/dev", + "goal": "Build" + }, + "type": "che" + }, + { + "commandLine": "cd /projects/spring-petclinic && buildah bud -t petclinic .", + "name": "Build Container Image", + "attributes": { + "machineName": "ws/dev", + "goal": "Build" + }, + "type": "che" + }, + { + "commandLine": "cd /projects/spring-petclinic && buildah push --tls-verify=false petclinic docker://$(kubectl get services -n kube-system -o jsonpath={.spec.clusterIP} registry)/mariolet/petclinic:latest", + "name": "Push Container Image", + "attributes": { + "machineName": "ws/dev", + "goal": "Build" + }, + "type": "che" + }, + { + "commandLine": "echo ${CHE_OSO_CLUSTER//api/console}", + "name": "Get OpenShift Console URL", + "attributes": {}, + "type": "custom" + } + ], + "links": [] + } +} diff --git a/test/helpers/requests/workspaceconfig.valid b/test/helpers/requests/workspaceconfig.valid new file mode 100644 index 000000000..e80362ecb --- /dev/null +++ b/test/helpers/requests/workspaceconfig.valid @@ -0,0 +1,96 @@ +{ + "environments": { + "default": { + "recipe": { + "contentType": "application/x-yaml", + "type": "kubernetes", + "content": "kind: List\nitems:\n - \n apiVersion: v1\n kind: Pod\n metadata:\n name: ws\n spec:\n containers:\n - \n image: mariolet/che-plugin-dev-tooling:jug\n name: dev\n resources:\n limits:\n memory: 512Mi\n" + }, + "machines": { + "ws/dev": { + "env": { + "": "" + }, + "installers": [], + "servers": {}, + "volumes": { + "projects": { + "path": "/projects" + } + }, + "attributes": { + "memoryLimitBytes": "536870912" + } + } + } + } + }, + "commands": [ + { + "commandLine": "cd /projects/spring-petclinic && kubectl apply -f app4jug.yaml", + "name": "Run Kube App", + "type": "che", + "attributes": { + "machineName": "ws/dev", + "goal": "Run" + } + }, + { + "commandLine": "cd /projects/spring-petclinic && ./mvnw package", + "name": "Build Java", + "type": "che", + "attributes": { + "machineName": "ws/dev", + "goal": "Build" + } + }, + { + "commandLine": "cd /projects/spring-petclinic && buildah bud -t petclinic .", + "name": "Build Container Image", + "type": "che", + "attributes": { + "machineName": "ws/dev", + "goal": "Build" + } + }, + { + "commandLine": "cd /projects/spring-petclinic && buildah push --tls-verify=false petclinic docker://$(kubectl get services -n kube-system -o jsonpath={.spec.clusterIP} registry)/mariolet/petclinic:latest", + "name": "Push Container Image", + "type": "che", + "attributes": { + "machineName": "ws/dev", + "goal": "Build" + } + }, + { + "commandLine": "echo ${CHE_OSO_CLUSTER//api/console}", + "name": "Get OpenShift Console URL", + "type": "custom", + "attributes": {} + } + ], + "projects": [ + { + "source": { + "location": "https://github.com/sleshchenko/spring-petclinic.git", + "type": "git", + "parameters": {} + }, + "links": [], + "description": "", + "problems": [], + "mixins": [], + "name": "spring-petclinic", + "path": "/spring-petclinic", + "attributes": {} + } + ], + "defaultEnv": "default", + "name": "inner-loop-buildah", + "attributes": { + "editor": "org.eclipse.che.editor.theia:1.0.0", + "plugins": "che-machine-exec-plugin:0.0.1", + "sidecar.theia-ide.memory_limit": "512Mi" + }, + "links": [] +}