@runium/plugin-docker 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -0
- package/package.json +4 -24
- package/tasks/docker-compose-task.js +1 -0
- package/tasks/docker-task-base.js +1 -0
- package/tasks/docker-task.js +1 -0
- package/utils/exec-async.js +1 -0
- package/utils/is-docker-installed.js +1 -0
- package/utils/is-docker-running.js +1 -0
- package/utils/pull-image.js +1 -0
- package/validation/docker-compose-task-project-schema.js +1 -0
- package/validation/docker-task-project-schema.js +1 -0
- package/.eslintrc.json +0 -19
- package/.prettierrc.json +0 -8
- package/build.js +0 -95
- package/src/index.ts +0 -23
- package/src/tasks/docker-compose-task.ts +0 -299
- package/src/tasks/docker-task-base.ts +0 -257
- package/src/tasks/docker-task.ts +0 -221
- package/src/utils/exec-async.ts +0 -4
- package/src/utils/is-docker-installed.ts +0 -13
- package/src/utils/is-docker-running.ts +0 -13
- package/src/utils/pull-image.ts +0 -47
- package/src/validation/docker-compose-task-project-schema.ts +0 -137
- package/src/validation/docker-task-project-schema.ts +0 -68
- package/tsconfig.json +0 -22
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DockerTask as o}from"./tasks/docker-task.js";import{DockerComposeTask as e}from"./tasks/docker-compose-task.js";import{getDockerTaskProjectSchema as r}from"./validation/docker-task-project-schema.js";import{getDockerComposeTaskProjectSchema as c}from"./validation/docker-compose-task-project-schema.js";function p(){return{name:"docker",project:{tasks:{docker:o,"docker-compose":e},validationSchema:{tasks:{Docker_DockerTask:r(),Docker_DockerComposeTask:c()}}}}}export{p as default};
|
package/package.json
CHANGED
|
@@ -1,38 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runium/plugin-docker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Runium plugin for Docker support",
|
|
5
5
|
"author": "TheBeastApp",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"main": "dist/index.js",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "rimraf ./lib && node build.js",
|
|
11
|
-
"build:tsc": "rimraf ./lib && tsc",
|
|
12
|
-
"lint": "eslint ./src --ext .ts",
|
|
13
|
-
"lint:fix": "eslint ./src --ext .ts --fix",
|
|
14
|
-
"format": "prettier --write \"src/**/*.ts\""
|
|
15
|
-
},
|
|
16
8
|
"keywords": [
|
|
17
9
|
"runium",
|
|
18
10
|
"runium-plugin",
|
|
19
11
|
"docker"
|
|
20
12
|
],
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
24
|
-
"@typescript-eslint/parser": "^7.0.0",
|
|
25
|
-
"esbuild": "^0.25.11",
|
|
26
|
-
"eslint": "^8.56.0",
|
|
27
|
-
"eslint-config-prettier": "^9.1.0",
|
|
28
|
-
"eslint-plugin-prettier": "^5.1.3",
|
|
29
|
-
"prettier": "^3.2.5",
|
|
30
|
-
"rimraf": "^6.1.0",
|
|
31
|
-
"ts-node": "^10.9.2",
|
|
32
|
-
"typescript": "^5.3.3",
|
|
33
|
-
"vite": "^7.1.12"
|
|
34
|
-
},
|
|
13
|
+
"module": "./index.js",
|
|
14
|
+
"main": "./index.js",
|
|
35
15
|
"dependencies": {
|
|
36
16
|
"yaml": "^2.8.2"
|
|
37
17
|
}
|
|
38
|
-
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as a}from"node:child_process";import{stringify as p}from"yaml";import{DockerTaskBase as c}from"./docker-task-base.js";import{isDockerInstalled as m}from"../utils/is-docker-installed.js";import{isDockerRunning as d}from"../utils/is-docker-running.js";import{pullImage as g}from"../utils/pull-image.js";const{TaskStatus:s}=runium.enum;class C extends c{constructor(r){super(r);this.options=r;this.projectName=`runium-docker-compose-${Date.now()}`,this.composeFile=`docker-compose-${this.projectName}.yml`}projectName;composeFile;version="3.8";correctExitCodes=[130];generateComposeYaml(){const r={version:this.version,services:{}};for(const[o,e]of Object.entries(this.options.services)){const t={image:e.image};e.containerName&&(t.container_name=e.containerName),e.command&&(t.command=e.command),e.ports&&e.ports.length>0&&(t.ports=e.ports),e.volumes&&e.volumes.length>0&&(t.volumes=e.volumes),e.environment&&Object.keys(e.environment).length>0&&(t.environment=e.environment),e.networks&&e.networks.length>0&&(t.networks=e.networks),e.dependsOn&&e.dependsOn.length>0&&(t.depends_on=e.dependsOn),e.restart&&(t.restart=e.restart),e.user&&(t.user=e.user),e.workdir&&(t.working_dir=e.workdir),e.entrypoint&&(t.entrypoint=e.entrypoint),e.privileged&&(t.privileged=e.privileged),e.expose&&e.expose.length>0&&(t.expose=e.expose),e.healthcheck&&(t.healthcheck=e.healthcheck),r.services[o]=t}return this.options.networks&&Object.keys(this.options.networks).length>0&&(r.networks=this.options.networks),this.options.volumes&&Object.keys(this.options.volumes).length>0&&(r.volumes=this.options.volumes),p(r)}async pullAllImages(){const r=Object.values(this.options.services).map(e=>e.image),o=[...new Set(r)];for(const e of o)this.updateState({reason:`pull image: ${e}`}),await g(e,{onStdErr:this.onStdErrData.bind(this)})}prepareDockerComposeArgs(r){const o=["compose"];return o.push("-p",this.projectName),o.push("-f",runium.storage.getPath(["docker",this.composeFile])),o.push(r),o}async start(){if(this.canStart()){if(this.updateState({status:s.STARTING,iteration:this.state.iteration+1,pid:-1,exitCode:void 0,error:void 0,reason:void 0}),!await m()){this.onError(new Error("Docker is not installed"));return}if(!await d()){this.onError(new Error("Docker is not running"));return}try{const r=this.generateComposeYaml();await runium.storage.write(["docker",this.composeFile],r),await this.initLogStreams(),await this.pullAllImages();const o=this.prepareDockerComposeArgs("up");this.process=a("docker",o,{stdio:["ignore","pipe","pipe"]}),this.addProcessListeners(),this.setTTLTimer(),this.updateState({status:s.STARTED,pid:this.process.pid,reason:void 0})}catch(r){this.onError(r)}}}async stop(r=""){if(this.canStop()){this.updateState({status:s.STOPPING,reason:r});try{const o=this.prepareDockerComposeArgs("down");await new Promise((e,t)=>{const n=a("docker",o);n.on("exit",i=>{i===0?e():t(new Error(`Docker compose down exited with code ${i}`))}),n.on("error",t)})}catch(o){this.onError(o)}}}}export{C as DockerComposeTask};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createWriteStream as o}from"node:fs";import{mkdir as a}from"node:fs/promises";import{dirname as n,resolve as u}from"node:path";import{setTimeout as l}from"node:timers";const d=-1,m=50,{TaskStatus:s,TaskEvent:i}=runium.enum;class P extends runium.class.RuniumTask{constructor(t){super(t);this.options=t;this.setMaxListeners(m)}correctExitCodes=[];state={status:s.IDLE,timestamp:Date.now(),iteration:0,pid:-1};process=null;stdoutStream=null;stderrStream=null;ttlTimer=null;getState(){return{...this.state}}getOptions(){return{...this.options}}canStart(){const{status:t}=this.state;return t!==s.STARTED&&t!==s.STARTING&&t!==s.STOPPING}canStop(){const{status:t}=this.state;return t===s.STARTED||t===s.STARTING}async restart(){await this.stop("restart"),await this.start()}async initLogStreams(){const{stdout:t=null,stderr:e=null}=this.options.log||{};if(t){const r=u(t);await a(n(r),{recursive:!0}),this.stdoutStream=o(t,{flags:"w"})}if(e){const r=u(e);await a(n(r),{recursive:!0}),this.stderrStream=o(e,{flags:"w"})}}setTTLTimer(){const{ttl:t}=this.options;t&&this.process&&(this.ttlTimer=l(()=>{this.stop("ttl")},t))}addProcessListeners(){this.process&&(this.process.stdout?.on("data",t=>this.onStdOutData(t)),this.process.stderr?.on("data",t=>this.onStdErrData(t)),this.process.on("exit",t=>this.onExit(t)),this.process.on("error",t=>this.onError(t)))}onStdOutData(t){const e=t.toString();this.emit(i.STDOUT,e),this.stdoutStream&&this.stdoutStream.write(e)}onStdErrData(t){const e=t.toString();this.emit(i.STDERR,e),this.stderrStream&&this.stderrStream.write(e)}onExit(t){const e=t!==null&&!this.correctExitCodes.includes(t)?t:d;this.updateState({status:e===0?s.COMPLETED:e===d?s.STOPPED:s.FAILED,exitCode:e}),this.cleanup()}onError(t){const e=t?.code??null;this.updateState({status:e===null?s.STOPPED:s.FAILED,reason:t.message||(e??"")}),this.cleanup()}updateState(t){const e={...this.state,...t,timestamp:Date.now()};this.state=Object.fromEntries(Object.entries(e).filter(([r,c])=>c!==void 0)),this.emit(i.STATE_CHANGE,this.getState()),this.emit(this.state.status,this.getState())}cleanup(){this.ttlTimer&&(clearTimeout(this.ttlTimer),this.ttlTimer=null),this.process&&(this.process.removeAllListeners(),this.process.stdout?.removeAllListeners(),this.process.stderr?.removeAllListeners(),this.process=null),this.stdoutStream&&(this.stdoutStream.end(),this.stdoutStream=null),this.stderrStream&&(this.stderrStream.end(),this.stderrStream=null)}}export{P as DockerTaskBase,d as SILENT_EXIT_CODE};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as n}from"node:child_process";import{DockerTaskBase as a}from"./docker-task-base.js";import{isDockerInstalled as p}from"../utils/is-docker-installed.js";import{isDockerRunning as h}from"../utils/is-docker-running.js";import{pullImage as c}from"../utils/pull-image.js";const{TaskStatus:o}=runium.enum;class T extends a{constructor(t){super(t);this.options=t;this.options.containerName??=`runium-${this.options.image.replaceAll("/","-").replaceAll(":","-")}-${Date.now()}`}correctExitCodes=[137];prepareDockerArgs(){const t=["run"];return this.options.autoRemove!==!1&&t.push("--rm"),this.options.containerName&&t.push("--name",this.options.containerName),this.options.ports&&this.options.ports.forEach(s=>{t.push("-p",s)}),this.options.volumes&&this.options.volumes.forEach(s=>{t.push("-v",s)}),this.options.env&&Object.entries(this.options.env).forEach(([s,i])=>{t.push("-e",`${s}=${i}`)}),this.options.network&&t.push("--network",this.options.network),this.options.user&&t.push("--user",this.options.user),this.options.workdir&&t.push("--workdir",this.options.workdir),this.options.entrypoint&&t.push("--entrypoint",this.options.entrypoint),this.options.privileged&&t.push("--privileged"),t.push(this.options.image),this.options.command&&t.push(this.options.command),this.options.arguments&&t.push(...this.options.arguments),t}async start(){if(this.canStart()){if(this.updateState({status:o.STARTING,iteration:this.state.iteration+1,pid:-1,containerName:void 0,exitCode:void 0,error:void 0,reason:void 0}),!await p()){this.onError(new Error("Docker is not installed"));return}if(!await h()){this.onError(new Error("Docker is not running"));return}try{await this.initLogStreams(),this.updateState({reason:`pull image: ${this.options.image}`}),await c(this.options.image,{onStdErr:this.onStdErrData.bind(this)});const t=this.prepareDockerArgs();this.process=n("docker",t,{stdio:["ignore","pipe","pipe"]}),this.addProcessListeners(),this.setTTLTimer(),this.updateState({status:o.STARTED,containerName:this.options.containerName,pid:this.process.pid,reason:void 0})}catch(t){this.onError(t)}}}async stop(t=""){if(this.canStop()){this.updateState({status:o.STOPPING,reason:t});try{await new Promise((s,i)=>{const e=n("docker",["stop",this.options.containerName]);e.on("exit",r=>{r===0?s():i(new Error(`Docker stop exited with code ${r}`))}),e.on("error",i)})}catch(s){this.onError(s)}finally{await new Promise(s=>{this.process?.kill("SIGTERM"),this.process?.on("exit",()=>{s()})})}}}}export{T as DockerTask};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{exec as o}from"node:child_process";import{promisify as r}from"node:util";const c=r(o);export{c as execAsync};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execAsync as e}from"./exec-async.js";async function o(){try{return await e("docker --version"),!0}catch{return!1}}export{o as isDockerInstalled};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execAsync as r}from"./exec-async.js";async function n(){try{return await r("docker info"),!0}catch{return!1}}export{n as isDockerRunning};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{exec as d}from"node:child_process";async function l(n,e={}){return new Promise((u,o)=>{const r=d(`docker pull ${n}`);r.stdout&&e.onStdOut&&r.stdout.on("data",t=>{e.onStdOut(t.toString())}),r.stderr&&e.onStdErr&&r.stderr.on("data",t=>{e.onStdErr(t.toString())}),r.on("exit",t=>{t===0?u():o({code:t,message:t===null?"":`docker pull exited with code ${t}`})}),r.on("error",t=>{o({message:t.message,code:-1})})})}export{l as pullImage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(){return{type:"docker-compose",options:{type:"object",properties:{services:{type:"object",patternProperties:{"^[a-zA-Z0-9_-]+$":{type:"object",properties:{image:{type:"string"},containerName:{type:"string"},command:{type:"string"},ports:{type:"array",items:{type:"string"}},volumes:{type:"array",items:{type:"string"}},environment:{$ref:"#/$defs/Runium_Env"},networks:{type:"array",items:{type:"string"}},dependsOn:{type:"array",items:{type:"string"}},restart:{type:"string",enum:["no","always","on-failure","unless-stopped"]},user:{type:"string"},workdir:{type:"string"},entrypoint:{type:"string"},privileged:{type:"boolean"},expose:{type:"array",items:{type:"string"}},healthcheck:{type:"object",properties:{test:{oneOf:[{type:"string"},{type:"array",items:{type:"string"}}]},interval:{type:"string"},timeout:{type:"string"},retries:{type:"number"},start_period:{type:"string"}},required:["test"],additionalProperties:!1}},required:["image"],additionalProperties:!1}},additionalProperties:!1},networks:{type:"object",patternProperties:{"^[a-zA-Z0-9_-]+$":{type:"object"}},additionalProperties:!1},volumes:{type:"object",patternProperties:{"^[a-zA-Z0-9_-]+$":{type:"object"}},additionalProperties:!1},ttl:{type:"number"},log:{$ref:"#/$defs/Runium_TaskLog"}},required:["services"],additionalProperties:!1}}}export{r as getDockerComposeTaskProjectSchema};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(){return{type:"docker",options:{type:"object",properties:{image:{type:"string"},containerName:{type:"string"},command:{type:"string"},arguments:{type:"array",items:{type:"string"}},ports:{type:"array",items:{type:"string"}},volumes:{type:"array",items:{type:"string"}},env:{$ref:"#/$defs/Runium_Env"},network:{type:"string"},autoRemove:{type:"boolean"},user:{type:"string"},workdir:{type:"string"},entrypoint:{type:"string"},privileged:{type:"boolean"},ttl:{type:"number"},log:{$ref:"#/$defs/Runium_TaskLog"}},required:["image"],additionalProperties:!1}}}export{r as getDockerTaskProjectSchema};
|
package/.eslintrc.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"parser": "@typescript-eslint/parser",
|
|
3
|
-
"extends": [
|
|
4
|
-
"eslint:recommended",
|
|
5
|
-
"plugin:@typescript-eslint/recommended",
|
|
6
|
-
"plugin:prettier/recommended"
|
|
7
|
-
],
|
|
8
|
-
"parserOptions": {
|
|
9
|
-
"ecmaVersion": 2022,
|
|
10
|
-
"sourceType": "module"
|
|
11
|
-
},
|
|
12
|
-
"rules": {
|
|
13
|
-
"@typescript-eslint/no-explicit-any": "warn",
|
|
14
|
-
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
15
|
-
"@typescript-eslint/no-unused-vars": ["error", {
|
|
16
|
-
"argsIgnorePattern": "^(_)$"
|
|
17
|
-
}]
|
|
18
|
-
}
|
|
19
|
-
}
|
package/.prettierrc.json
DELETED
package/build.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
chmodSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from 'node:fs';
|
|
10
|
-
import { resolve } from 'node:path';
|
|
11
|
-
import { build } from 'esbuild';
|
|
12
|
-
|
|
13
|
-
const buildConfig = {
|
|
14
|
-
entryPoints: ['src/**/*.ts'],
|
|
15
|
-
bundle: false,
|
|
16
|
-
platform: 'node',
|
|
17
|
-
target: 'esnext',
|
|
18
|
-
format: 'esm',
|
|
19
|
-
outdir: 'lib',
|
|
20
|
-
sourcemap: false,
|
|
21
|
-
minify: true,
|
|
22
|
-
treeShaking: false,
|
|
23
|
-
plugins: [],
|
|
24
|
-
metafile: true,
|
|
25
|
-
logLevel: 'info',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
async function buildProject() {
|
|
29
|
-
try {
|
|
30
|
-
console.log('🔨 Building project with esbuild...');
|
|
31
|
-
|
|
32
|
-
const libDir = 'lib';
|
|
33
|
-
if (!existsSync(libDir)) {
|
|
34
|
-
mkdirSync(libDir, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const result = await build(buildConfig);
|
|
38
|
-
|
|
39
|
-
if (process.platform !== 'win32') {
|
|
40
|
-
try {
|
|
41
|
-
chmodSync('lib/index.js', '755');
|
|
42
|
-
console.log('🔐 Made lib/index.js executable');
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.warn('⚠️ Could not make file executable:', error.message);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log('🔨 Processing package.json...');
|
|
49
|
-
await processPackageJson();
|
|
50
|
-
|
|
51
|
-
console.log('✅ Build completed successfully!');
|
|
52
|
-
|
|
53
|
-
// Print build stats if in development mode
|
|
54
|
-
if (result.metafile) {
|
|
55
|
-
console.log('\n📊 Build statistics:');
|
|
56
|
-
const outputs = result.metafile.outputs;
|
|
57
|
-
for (const [file, info] of Object.entries(outputs)) {
|
|
58
|
-
console.log(` ${file}: ${(info.bytes / 1024).toFixed(2)} KB`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('❌ Build failed:', error);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function processPackageJson() {
|
|
68
|
-
let originalPackageJson = readFileSync('./package.json', {
|
|
69
|
-
encoding: 'utf-8',
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
let packageJson = JSON.parse(originalPackageJson);
|
|
73
|
-
const { dependencies } = packageJson;
|
|
74
|
-
|
|
75
|
-
delete packageJson.dependencies;
|
|
76
|
-
delete packageJson.devDependencies;
|
|
77
|
-
delete packageJson.scripts;
|
|
78
|
-
delete packageJson.module;
|
|
79
|
-
delete packageJson.main;
|
|
80
|
-
|
|
81
|
-
console.log('✅ Processed package.json', packageJson);
|
|
82
|
-
|
|
83
|
-
packageJson = Object.assign(packageJson, {
|
|
84
|
-
module: './index.js',
|
|
85
|
-
main: './index.js',
|
|
86
|
-
dependencies,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
writeFileSync(
|
|
90
|
-
resolve(import.meta.dirname, 'lib', 'package.json'),
|
|
91
|
-
JSON.stringify(packageJson, null, 2)
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
buildProject();
|
package/src/index.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Plugin } from '@runium/types-plugin';
|
|
2
|
-
import { DockerTask } from './tasks/docker-task.js';
|
|
3
|
-
import { DockerComposeTask } from './tasks/docker-compose-task.js';
|
|
4
|
-
import { getDockerTaskProjectSchema } from './validation/docker-task-project-schema.js';
|
|
5
|
-
import { getDockerComposeTaskProjectSchema } from './validation/docker-compose-task-project-schema.js';
|
|
6
|
-
|
|
7
|
-
export default function (): Plugin {
|
|
8
|
-
return {
|
|
9
|
-
name: 'docker',
|
|
10
|
-
project: {
|
|
11
|
-
tasks: {
|
|
12
|
-
docker: DockerTask,
|
|
13
|
-
'docker-compose': DockerComposeTask,
|
|
14
|
-
},
|
|
15
|
-
validationSchema: {
|
|
16
|
-
tasks: {
|
|
17
|
-
Docker_DockerTask: getDockerTaskProjectSchema(),
|
|
18
|
-
Docker_DockerComposeTask: getDockerComposeTaskProjectSchema(),
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
} as Plugin;
|
|
23
|
-
}
|
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
import { spawn, SpawnOptionsWithoutStdio } from 'node:child_process';
|
|
2
|
-
import { stringify } from 'yaml';
|
|
3
|
-
import {
|
|
4
|
-
DockerTaskBase,
|
|
5
|
-
DockerTaskBaseOptions,
|
|
6
|
-
DockerTaskBaseState,
|
|
7
|
-
} from './docker-task-base.js';
|
|
8
|
-
import { isDockerInstalled } from '../utils/is-docker-installed.js';
|
|
9
|
-
import { isDockerRunning } from '../utils/is-docker-running.js';
|
|
10
|
-
import { pullImage } from '../utils/pull-image.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Docker service configuration
|
|
14
|
-
*/
|
|
15
|
-
export interface DockerComposeService {
|
|
16
|
-
image: string;
|
|
17
|
-
containerName?: string;
|
|
18
|
-
command?: string;
|
|
19
|
-
ports?: string[];
|
|
20
|
-
volumes?: string[];
|
|
21
|
-
environment?: { [key: string]: string | number | boolean };
|
|
22
|
-
networks?: string[];
|
|
23
|
-
dependsOn?: string[];
|
|
24
|
-
restart?: 'no' | 'always' | 'on-failure' | 'unless-stopped';
|
|
25
|
-
user?: string;
|
|
26
|
-
workdir?: string;
|
|
27
|
-
entrypoint?: string | string[];
|
|
28
|
-
privileged?: boolean;
|
|
29
|
-
expose?: string[];
|
|
30
|
-
healthcheck?: {
|
|
31
|
-
test: string | string[];
|
|
32
|
-
interval?: string;
|
|
33
|
-
timeout?: string;
|
|
34
|
-
retries?: number;
|
|
35
|
-
start_period?: string;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Docker compose task options
|
|
41
|
-
*/
|
|
42
|
-
export interface DockerComposeTaskOptions extends DockerTaskBaseOptions {
|
|
43
|
-
services: { [serviceName: string]: DockerComposeService };
|
|
44
|
-
networks?: { [networkName: string]: unknown };
|
|
45
|
-
volumes?: { [volumeName: string]: unknown };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Docker compose task state
|
|
50
|
-
*/
|
|
51
|
-
export type DockerComposeTaskState = DockerTaskBaseState;
|
|
52
|
-
|
|
53
|
-
interface DockerComposeYaml {
|
|
54
|
-
version: string;
|
|
55
|
-
services: Record<string, Record<string, unknown>>;
|
|
56
|
-
networks?: Record<string, unknown>;
|
|
57
|
-
volumes?: Record<string, unknown>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const { TaskStatus } = runium.enum;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Docker compose task class
|
|
64
|
-
*/
|
|
65
|
-
export class DockerComposeTask extends DockerTaskBase<
|
|
66
|
-
DockerComposeTaskOptions,
|
|
67
|
-
DockerComposeTaskState
|
|
68
|
-
> {
|
|
69
|
-
protected readonly projectName: string;
|
|
70
|
-
protected readonly composeFile: string;
|
|
71
|
-
protected readonly version: string = '3.8';
|
|
72
|
-
|
|
73
|
-
// 128 + SIGTERM when stopping docker-compose
|
|
74
|
-
protected correctExitCodes: number[] = [130];
|
|
75
|
-
|
|
76
|
-
constructor(protected readonly options: DockerComposeTaskOptions) {
|
|
77
|
-
super(options);
|
|
78
|
-
|
|
79
|
-
// TODO: unique project name
|
|
80
|
-
this.projectName = `runium-docker-compose-${Date.now()}`;
|
|
81
|
-
this.composeFile = `docker-compose-${this.projectName}.yml`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Generate docker-compose YAML content
|
|
86
|
-
*/
|
|
87
|
-
private generateComposeYaml(): string {
|
|
88
|
-
const compose: DockerComposeYaml = {
|
|
89
|
-
version: this.version,
|
|
90
|
-
services: {},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// services
|
|
94
|
-
for (const [serviceName, service] of Object.entries(
|
|
95
|
-
this.options.services
|
|
96
|
-
)) {
|
|
97
|
-
const serviceConfig: Record<string, unknown> = {
|
|
98
|
-
image: service.image,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
if (service.containerName) {
|
|
102
|
-
serviceConfig.container_name = service.containerName;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (service.command) {
|
|
106
|
-
serviceConfig.command = service.command;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (service.ports && service.ports.length > 0) {
|
|
110
|
-
serviceConfig.ports = service.ports;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (service.volumes && service.volumes.length > 0) {
|
|
114
|
-
serviceConfig.volumes = service.volumes;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (service.environment && Object.keys(service.environment).length > 0) {
|
|
118
|
-
serviceConfig.environment = service.environment;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (service.networks && service.networks.length > 0) {
|
|
122
|
-
serviceConfig.networks = service.networks;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (service.dependsOn && service.dependsOn.length > 0) {
|
|
126
|
-
serviceConfig.depends_on = service.dependsOn;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (service.restart) {
|
|
130
|
-
serviceConfig.restart = service.restart;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (service.user) {
|
|
134
|
-
serviceConfig.user = service.user;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (service.workdir) {
|
|
138
|
-
serviceConfig.working_dir = service.workdir;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (service.entrypoint) {
|
|
142
|
-
serviceConfig.entrypoint = service.entrypoint;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (service.privileged) {
|
|
146
|
-
serviceConfig.privileged = service.privileged;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (service.expose && service.expose.length > 0) {
|
|
150
|
-
serviceConfig.expose = service.expose;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (service.healthcheck) {
|
|
154
|
-
serviceConfig.healthcheck = service.healthcheck;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
compose.services[serviceName] = serviceConfig;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// networks if specified
|
|
161
|
-
if (
|
|
162
|
-
this.options.networks &&
|
|
163
|
-
Object.keys(this.options.networks).length > 0
|
|
164
|
-
) {
|
|
165
|
-
compose.networks = this.options.networks;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// volumes if specified
|
|
169
|
-
if (this.options.volumes && Object.keys(this.options.volumes).length > 0) {
|
|
170
|
-
compose.volumes = this.options.volumes;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// convert to YAML format using yaml package
|
|
174
|
-
return stringify(compose);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Pull all images for services
|
|
179
|
-
*/
|
|
180
|
-
private async pullAllImages(): Promise<void> {
|
|
181
|
-
const images = Object.values(this.options.services).map(
|
|
182
|
-
service => service.image
|
|
183
|
-
);
|
|
184
|
-
const uniqueImages = [...new Set(images)];
|
|
185
|
-
|
|
186
|
-
for (const image of uniqueImages) {
|
|
187
|
-
this.updateState({
|
|
188
|
-
reason: `pull image: ${image}`,
|
|
189
|
-
});
|
|
190
|
-
await pullImage(image, {
|
|
191
|
-
onStdErr: this.onStdErrData.bind(this),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Prepare docker-compose command arguments
|
|
198
|
-
*/
|
|
199
|
-
private prepareDockerComposeArgs(command: string): string[] {
|
|
200
|
-
const args: string[] = ['compose'];
|
|
201
|
-
|
|
202
|
-
args.push('-p', this.projectName);
|
|
203
|
-
args.push('-f', runium.storage.getPath(['docker', this.composeFile]));
|
|
204
|
-
args.push(command);
|
|
205
|
-
|
|
206
|
-
return args;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Start task
|
|
211
|
-
*/
|
|
212
|
-
async start(): Promise<void> {
|
|
213
|
-
if (!this.canStart()) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this.updateState({
|
|
218
|
-
status: TaskStatus.STARTING,
|
|
219
|
-
iteration: this.state.iteration + 1,
|
|
220
|
-
pid: -1,
|
|
221
|
-
exitCode: undefined,
|
|
222
|
-
error: undefined,
|
|
223
|
-
reason: undefined,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
if (!(await isDockerInstalled())) {
|
|
227
|
-
this.onError(new Error('Docker is not installed'));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!(await isDockerRunning())) {
|
|
232
|
-
this.onError(new Error('Docker is not running'));
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
// generate docker-compose yaml file
|
|
238
|
-
const yamlContent = this.generateComposeYaml();
|
|
239
|
-
await runium.storage.write(['docker', this.composeFile], yamlContent);
|
|
240
|
-
|
|
241
|
-
await this.initLogStreams();
|
|
242
|
-
|
|
243
|
-
await this.pullAllImages();
|
|
244
|
-
|
|
245
|
-
// Start docker-compose
|
|
246
|
-
const args = this.prepareDockerComposeArgs('up');
|
|
247
|
-
|
|
248
|
-
this.process = spawn('docker', args, {
|
|
249
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
250
|
-
} as SpawnOptionsWithoutStdio);
|
|
251
|
-
|
|
252
|
-
this.addProcessListeners();
|
|
253
|
-
|
|
254
|
-
this.setTTLTimer();
|
|
255
|
-
|
|
256
|
-
this.updateState({
|
|
257
|
-
status: TaskStatus.STARTED,
|
|
258
|
-
pid: this.process!.pid,
|
|
259
|
-
reason: undefined,
|
|
260
|
-
});
|
|
261
|
-
} catch (error) {
|
|
262
|
-
this.onError(error as Error);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Stop task
|
|
268
|
-
* @param reason
|
|
269
|
-
*/
|
|
270
|
-
async stop(reason: string = ''): Promise<void> {
|
|
271
|
-
if (!this.canStop()) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
this.updateState({
|
|
276
|
-
status: TaskStatus.STOPPING,
|
|
277
|
-
reason,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
// stop docker-compose services
|
|
282
|
-
const args = this.prepareDockerComposeArgs('down');
|
|
283
|
-
|
|
284
|
-
await new Promise<void>((resolve, reject) => {
|
|
285
|
-
const stopProcess = spawn('docker', args);
|
|
286
|
-
stopProcess.on('exit', code => {
|
|
287
|
-
if (code === 0) {
|
|
288
|
-
resolve();
|
|
289
|
-
} else {
|
|
290
|
-
reject(new Error(`Docker compose down exited with code ${code}`));
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
stopProcess.on('error', reject);
|
|
294
|
-
});
|
|
295
|
-
} catch (error) {
|
|
296
|
-
this.onError(error as Error);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
-
import { createWriteStream, WriteStream } from 'node:fs';
|
|
3
|
-
import { mkdir } from 'node:fs/promises';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
-
import { setTimeout } from 'node:timers';
|
|
6
|
-
import * as Runium from '@runium/types-plugin';
|
|
7
|
-
|
|
8
|
-
export const SILENT_EXIT_CODE = -1;
|
|
9
|
-
|
|
10
|
-
const MAX_LISTENERS_COUNT = 50;
|
|
11
|
-
|
|
12
|
-
const { TaskStatus, TaskEvent } = runium.enum;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Docker task base options
|
|
16
|
-
*/
|
|
17
|
-
export interface DockerTaskBaseOptions {
|
|
18
|
-
ttl?: number;
|
|
19
|
-
log?: {
|
|
20
|
-
stdout?: string | null;
|
|
21
|
-
stderr?: string | null;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Docker task base state
|
|
27
|
-
*/
|
|
28
|
-
export interface DockerTaskBaseState extends Runium.RuniumTaskState {
|
|
29
|
-
pid?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Docker task base class
|
|
34
|
-
*/
|
|
35
|
-
export abstract class DockerTaskBase<
|
|
36
|
-
Options extends DockerTaskBaseOptions = DockerTaskBaseOptions,
|
|
37
|
-
State extends DockerTaskBaseState = DockerTaskBaseState,
|
|
38
|
-
> extends runium.class.RuniumTask<Options, State> {
|
|
39
|
-
protected correctExitCodes: number[] = [];
|
|
40
|
-
|
|
41
|
-
protected state: State = {
|
|
42
|
-
status: TaskStatus.IDLE,
|
|
43
|
-
timestamp: Date.now(),
|
|
44
|
-
iteration: 0,
|
|
45
|
-
pid: -1,
|
|
46
|
-
} as State;
|
|
47
|
-
|
|
48
|
-
protected process: ChildProcessWithoutNullStreams | null = null;
|
|
49
|
-
protected stdoutStream: WriteStream | null = null;
|
|
50
|
-
protected stderrStream: WriteStream | null = null;
|
|
51
|
-
protected ttlTimer: NodeJS.Timeout | null = null;
|
|
52
|
-
|
|
53
|
-
constructor(protected readonly options: Options) {
|
|
54
|
-
super(options);
|
|
55
|
-
this.setMaxListeners(MAX_LISTENERS_COUNT);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get task state
|
|
60
|
-
*/
|
|
61
|
-
public getState(): State {
|
|
62
|
-
return { ...this.state };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get task options
|
|
67
|
-
*/
|
|
68
|
-
public getOptions(): Options {
|
|
69
|
-
return { ...this.options };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Check if task can start
|
|
74
|
-
*/
|
|
75
|
-
protected canStart(): boolean {
|
|
76
|
-
const { status } = this.state;
|
|
77
|
-
return (
|
|
78
|
-
status !== TaskStatus.STARTED &&
|
|
79
|
-
status !== TaskStatus.STARTING &&
|
|
80
|
-
status !== TaskStatus.STOPPING
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Check if task can stop
|
|
86
|
-
*/
|
|
87
|
-
protected canStop(): boolean {
|
|
88
|
-
const { status } = this.state;
|
|
89
|
-
return status === TaskStatus.STARTED || status === TaskStatus.STARTING;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Start task
|
|
94
|
-
*/
|
|
95
|
-
abstract start(): Promise<void>;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Stop task
|
|
99
|
-
* @param reason
|
|
100
|
-
*/
|
|
101
|
-
abstract stop(reason: string): Promise<void>;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Restart task
|
|
105
|
-
*/
|
|
106
|
-
async restart(): Promise<void> {
|
|
107
|
-
await this.stop('restart');
|
|
108
|
-
await this.start();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Initialize log streams
|
|
113
|
-
*/
|
|
114
|
-
protected async initLogStreams(): Promise<void> {
|
|
115
|
-
const { stdout = null, stderr = null } = this.options.log || {};
|
|
116
|
-
if (stdout) {
|
|
117
|
-
const stdOutPath = resolve(stdout);
|
|
118
|
-
await mkdir(dirname(stdOutPath), { recursive: true });
|
|
119
|
-
this.stdoutStream = createWriteStream(stdout, {
|
|
120
|
-
flags: 'w',
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
if (stderr) {
|
|
124
|
-
const stdErrPath = resolve(stderr);
|
|
125
|
-
await mkdir(dirname(stdErrPath), { recursive: true });
|
|
126
|
-
this.stderrStream = createWriteStream(stderr, {
|
|
127
|
-
flags: 'w',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Set TTL timer
|
|
134
|
-
*/
|
|
135
|
-
protected setTTLTimer(): void {
|
|
136
|
-
const { ttl } = this.options;
|
|
137
|
-
if (ttl && this.process) {
|
|
138
|
-
this.ttlTimer = setTimeout(() => {
|
|
139
|
-
this.stop('ttl');
|
|
140
|
-
}, ttl);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Add process listeners
|
|
146
|
-
*/
|
|
147
|
-
protected addProcessListeners(): void {
|
|
148
|
-
if (!this.process) return;
|
|
149
|
-
|
|
150
|
-
this.process.stdout?.on('data', (data: Buffer) => this.onStdOutData(data));
|
|
151
|
-
|
|
152
|
-
this.process.stderr?.on('data', (data: Buffer) => this.onStdErrData(data));
|
|
153
|
-
|
|
154
|
-
this.process.on('exit', (code: number | null) => this.onExit(code));
|
|
155
|
-
|
|
156
|
-
this.process.on('error', (error: Error) => this.onError(error));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* On standard output data
|
|
161
|
-
*/
|
|
162
|
-
protected onStdOutData(data: Buffer): void {
|
|
163
|
-
const output = data.toString();
|
|
164
|
-
this.emit(TaskEvent.STDOUT, output);
|
|
165
|
-
if (this.stdoutStream) {
|
|
166
|
-
this.stdoutStream!.write(output);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* On standard error data
|
|
172
|
-
*/
|
|
173
|
-
protected onStdErrData(data: Buffer): void {
|
|
174
|
-
const output = data.toString();
|
|
175
|
-
this.emit(TaskEvent.STDERR, output);
|
|
176
|
-
if (this.stderrStream) {
|
|
177
|
-
this.stderrStream!.write(output);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* On exit
|
|
183
|
-
* @param code
|
|
184
|
-
*/
|
|
185
|
-
protected onExit(code: number | null): void {
|
|
186
|
-
const exitCode =
|
|
187
|
-
code !== null && !this.correctExitCodes.includes(code)
|
|
188
|
-
? code
|
|
189
|
-
: SILENT_EXIT_CODE;
|
|
190
|
-
|
|
191
|
-
this.updateState({
|
|
192
|
-
status:
|
|
193
|
-
exitCode === 0
|
|
194
|
-
? TaskStatus.COMPLETED
|
|
195
|
-
: exitCode === SILENT_EXIT_CODE
|
|
196
|
-
? TaskStatus.STOPPED
|
|
197
|
-
: TaskStatus.FAILED,
|
|
198
|
-
exitCode,
|
|
199
|
-
} as State);
|
|
200
|
-
|
|
201
|
-
this.cleanup();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* On error
|
|
206
|
-
*/
|
|
207
|
-
protected onError(error: Error): void {
|
|
208
|
-
const code = ((error as { code?: string })?.code ?? null) as number | null;
|
|
209
|
-
this.updateState({
|
|
210
|
-
status: code === null ? TaskStatus.STOPPED : TaskStatus.FAILED,
|
|
211
|
-
reason: error.message || (code ?? ''),
|
|
212
|
-
} as State);
|
|
213
|
-
|
|
214
|
-
this.cleanup();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Update task state
|
|
219
|
-
*/
|
|
220
|
-
protected updateState(state: Partial<State>): void {
|
|
221
|
-
const newState = { ...this.state, ...state, timestamp: Date.now() };
|
|
222
|
-
this.state = Object.fromEntries(
|
|
223
|
-
Object.entries(newState).filter(([_, value]) => {
|
|
224
|
-
return value !== undefined;
|
|
225
|
-
})
|
|
226
|
-
) as unknown as State;
|
|
227
|
-
this.emit(TaskEvent.STATE_CHANGE, this.getState());
|
|
228
|
-
this.emit(this.state.status, this.getState());
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Cleanup
|
|
233
|
-
*/
|
|
234
|
-
protected cleanup(): void {
|
|
235
|
-
if (this.ttlTimer) {
|
|
236
|
-
clearTimeout(this.ttlTimer);
|
|
237
|
-
this.ttlTimer = null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (this.process) {
|
|
241
|
-
this.process.removeAllListeners();
|
|
242
|
-
this.process.stdout?.removeAllListeners();
|
|
243
|
-
this.process.stderr?.removeAllListeners();
|
|
244
|
-
this.process = null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (this.stdoutStream) {
|
|
248
|
-
this.stdoutStream.end();
|
|
249
|
-
this.stdoutStream = null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (this.stderrStream) {
|
|
253
|
-
this.stderrStream.end();
|
|
254
|
-
this.stderrStream = null;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
package/src/tasks/docker-task.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { spawn, SpawnOptionsWithoutStdio } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
DockerTaskBase,
|
|
4
|
-
DockerTaskBaseOptions,
|
|
5
|
-
DockerTaskBaseState,
|
|
6
|
-
} from './docker-task-base.js';
|
|
7
|
-
import { isDockerInstalled } from '../utils/is-docker-installed.js';
|
|
8
|
-
import { isDockerRunning } from '../utils/is-docker-running.js';
|
|
9
|
-
import { pullImage } from '../utils/pull-image.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Docker task options
|
|
13
|
-
*/
|
|
14
|
-
export interface DockerTaskOptions extends DockerTaskBaseOptions {
|
|
15
|
-
// TODO: support more options
|
|
16
|
-
// env-file, expose, healthchecking, mount
|
|
17
|
-
image: string;
|
|
18
|
-
containerName?: string;
|
|
19
|
-
command?: string;
|
|
20
|
-
arguments?: string[];
|
|
21
|
-
ports?: string[];
|
|
22
|
-
volumes?: string[];
|
|
23
|
-
env?: { [key: string]: string | number | boolean };
|
|
24
|
-
network?: string;
|
|
25
|
-
autoRemove?: boolean;
|
|
26
|
-
user?: string;
|
|
27
|
-
workdir?: string;
|
|
28
|
-
entrypoint?: string;
|
|
29
|
-
privileged?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Docker task state
|
|
34
|
-
*/
|
|
35
|
-
export interface DockerTaskState extends DockerTaskBaseState {
|
|
36
|
-
containerName?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const { TaskStatus } = runium.enum;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Docker task class
|
|
43
|
-
*/
|
|
44
|
-
export class DockerTask extends DockerTaskBase<
|
|
45
|
-
DockerTaskOptions,
|
|
46
|
-
DockerTaskState
|
|
47
|
-
> {
|
|
48
|
-
// 128 + SIGKILL when stopping docker container
|
|
49
|
-
protected correctExitCodes: number[] = [137];
|
|
50
|
-
|
|
51
|
-
constructor(protected readonly options: DockerTaskOptions) {
|
|
52
|
-
super(options);
|
|
53
|
-
|
|
54
|
-
this.options.containerName ??= `runium-${this.options.image.replaceAll('/', '-').replaceAll(':', '-')}-${Date.now()}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Prepare docker run command arguments
|
|
59
|
-
*/
|
|
60
|
-
private prepareDockerArgs(): string[] {
|
|
61
|
-
const args: string[] = ['run'];
|
|
62
|
-
|
|
63
|
-
if (this.options.autoRemove !== false) {
|
|
64
|
-
args.push('--rm');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (this.options.containerName) {
|
|
68
|
-
args.push('--name', this.options.containerName);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (this.options.ports) {
|
|
72
|
-
this.options.ports.forEach(port => {
|
|
73
|
-
args.push('-p', port);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (this.options.volumes) {
|
|
78
|
-
this.options.volumes.forEach(volume => {
|
|
79
|
-
args.push('-v', volume);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (this.options.env) {
|
|
84
|
-
Object.entries(this.options.env).forEach(([key, value]) => {
|
|
85
|
-
args.push('-e', `${key}=${value}`);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (this.options.network) {
|
|
90
|
-
args.push('--network', this.options.network);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (this.options.user) {
|
|
94
|
-
args.push('--user', this.options.user);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (this.options.workdir) {
|
|
98
|
-
args.push('--workdir', this.options.workdir);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (this.options.entrypoint) {
|
|
102
|
-
args.push('--entrypoint', this.options.entrypoint);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (this.options.privileged) {
|
|
106
|
-
args.push('--privileged');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
args.push(this.options.image);
|
|
110
|
-
|
|
111
|
-
if (this.options.command) {
|
|
112
|
-
args.push(this.options.command);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (this.options.arguments) {
|
|
116
|
-
args.push(...this.options.arguments);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return args;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Start task
|
|
124
|
-
*/
|
|
125
|
-
async start(): Promise<void> {
|
|
126
|
-
if (!this.canStart()) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.updateState({
|
|
131
|
-
status: TaskStatus.STARTING,
|
|
132
|
-
iteration: this.state.iteration + 1,
|
|
133
|
-
pid: -1,
|
|
134
|
-
containerName: undefined,
|
|
135
|
-
exitCode: undefined,
|
|
136
|
-
error: undefined,
|
|
137
|
-
reason: undefined,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!(await isDockerInstalled())) {
|
|
141
|
-
this.onError(new Error('Docker is not installed'));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!(await isDockerRunning())) {
|
|
146
|
-
this.onError(new Error('Docker is not running'));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
await this.initLogStreams();
|
|
152
|
-
|
|
153
|
-
this.updateState({
|
|
154
|
-
reason: `pull image: ${this.options.image}`,
|
|
155
|
-
});
|
|
156
|
-
await pullImage(this.options.image, {
|
|
157
|
-
onStdErr: this.onStdErrData.bind(this),
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const args = this.prepareDockerArgs();
|
|
161
|
-
|
|
162
|
-
this.process = spawn('docker', args, {
|
|
163
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
164
|
-
} as SpawnOptionsWithoutStdio);
|
|
165
|
-
|
|
166
|
-
this.addProcessListeners();
|
|
167
|
-
|
|
168
|
-
this.setTTLTimer();
|
|
169
|
-
|
|
170
|
-
this.updateState({
|
|
171
|
-
status: TaskStatus.STARTED,
|
|
172
|
-
containerName: this.options.containerName,
|
|
173
|
-
pid: this.process!.pid,
|
|
174
|
-
reason: undefined,
|
|
175
|
-
});
|
|
176
|
-
} catch (error) {
|
|
177
|
-
this.onError(error as Error);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Stop task
|
|
183
|
-
* @param reason
|
|
184
|
-
*/
|
|
185
|
-
async stop(reason: string = ''): Promise<void> {
|
|
186
|
-
if (!this.canStop()) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
this.updateState({
|
|
191
|
-
status: TaskStatus.STOPPING,
|
|
192
|
-
reason,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
await new Promise<void>((resolve, reject) => {
|
|
197
|
-
const stopProcess = spawn('docker', [
|
|
198
|
-
'stop',
|
|
199
|
-
this.options.containerName!,
|
|
200
|
-
]);
|
|
201
|
-
stopProcess.on('exit', code => {
|
|
202
|
-
if (code === 0) {
|
|
203
|
-
resolve();
|
|
204
|
-
} else {
|
|
205
|
-
reject(new Error(`Docker stop exited with code ${code}`));
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
stopProcess.on('error', reject);
|
|
209
|
-
});
|
|
210
|
-
} catch (error) {
|
|
211
|
-
this.onError(error as Error);
|
|
212
|
-
} finally {
|
|
213
|
-
await new Promise<void>(resolve => {
|
|
214
|
-
this.process?.kill('SIGTERM' as NodeJS.Signals);
|
|
215
|
-
this.process?.on('exit', () => {
|
|
216
|
-
resolve();
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
package/src/utils/exec-async.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { execAsync } from './exec-async.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Check if Docker is installed
|
|
5
|
-
*/
|
|
6
|
-
export async function isDockerInstalled(): Promise<boolean> {
|
|
7
|
-
try {
|
|
8
|
-
await execAsync('docker --version');
|
|
9
|
-
return true;
|
|
10
|
-
} catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
package/src/utils/pull-image.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { exec } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
interface PullImageOptions {
|
|
4
|
-
onStdOut?: (output: Buffer) => void;
|
|
5
|
-
onStdErr?: (output: Buffer) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Pull a Docker image
|
|
10
|
-
* @param imageName
|
|
11
|
-
* @param options
|
|
12
|
-
*/
|
|
13
|
-
export async function pullImage(
|
|
14
|
-
imageName: string,
|
|
15
|
-
options: PullImageOptions = {}
|
|
16
|
-
): Promise<void> {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
const child = exec(`docker pull ${imageName}`);
|
|
19
|
-
|
|
20
|
-
if (child.stdout && options.onStdOut) {
|
|
21
|
-
child.stdout.on('data', data => {
|
|
22
|
-
options.onStdOut!(data.toString());
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (child.stderr && options.onStdErr) {
|
|
27
|
-
child.stderr.on('data', data => {
|
|
28
|
-
options.onStdErr!(data.toString());
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
child.on('exit', (code: number | null) => {
|
|
33
|
-
if (code === 0) {
|
|
34
|
-
resolve();
|
|
35
|
-
} else {
|
|
36
|
-
reject({
|
|
37
|
-
code,
|
|
38
|
-
message: code === null ? '' : `docker pull exited with code ${code}`,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
child.on('error', (error: Error) => {
|
|
44
|
-
reject({ message: error.message, code: -1 });
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { ProjectSchemaExtensionTask } from '@runium/types-plugin';
|
|
2
|
-
|
|
3
|
-
export function getDockerComposeTaskProjectSchema(): ProjectSchemaExtensionTask {
|
|
4
|
-
return {
|
|
5
|
-
type: 'docker-compose',
|
|
6
|
-
options: {
|
|
7
|
-
type: 'object',
|
|
8
|
-
properties: {
|
|
9
|
-
services: {
|
|
10
|
-
type: 'object',
|
|
11
|
-
patternProperties: {
|
|
12
|
-
'^[a-zA-Z0-9_-]+$': {
|
|
13
|
-
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
image: {
|
|
16
|
-
type: 'string',
|
|
17
|
-
},
|
|
18
|
-
containerName: {
|
|
19
|
-
type: 'string',
|
|
20
|
-
},
|
|
21
|
-
command: {
|
|
22
|
-
type: 'string',
|
|
23
|
-
},
|
|
24
|
-
ports: {
|
|
25
|
-
type: 'array',
|
|
26
|
-
items: {
|
|
27
|
-
type: 'string',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
volumes: {
|
|
31
|
-
type: 'array',
|
|
32
|
-
items: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
environment: {
|
|
37
|
-
$ref: '#/$defs/Runium_Env',
|
|
38
|
-
},
|
|
39
|
-
networks: {
|
|
40
|
-
type: 'array',
|
|
41
|
-
items: {
|
|
42
|
-
type: 'string',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
dependsOn: {
|
|
46
|
-
type: 'array',
|
|
47
|
-
items: {
|
|
48
|
-
type: 'string',
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
restart: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
enum: ['no', 'always', 'on-failure', 'unless-stopped'],
|
|
54
|
-
},
|
|
55
|
-
user: {
|
|
56
|
-
type: 'string',
|
|
57
|
-
},
|
|
58
|
-
workdir: {
|
|
59
|
-
type: 'string',
|
|
60
|
-
},
|
|
61
|
-
entrypoint: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
},
|
|
64
|
-
privileged: {
|
|
65
|
-
type: 'boolean',
|
|
66
|
-
},
|
|
67
|
-
expose: {
|
|
68
|
-
type: 'array',
|
|
69
|
-
items: {
|
|
70
|
-
type: 'string',
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
healthcheck: {
|
|
74
|
-
type: 'object',
|
|
75
|
-
properties: {
|
|
76
|
-
test: {
|
|
77
|
-
oneOf: [
|
|
78
|
-
{ type: 'string' },
|
|
79
|
-
{
|
|
80
|
-
type: 'array',
|
|
81
|
-
items: { type: 'string' },
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
interval: {
|
|
86
|
-
type: 'string',
|
|
87
|
-
},
|
|
88
|
-
timeout: {
|
|
89
|
-
type: 'string',
|
|
90
|
-
},
|
|
91
|
-
retries: {
|
|
92
|
-
type: 'number',
|
|
93
|
-
},
|
|
94
|
-
start_period: {
|
|
95
|
-
type: 'string',
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
required: ['test'],
|
|
99
|
-
additionalProperties: false,
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
required: ['image'],
|
|
103
|
-
additionalProperties: false,
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
additionalProperties: false,
|
|
107
|
-
},
|
|
108
|
-
networks: {
|
|
109
|
-
type: 'object',
|
|
110
|
-
patternProperties: {
|
|
111
|
-
'^[a-zA-Z0-9_-]+$': {
|
|
112
|
-
type: 'object',
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
additionalProperties: false,
|
|
116
|
-
},
|
|
117
|
-
volumes: {
|
|
118
|
-
type: 'object',
|
|
119
|
-
patternProperties: {
|
|
120
|
-
'^[a-zA-Z0-9_-]+$': {
|
|
121
|
-
type: 'object',
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
additionalProperties: false,
|
|
125
|
-
},
|
|
126
|
-
ttl: {
|
|
127
|
-
type: 'number',
|
|
128
|
-
},
|
|
129
|
-
log: {
|
|
130
|
-
$ref: '#/$defs/Runium_TaskLog',
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
required: ['services'],
|
|
134
|
-
additionalProperties: false,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { ProjectSchemaExtensionTask } from '@runium/types-plugin';
|
|
2
|
-
|
|
3
|
-
export function getDockerTaskProjectSchema(): ProjectSchemaExtensionTask {
|
|
4
|
-
return {
|
|
5
|
-
type: 'docker',
|
|
6
|
-
options: {
|
|
7
|
-
type: 'object',
|
|
8
|
-
properties: {
|
|
9
|
-
image: {
|
|
10
|
-
type: 'string',
|
|
11
|
-
},
|
|
12
|
-
containerName: {
|
|
13
|
-
type: 'string',
|
|
14
|
-
},
|
|
15
|
-
command: {
|
|
16
|
-
type: 'string',
|
|
17
|
-
},
|
|
18
|
-
arguments: {
|
|
19
|
-
type: 'array',
|
|
20
|
-
items: {
|
|
21
|
-
type: 'string',
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
ports: {
|
|
25
|
-
type: 'array',
|
|
26
|
-
items: {
|
|
27
|
-
type: 'string',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
volumes: {
|
|
31
|
-
type: 'array',
|
|
32
|
-
items: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
env: {
|
|
37
|
-
$ref: '#/$defs/Runium_Env',
|
|
38
|
-
},
|
|
39
|
-
network: {
|
|
40
|
-
type: 'string',
|
|
41
|
-
},
|
|
42
|
-
autoRemove: {
|
|
43
|
-
type: 'boolean',
|
|
44
|
-
},
|
|
45
|
-
user: {
|
|
46
|
-
type: 'string',
|
|
47
|
-
},
|
|
48
|
-
workdir: {
|
|
49
|
-
type: 'string',
|
|
50
|
-
},
|
|
51
|
-
entrypoint: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
},
|
|
54
|
-
privileged: {
|
|
55
|
-
type: 'boolean',
|
|
56
|
-
},
|
|
57
|
-
ttl: {
|
|
58
|
-
type: 'number',
|
|
59
|
-
},
|
|
60
|
-
log: {
|
|
61
|
-
$ref: '#/$defs/Runium_TaskLog',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
required: ['image'],
|
|
65
|
-
additionalProperties: false,
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"baseUrl": ".",
|
|
4
|
-
"target": "esnext",
|
|
5
|
-
"outDir": "./lib",
|
|
6
|
-
"rootDir": "src",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"module": "esnext",
|
|
11
|
-
"moduleResolution": "node",
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"declaration": false,
|
|
14
|
-
"sourceMap": false,
|
|
15
|
-
"strictNullChecks": true,
|
|
16
|
-
"removeComments": true,
|
|
17
|
-
"allowSyntheticDefaultImports": true,
|
|
18
|
-
"types": ["node", "@runium/types-plugin"]
|
|
19
|
-
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
21
|
-
"exclude": ["node_modules", "dist"]
|
|
22
|
-
}
|