From 363eef70b231647dec4f769f03d272cbe74227ed Mon Sep 17 00:00:00 2001 From: Alan Zanatta Date: Thu, 6 Dec 2018 14:21:58 +0100 Subject: [PATCH] Adds docker plugin (#460) --- images/plugins/docker.svg | 1 + .../ConsoleFeature/Epics/ConsoleEpics.js | 6 + .../ConsoleFeature/Reducers/ConsoleSources.js | 2 +- .../Epics/languageClient.js | 2 +- .../Plugins/Docker/index.js | 203 ++++++++++++++++++ .../Plugins/Docker/lsp.js | 59 +++++ .../PackageFeature/Epics/GeneratePlans.js | 1 + lib/molecule-dev-environment.js | 6 + package.json | 2 + testsData/test-docker/docker-compose.yaml | 49 +++++ yarn.lock | 12 ++ 11 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 images/plugins/docker.svg create mode 100644 lib/ExecutionControlEpic/Plugins/Docker/index.js create mode 100644 lib/ExecutionControlEpic/Plugins/Docker/lsp.js create mode 100644 testsData/test-docker/docker-compose.yaml diff --git a/images/plugins/docker.svg b/images/plugins/docker.svg new file mode 100644 index 00000000..6b9e63c9 --- /dev/null +++ b/images/plugins/docker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/ExecutionControlEpic/ConsoleFeature/Epics/ConsoleEpics.js b/lib/ExecutionControlEpic/ConsoleFeature/Epics/ConsoleEpics.js index 2d973f57..4190225e 100644 --- a/lib/ExecutionControlEpic/ConsoleFeature/Epics/ConsoleEpics.js +++ b/lib/ExecutionControlEpic/ConsoleFeature/Epics/ConsoleEpics.js @@ -3,6 +3,7 @@ import Rx from "rxjs"; import { addConsoleLogsForTask } from "../Actions/AddConsoleLog"; +import { addConsoleSource } from "../Actions/AddConsoleSource"; import { addBufferedLogsForTask } from "../Actions/AddBufferedLogs"; import { ConsoleLogError, @@ -39,6 +40,11 @@ const consoleEpic = () => (action$: any) => { }), ), + action$ + .ofType("ADD_PLAN_CONFIGURATION") + .filter(action => action.payload.ownSource) + .map(action => addConsoleSource(action.payload.name, true)), + action$.ofType("FEATURE_LOAD").map(action => addConsoleLogsForTask({ ...molecule, diff --git a/lib/ExecutionControlEpic/ConsoleFeature/Reducers/ConsoleSources.js b/lib/ExecutionControlEpic/ConsoleFeature/Reducers/ConsoleSources.js index 21820520..9217d574 100644 --- a/lib/ExecutionControlEpic/ConsoleFeature/Reducers/ConsoleSources.js +++ b/lib/ExecutionControlEpic/ConsoleFeature/Reducers/ConsoleSources.js @@ -3,7 +3,7 @@ import { Map } from "immutable"; -const initialState = Map().setIn(["Molecule"], ["Molecule", true]); +const initialState = Map().setIn(["Molecule"], ["Molecule", false]); export default function( state: ConsoleSourcesReducer = initialState, diff --git a/lib/ExecutionControlEpic/LanguageServerProtocolFeature/Epics/languageClient.js b/lib/ExecutionControlEpic/LanguageServerProtocolFeature/Epics/languageClient.js index 7078dc47..53ad35b1 100644 --- a/lib/ExecutionControlEpic/LanguageServerProtocolFeature/Epics/languageClient.js +++ b/lib/ExecutionControlEpic/LanguageServerProtocolFeature/Epics/languageClient.js @@ -46,7 +46,7 @@ export function languageClientGetActions( observer.next( // TODO - Remove hard-coded values bufferConsoleLogsForTask({ - source: "Molecule", + source: planConfig.ownSource ? planConfig.name : "Molecule", color: "#592b71", version: "0.4.0", severity: data.type, diff --git a/lib/ExecutionControlEpic/Plugins/Docker/index.js b/lib/ExecutionControlEpic/Plugins/Docker/index.js new file mode 100644 index 00000000..3f2a8768 --- /dev/null +++ b/lib/ExecutionControlEpic/Plugins/Docker/index.js @@ -0,0 +1,203 @@ +"use babel"; +// @flow + +import type { + GeneratedPlanObject, + PlanConfig, +} from "../../PlanConfigurationFeature/Types/types"; +import type { PackageTesterResult } from "../../../ProjectSystemEpic/PackageFeature/Types/types"; +import type { Plugin } from "../../DevtoolLoadingFeature/Types/types"; +import path from "path"; +import yaml from "js-yaml"; +import fs from "fs"; +import moment from "moment"; + +const plugin: Plugin = { + info: { + name: "docker", + dominantColor: "#339fee", + iconUri: "atom://molecule/images/plugins/docker.svg", + }, + + configSchema: { + type: "object", + schemas: { + command: { + type: "enum", + label: "command", + default: "start", + enum: [ + { value: "build", description: "build" }, + { value: "up", description: "up" }, + { value: "push", description: "push" }, + ], + }, + serviceName: { + type: "string", + label: "service name", + placeholder: "database,web... empty for all", + default: "", + }, + bin: { + type: "string", + label: "binary path", + placeholder: "docker-compose", + default: "docker-compose", + }, + }, + }, + + getStrategyForPlan(plan: PlanConfig) { + if (plan.config.command === "up") { + return { + strategy: { + type: "node", + path: path.join(__dirname, "lsp"), + cwd: path.dirname(plan.packageInfo.path), + lsp: true, + env: { + COMPOSE_COMMAND: JSON.stringify({ + bin: plan.config.bin, + command: plan.config.command, + service: plan.config.serviceName, + configFile: plan.packageInfo.path, + }), + }, + }, + }; + } else { + const command = `${plan.config.bin} -f ${plan.packageInfo.path} ${ + plan.config.command + } ${plan.config.serviceName}`; + return { + strategy: { + type: "terminal", + cwd: path.dirname(plan.packageInfo.path), + command: command, + env: process.env, + }, + controller: { + onData(data: string, taskAPI: TaskAPI, helperAPI: HelperAPI) {}, + onError(err: any, taskAPI: TaskAPI, helperAPI: HelperAPI) { + taskAPI.diagnostics.setForWorkspace({ + uri: "docker-compose", + diagnostics: [ + { + range: { + start: { line: -1, character: -1 }, + end: { line: -1, character: -1 }, + }, + severity: helperAPI.severity.error, + message: `Error: ${err.code | 1}`.toString(), + date: moment().unix(), + }, + ], + }); + }, + }, + }; + } + }, + + async isPackage(packagePath: string): PackageTesterResult { + if (!packagePath.endsWith(".yaml")) { + return false; + } + + const rawData = await new Promise((resolve, reject) => { + fs.readFile(packagePath, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + const fileConfig = yaml.safeLoad(rawData); + return "version" in fileConfig && "services" in fileConfig; + }, + + async generatePlansForPackage( + packagePath: string, + ): Promise> { + const rawData = await new Promise((resolve, reject) => { + fs.readFile(packagePath, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + const fileConfig = yaml.safeLoad(rawData); + + return Promise.resolve([ + { + name: `up`, + value: { + command: "up", + serviceName: "", + bin: "docker-compose", + }, + autorun: false, + ownSource: true, + }, + { + name: `build`, + value: { + command: "build", + serviceName: "", + bin: "docker-compose", + }, + autorun: false, + }, + { + name: `push`, + value: { + command: "push", + serviceName: "", + bin: "docker-compose", + }, + autorun: false, + }, + ...Object.keys(fileConfig.services || {}) + .map(serviceName => [ + { + name: `up ${serviceName}`, + value: { + command: "up", + serviceName: serviceName, + bin: "docker-compose", + }, + autorun: false, + ownSource: true, + }, + { + name: `${ + fileConfig.services[serviceName].image ? "pull" : "build" + } ${serviceName}`, + value: { + command: `${ + fileConfig.services[serviceName].image ? "pull" : "build" + }`, + serviceName: serviceName, + bin: "docker-compose", + }, + autorun: false, + }, + { + name: `push ${serviceName}`, + value: { + command: "push", + serviceName: serviceName, + bin: "docker-compose", + }, + autorun: false, + }, + ]) + .reduce((red, arr) => [...red, ...arr], []), + ]); + }, +}; + +export default plugin; diff --git a/lib/ExecutionControlEpic/Plugins/Docker/lsp.js b/lib/ExecutionControlEpic/Plugins/Docker/lsp.js new file mode 100644 index 00000000..d6f5f960 --- /dev/null +++ b/lib/ExecutionControlEpic/Plugins/Docker/lsp.js @@ -0,0 +1,59 @@ +/* eslint-disable */ +const { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, +} = require("vscode-jsonrpc"); + +const { spawn, exec } = require("child_process"); +const process = require("process"); +const StreamSplitter = require("stream-splitter"); +const stripAnsi = require("strip-ansi"); + +const connection = createMessageConnection( + new StreamMessageReader(process.stdin), + new StreamMessageWriter(process.stdout), +); + +connection.onRequest("initialize", () => { + return new Promise((resolve, reject) => { + const { bin, command, service, configFile } = JSON.parse( + process.env.COMPOSE_COMMAND, + ); + + const dockerProcess = spawn(bin, ["-f", configFile, command, service], { + stderr: "pipe", + stdout: "pipe", + }); + dockerProcess.on("error", err => + connection.sendNotification("window/logMessage", { + type: 3, + message: `Error running Docker: ${err}`, + }), + ); + + const lines = dockerProcess.stdout.pipe(StreamSplitter("\n")); + lines.on("token", line => + connection.sendNotification("window/logMessage", { + type: 1, + message: stripAnsi(line.toString()), + }), + ); + + connection.onRequest("shutdown", () => { + return new Promise(resolve => { + dockerProcess.kill(); + exec( + `${bin} -f ${configFile} stop ${service}`, + {}, + (err, stdout, stderr) => { + resolve(); + }, + ); + }); + }); + resolve(); + }); +}); + +connection.listen(); diff --git a/lib/ProjectSystemEpic/PackageFeature/Epics/GeneratePlans.js b/lib/ProjectSystemEpic/PackageFeature/Epics/GeneratePlans.js index aba7fad1..29a188cd 100644 --- a/lib/ProjectSystemEpic/PackageFeature/Epics/GeneratePlans.js +++ b/lib/ProjectSystemEpic/PackageFeature/Epics/GeneratePlans.js @@ -43,6 +43,7 @@ const generatePlansEpic = (action$: Observable, store: Store) => { autoGenerated: true, autoRun: planObject.autoRun, pinned: planObject.autoRun === true ? true : false, + ownSource: planObject.ownSource || false, }), ) .filter( diff --git a/lib/molecule-dev-environment.js b/lib/molecule-dev-environment.js index 24272b44..d0fb5c1c 100644 --- a/lib/molecule-dev-environment.js +++ b/lib/molecule-dev-environment.js @@ -50,6 +50,11 @@ export default { plugins: { type: "object", properties: { + Docker: { + type: "boolean", + description: "Activate or not Docker in Molecule", + default: true, + }, Eslint: { type: "boolean", description: "Activate or not Eslint in Molecule", @@ -142,6 +147,7 @@ export default { // Bind plugins. Path should be relative to /lib/ExecutionControlEpic/Plugins/ [ + "Docker/index.js", "Eslint/index.js", "Nightwatch/index.js", "Flowtype.js", diff --git a/package.json b/package.json index 0b8bb1c9..434decff 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "flow-language-server": "^0.5.0", "immutable": "^3.8.1", "jest-cli": "^23.1.0", + "js-yaml": "^3.12.0", "less": "^3.0.4", "mocha": "^5.1.1", "moment": "^2.19.2", @@ -76,6 +77,7 @@ "remote-redux-devtools": "^0.5.7", "rxjs": "^5.4.3", "stream-buffers": "^3.0.1", + "stream-splitter": "^0.3.2", "styled-components": "^3.2.6", "tree-kill": "^1.2.0", "vscode-jsonrpc": "^3.6.1", diff --git a/testsData/test-docker/docker-compose.yaml b/testsData/test-docker/docker-compose.yaml new file mode 100644 index 00000000..a4679fd7 --- /dev/null +++ b/testsData/test-docker/docker-compose.yaml @@ -0,0 +1,49 @@ +version: "3" +services: + redis: + image: redis:alpine + ports: + - "6379" + networks: + - frontend + + db: + image: postgres:9.4 + networks: + - backend + + vote: + image: dockersamples/examplevotingapp_vote:before + ports: + - "5000:80" + networks: + - frontend + depends_on: + - redis + + result: + image: dockersamples/examplevotingapp_result:before + ports: + - "5001:80" + networks: + - backend + depends_on: + - db + + worker: + image: dockersamples/examplevotingapp_worker + networks: + - frontend + - backend + + visualizer: + image: dockersamples/visualizer:stable + ports: + - "8080:8080" + stop_grace_period: 1m30s + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + +networks: + frontend: + backend: diff --git a/yarn.lock b/yarn.lock index 226496af..b73db16f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1248,6 +1248,11 @@ buffer@^5.0.3: base64-js "^1.0.2" ieee754 "^1.1.4" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -6445,6 +6450,13 @@ stream-buffers@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.1.tgz#68a38c5faadeded79ff79988d368e3fb1325ef06" +stream-splitter@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/stream-splitter/-/stream-splitter-0.3.2.tgz#e2f28262e53c2d8bbc492e72f2960fc19857984d" + integrity sha1-4vKCYuU8LYu8SS5y8pYPwZhXmE0= + dependencies: + buffers "~0.1.1" + streamroller@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.4.1.tgz#d435bd5974373abd9bd9068359513085106cc05f"