Skip to content

Commit

Permalink
feat(sdk): support mounting sim.Container to state directories (#6295)
Browse files Browse the repository at this point in the history
To support retaining the state of `sim.Container` across separate Wing Console sessions, this PR adds the capability to reference the resource's state directory through the `WING_STATE_DIR` environment variable. This will be used by the `postgres` winglib to persist state through a mounted volume:

```js
let container = new sim.Container(
  name: "postgres",
  image: image,
  env: {
    POSTGRES_PASSWORD: "password"
  },
  volumes: ["$WING_STATE_DIR/pgdata:/var/lib/postgresql/data"],
  containerPort: 5432
);
```

Related to #6284

## Checklist

- [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [x] Description explains motivation and solution
- [x] Tests added (always)
- [ ] Docs updated (only required for features)
- [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
Chriscbr committed Apr 22, 2024
1 parent eca7c9b commit ec7e177
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 4 deletions.
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;
}

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"));
});

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

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

0 comments on commit ec7e177

Please sign in to comment.