Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): support mounting sim.Container to state directories #6295

Merged
merged 3 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion libs/wingsdk/src/shared/misc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ExecFileOptions, execFile } from "child_process";
import { ExecOptions, ExecFileOptions, exec, execFile } from "child_process";
import { readFileSync } from "fs";
import { promisify } from "util";

const execPromise = promisify(exec);
const execFilePromise = promisify(execFile);

export function readJsonSync(file: string) {
Expand Down Expand Up @@ -35,6 +36,19 @@ export async function runCommand(
return stdout;
}

/**
* Just a helpful wrapper around `exec` that returns a promise.
* This will run commands through the shell, while `runCommand` doesn't.
*/
export async function shell(
cmd: string,
args: string[],
options?: ExecOptions
): Promise<any> {
const { stdout } = await execPromise(cmd + " " + args.join(" "), options);
return stdout;
Chriscbr marked this conversation as resolved.
Show resolved Hide resolved
}

export function isPath(s: string) {
s = normalPath(s);
return s.startsWith("./") || s.startsWith("/");
Expand Down
11 changes: 9 additions & 2 deletions libs/wingsdk/src/target-sim/container.inflight.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IContainerClient, HOST_PORT_ATTR } from "./container";
import { ContainerAttributes, ContainerSchema } from "./schema-resources";
import { isPath, runCommand } from "../shared/misc";
import { isPath, runCommand, shell } from "../shared/misc";
import {
ISimulatorContext,
ISimulatorResourceInstance,
Expand All @@ -9,6 +9,8 @@ import {
import { Duration, TraceType } from "../std";
import { Util } from "../util";

export const WING_STATE_DIR_ENV = "WING_STATE_DIR";

export class Container implements IContainerClient, ISimulatorResourceInstance {
private readonly imageTag: string;
private readonly containerName: string;
Expand Down Expand Up @@ -89,7 +91,12 @@ export class Container implements IContainerClient, ISimulatorResourceInstance {
this.log(`starting container from image ${this.imageTag}`);
this.log(`docker ${dockerRun.join(" ")}`);

await runCommand("docker", dockerRun);
await shell("docker", dockerRun, {
env: {
...process.env,
[WING_STATE_DIR_ENV]: this.context.statedir,
},
});

this.log(`containerName=${this.containerName}`);

Expand Down
17 changes: 17 additions & 0 deletions libs/wingsdk/src/target-sim/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ new sim.Container(
);
```

### Retaining state

When the Wing Console is closed, all containers are stopped and removed.
To retain the state of a container across console restarts, you can mount a volume
to a subdirectory of the resource's simulator state directory, which is available through `$WING_STATE_DIR`:

```js
new sim.Container(
name: "my-service",
image: "./my-service",
containerPort: 8080,
volumes: ["$WING_STATE_DIR/volume1:/var/data"],
);
```

`$WING_STATE_DIR` is a directory that is unique to that `sim.Container` instance.

## API

* `name` - a name for the container.
Expand Down
41 changes: 40 additions & 1 deletion libs/wingsdk/test/target-sim/container.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cpSync, writeFileSync } from "fs";
import { cpSync, writeFileSync, readdirSync } from "fs";
import { join, basename } from "path";
import { test, expect } from "vitest";
import { Function, IFunctionClient } from "../../src/cloud";
Expand Down Expand Up @@ -172,3 +172,42 @@ test("simple container with a volume", async () => {

await sim.stop();
});

test("container can mount a volume to the state directory", async () => {
const app = new SimApp();

const c = new Container(app, "Container", {
name: "my-app",
image: join(__dirname, "my-docker-image.mounted-volume"),
containerPort: 3000,
volumes: ["$WING_STATE_DIR:/tmp"],
});

new Function(
app,
"Function",
Testing.makeHandler(
`
async handle() {
const url = "http://localhost:" + this.hostPort;
const res = await fetch(url);
return res.text();
}
`,
{ hostPort: { obj: c.hostPort, ops: [] } }
)
);

const sim = await app.startSimulator();
sim.onTrace({ callback: (trace) => console.log(">", trace.data.message) });

const fn = sim.getResource("root/Function") as IFunctionClient;
const response = await fn.invoke();
expect(response).contains("hello.txt");

const statedir = sim.getResourceStateDir("root/Container");
const files = readdirSync(statedir);
expect(files).toEqual(["hello.txt"]);

await sim.stop();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM node:20.8.0-alpine
EXPOSE 3000
ADD index.js /app/index.js
ENTRYPOINT [ "/app/index.js" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node
const http = require("http");
const fs = require("fs");

process.on("SIGINT", () => {
console.info("Interrupted");
process.exit(0);
});

const server = http.createServer((req, res) => {
console.log(`request received: ${req.method} ${req.url}`);
res.end(fs.readdirSync("/tmp").join("\n"));
github-advanced-security[bot] marked this conversation as resolved.
Dismissed
Show resolved Hide resolved
});

fs.writeFileSync("/tmp/hello.txt", "Hello, World!", "utf8");

console.log("listening on port 3000");
server.listen(3000);
Loading