From fa3a2b226702f1327d9d6a6f866b0e934829041e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 14 Mar 2024 15:44:17 +0100 Subject: [PATCH 1/4] fix(containers): ensure containers are destroyed when wing console is interrupted --- containers/package-lock.json | 4 +- containers/package.json | 2 +- containers/utils.js | 25 +++-- containers/utils.w | 15 ++- containers/workload.sim.w | 176 ++++++++++++++++++----------------- 5 files changed, 127 insertions(+), 95 deletions(-) diff --git a/containers/package-lock.json b/containers/package-lock.json index 5a1aa38e..ea40dc32 100644 --- a/containers/package-lock.json +++ b/containers/package-lock.json @@ -1,12 +1,12 @@ { "name": "@winglibs/containers", - "version": "0.0.21", + "version": "0.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@winglibs/containers", - "version": "0.0.21", + "version": "0.0.23", "license": "MIT", "dependencies": { "@cdktf/provider-aws": "^18.0.5", diff --git a/containers/package.json b/containers/package.json index 7935a6b6..500c357e 100644 --- a/containers/package.json +++ b/containers/package.json @@ -1,6 +1,6 @@ { "name": "@winglibs/containers", - "version": "0.0.22", + "version": "0.0.23", "description": "Container support for Wing", "repository": { "type": "git", diff --git a/containers/utils.js b/containers/utils.js index 0481b993..7c7873b0 100644 --- a/containers/utils.js +++ b/containers/utils.js @@ -1,8 +1,21 @@ const child_process = require("child_process"); -const fs = require('fs'); -const crypto = require('crypto'); -const glob = require('glob'); -const path = require('path'); +const fs = require("fs"); +const crypto = require("crypto"); +const glob = require("glob"); +const path = require("path"); + +exports.spawn = async (options) => { + const child = child_process.spawn(options.command, options.arguments, { + cwd: options.cwd, + stdio: options.stdio, + }); + + return { + kill() { + child.kill("SIGINT"); + }, + }; +}; exports.shell = async function (command, args, cwd) { return new Promise((resolve, reject) => { @@ -29,8 +42,8 @@ exports.dirname = function() { return __dirname; }; -exports.contentHash = function(patterns, cwd) { - const hash = crypto.createHash('md5'); +exports.contentHash = function (patterns, cwd) { + const hash = crypto.createHash("md5"); const files = glob.sync(patterns, { nodir: true, cwd }); for (const f of files) { const data = fs.readFileSync(path.join(cwd, f)); diff --git a/containers/utils.w b/containers/utils.w index 1357b3b7..d945accb 100644 --- a/containers/utils.w +++ b/containers/utils.w @@ -1,7 +1,18 @@ bring "./api.w" as api; bring fs; +interface Process { + inflight kill(): void; +} + +struct SpawnOptions { + command: str; + arguments: Array; + stdio: str?; +} + pub class Util { + extern "./utils.js" pub static inflight spawn(options: SpawnOptions): Process; extern "./utils.js" pub static inflight shell(command: str, args: Array, cwd: str?): str; extern "./utils.js" pub static contentHash(files: Array, cwd: str): str; extern "./utils.js" pub static dirname(): str; @@ -22,9 +33,9 @@ pub class Util { if !Util.isPath(props.image) { return nil; } - + let sources = props.sources ?? ["**/*"]; let imageDir = props.image; return props.sourceHash ?? Util.contentHash(sources, imageDir); } -} \ No newline at end of file +} diff --git a/containers/workload.sim.w b/containers/workload.sim.w index 2870b3a4..63dd42ba 100644 --- a/containers/workload.sim.w +++ b/containers/workload.sim.w @@ -18,7 +18,7 @@ pub class Workload_sim { imageTag: str; public: bool; state: sim.State; - + new(props: api.WorkloadProps) { this.appDir = utils.entrypointDir(this); this.props = props; @@ -51,108 +51,116 @@ pub class Workload_sim { } let s = new cloud.Service(inflight () => { - this.start(); - return () => { this.stop(); }; - }); - - std.Node.of(s).hidden = true; - std.Node.of(this.state).hidden = true; - } + log("starting workload..."); + + let opts = this.props; + + // if this a reference to a local directory, build the image from a docker file + if utils.isPathInflight(opts.image) { + // check if the image is already built + try { + utils.shell("docker", ["inspect", this.imageTag]); + log("image {this.imageTag} already exists"); + } catch { + log("building locally from {opts.image} and tagging {this.imageTag}..."); + utils.shell("docker", ["build", "-t", this.imageTag, opts.image], this.appDir); + } + } else { + try { + utils.shell("docker", ["inspect", this.imageTag]); + log("image {this.imageTag} already exists"); + } catch { + log("pulling {this.imageTag}"); + utils.shell("docker", ["pull", this.imageTag]); + } + } - inflight start(): void { - log("starting workload..."); + // start the new container + let dockerRun = MutArray[]; + dockerRun.push("run"); + dockerRun.push("--rm"); - let opts = this.props; + let name = util.uuidv4(); + dockerRun.push("--name", name); - // if this a reference to a local directory, build the image from a docker file - if utils.isPathInflight(opts.image) { - // check if the image is already built - try { - utils.shell("docker", ["inspect", this.imageTag]); - log("image {this.imageTag} already exists"); - } catch { - log("building locally from {opts.image} and tagging {this.imageTag}..."); - utils.shell("docker", ["build", "-t", this.imageTag, opts.image], this.appDir); + if let port = opts.port { + dockerRun.push("-p"); + dockerRun.push("{port}"); } - } else { - try { - utils.shell("docker", ["inspect", this.imageTag]); - log("image {this.imageTag} already exists"); - } catch { - log("pulling {this.imageTag}"); - utils.shell("docker", ["pull", this.imageTag]); - } - } - - // start the new container - let dockerRun = MutArray[]; - dockerRun.push("run"); - dockerRun.push("--detach"); - if let port = opts.port { - dockerRun.push("-p"); - dockerRun.push("{port}"); - } - - if let env = opts.env { - if env.size() > 0 { - dockerRun.push("-e"); - for k in env.keys() { - dockerRun.push("{k}={env.get(k)!}"); + if let env = opts.env { + if env.size() > 0 { + dockerRun.push("-e"); + for k in env.keys() { + dockerRun.push("{k}={env.get(k)!}"); + } } } - } - dockerRun.push(this.imageTag); + dockerRun.push(this.imageTag); - if let runArgs = this.props.args { - for a in runArgs { - dockerRun.push(a); + if let runArgs = this.props.args { + for a in runArgs { + dockerRun.push(a); + } } - } - log("starting container from image {this.imageTag}"); - log("docker {dockerRun.join(" ")}"); - let containerId = utils.shell("docker", dockerRun.copy()).trim(); - this.state.set(this.containerIdKey, containerId); + log("starting container from image {this.imageTag}"); + log("docker {dockerRun.join(" ")}"); + let container = utils.spawn(command: "docker", arguments: dockerRun.copy()); + this.state.set(this.containerIdKey, name); - log("containerId={containerId}"); + log("containerName={name}"); - let out = Json.parse(utils.shell("docker", ["inspect", containerId])); + let var out: Json? = nil; + let inspected = util.waitUntil(inflight () => { + try { + out = Json.parse(utils.shell("docker", ["inspect", name])); + return true; + } catch { + return false; + } + }, interval: 250ms, timeout: 2s); - if let port = opts.port { - let hostPort = out.tryGetAt(0)?.tryGet("NetworkSettings")?.tryGet("Ports")?.tryGet("{port}/tcp")?.tryGetAt(0)?.tryGet("HostPort")?.tryAsStr(); - if !hostPort? { - throw "Container does not listen to port {port}"; + if inspected == false { + throw "container did not start in time"; } - let publicUrl = "http://localhost:{hostPort!}"; + if let port = opts.port { + let hostPort = out?.tryGetAt(0)?.tryGet("NetworkSettings")?.tryGet("Ports")?.tryGet("{port}/tcp")?.tryGetAt(0)?.tryGet("HostPort")?.tryAsStr(); + if !hostPort? { + throw "Container does not listen to port {port}"; + } - if let k = this.publicUrlKey { - this.state.set(k, publicUrl); - } + let publicUrl = "http://localhost:{hostPort!}"; - if let k = this.internalUrlKey { - this.state.set(k, "http://host.docker.internal:{hostPort!}"); - } + if let k = this.publicUrlKey { + this.state.set(k, publicUrl); + } - if let readiness = opts.readiness { - let readinessUrl = "{publicUrl}{readiness}"; - log("waiting for container to be ready: {readinessUrl}..."); - util.waitUntil(inflight () => { - try { - return http.get(readinessUrl).ok; - } catch { - return false; - } - }, interval: 0.1s); + if let k = this.internalUrlKey { + this.state.set(k, "http://host.docker.internal:{hostPort!}"); + } + + if let readiness = opts.readiness { + let readinessUrl = "{publicUrl}{readiness}"; + log("waiting for container to be ready: {readinessUrl}..."); + util.waitUntil(inflight () => { + try { + return http.get(readinessUrl).ok; + } catch { + return false; + } + }, interval: 0.1s); + } } - } - } - inflight stop() { - let containerId = this.state.get(this.containerIdKey).asStr(); - log("stopping container {containerId}"); - utils.shell("docker", ["rm", "-f", containerId]); + return () => { + container.kill(); + }; + }); + + std.Node.of(s).hidden = true; + std.Node.of(this.state).hidden = true; } -} \ No newline at end of file +} From 9c7169f0ea342ddf89125a3368a68b6b96421f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 14 Mar 2024 16:17:47 +0100 Subject: [PATCH 2/4] wip --- containers/workload.sim.w | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/containers/workload.sim.w b/containers/workload.sim.w index 63dd42ba..2d63f27d 100644 --- a/containers/workload.sim.w +++ b/containers/workload.sim.w @@ -113,18 +113,14 @@ pub class Workload_sim { log("containerName={name}"); let var out: Json? = nil; - let inspected = util.waitUntil(inflight () => { + util.waitUntil(inflight () => { try { out = Json.parse(utils.shell("docker", ["inspect", name])); return true; } catch { return false; } - }, interval: 250ms, timeout: 2s); - - if inspected == false { - throw "container did not start in time"; - } + }, interval: 0.1s); if let port = opts.port { let hostPort = out?.tryGetAt(0)?.tryGet("NetworkSettings")?.tryGet("Ports")?.tryGet("{port}/tcp")?.tryGetAt(0)?.tryGet("HostPort")?.tryAsStr(); @@ -155,6 +151,7 @@ pub class Workload_sim { } } + log("container ready"); return () => { container.kill(); }; From fcd49f842225963896f0db8912cb33a5d667f923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 14 Mar 2024 16:33:31 +0100 Subject: [PATCH 3/4] avoid hanging containers if an exception is thrown --- containers/workload.sim.w | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/containers/workload.sim.w b/containers/workload.sim.w index 2d63f27d..f3177626 100644 --- a/containers/workload.sim.w +++ b/containers/workload.sim.w @@ -112,6 +112,15 @@ pub class Workload_sim { log("containerName={name}"); + return () => { + container.kill(); + }; + }); + std.Node.of(s).hidden = true; + + let s2 = new cloud.Service(inflight () => { + let name = this.state.get(this.containerIdKey).asStr(); + let opts = this.props; let var out: Json? = nil; util.waitUntil(inflight () => { try { @@ -150,14 +159,9 @@ pub class Workload_sim { }, interval: 0.1s); } } + }) as "Port Retrieval"; + std.Node.of(s2).hidden = true; - log("container ready"); - return () => { - container.kill(); - }; - }); - - std.Node.of(s).hidden = true; std.Node.of(this.state).hidden = true; } } From 8c392ddbb35ff938827c1c5bcf79bc969285171f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 14 Mar 2024 17:10:17 +0100 Subject: [PATCH 4/4] use `docker --rm --detach` --- containers/utils.js | 21 ++++----------------- containers/utils.w | 11 ----------- containers/workload.sim.w | 5 +++-- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/containers/utils.js b/containers/utils.js index 7c7873b0..ce74d815 100644 --- a/containers/utils.js +++ b/containers/utils.js @@ -4,19 +4,6 @@ const crypto = require("crypto"); const glob = require("glob"); const path = require("path"); -exports.spawn = async (options) => { - const child = child_process.spawn(options.command, options.arguments, { - cwd: options.cwd, - stdio: options.stdio, - }); - - return { - kill() { - child.kill("SIGINT"); - }, - }; -}; - exports.shell = async function (command, args, cwd) { return new Promise((resolve, reject) => { child_process.execFile(command, args, { cwd }, (error, stdout, stderr) => { @@ -31,14 +18,14 @@ exports.shell = async function (command, args, cwd) { }; exports.entrypointDir = function (scope) { - if (typeof(scope.entrypointDir) == "string") { + if (typeof scope.entrypointDir == "string") { return scope.entrypointDir; } return exports.entrypointDir(scope.node.scope); }; -exports.dirname = function() { +exports.dirname = function () { return __dirname; }; @@ -49,5 +36,5 @@ exports.contentHash = function (patterns, cwd) { const data = fs.readFileSync(path.join(cwd, f)); hash.update(data); } - return hash.digest('hex'); -}; \ No newline at end of file + return hash.digest("hex"); +}; diff --git a/containers/utils.w b/containers/utils.w index d945accb..90143a86 100644 --- a/containers/utils.w +++ b/containers/utils.w @@ -1,18 +1,7 @@ bring "./api.w" as api; bring fs; -interface Process { - inflight kill(): void; -} - -struct SpawnOptions { - command: str; - arguments: Array; - stdio: str?; -} - pub class Util { - extern "./utils.js" pub static inflight spawn(options: SpawnOptions): Process; extern "./utils.js" pub static inflight shell(command: str, args: Array, cwd: str?): str; extern "./utils.js" pub static contentHash(files: Array, cwd: str): str; extern "./utils.js" pub static dirname(): str; diff --git a/containers/workload.sim.w b/containers/workload.sim.w index f3177626..e9528bb9 100644 --- a/containers/workload.sim.w +++ b/containers/workload.sim.w @@ -78,6 +78,7 @@ pub class Workload_sim { // start the new container let dockerRun = MutArray[]; dockerRun.push("run"); + dockerRun.push("--detach"); dockerRun.push("--rm"); let name = util.uuidv4(); @@ -107,13 +108,13 @@ pub class Workload_sim { log("starting container from image {this.imageTag}"); log("docker {dockerRun.join(" ")}"); - let container = utils.spawn(command: "docker", arguments: dockerRun.copy()); + utils.shell("docker", dockerRun.copy()); this.state.set(this.containerIdKey, name); log("containerName={name}"); return () => { - container.kill(); + utils.shell("docker", ["rm", "-f", name]); }; }); std.Node.of(s).hidden = true;