Skip to content

Commit

Permalink
Merge of #6336
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Apr 26, 2024
2 parents e84ccb0 + 5af875c commit ce4184b
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 29 deletions.
4 changes: 1 addition & 3 deletions examples/tests/sdk_tests/container/mount.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ skipPlatforms:
bring sim;
bring util;

// This test was added to check that "wing test" still works when sim.Container is mounted to the state directory

// only relevant in simulator
if util.env("WING_TARGET") == "sim" {
let container = new sim.Container(
Expand All @@ -17,7 +15,7 @@ if util.env("WING_TARGET") == "sim" {
POSTGRES_PASSWORD: "password"
},
containerPort: 5432,
volumes: ["$WING_STATE_DIR:/var/lib/postgresql/data"],
volumes: ["/var/lib/postgresql/data"],
);

test "my test" {
Expand Down
75 changes: 66 additions & 9 deletions libs/wingsdk/src/target-sim/container.inflight.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as fs from "fs";
import { join } from "path";
import { IContainerClient, HOST_PORT_ATTR } from "./container";
import { ContainerAttributes, ContainerSchema } from "./schema-resources";
import { exists } from "./util";
import { isPath, runCommand, shell } from "../shared/misc";
import {
ISimulatorContext,
Expand All @@ -9,17 +12,30 @@ import {
import { Duration, TraceType } from "../std";
import { Util } from "../util";

export const WING_STATE_DIR_ENV = "WING_STATE_DIR";
const STATE_FILENAME = "state.json";

/**
* Contents of the state file for this resource.
*/
interface StateFileContents {
/**
* A mapping of volume paths to the Wing-managed volume names, which will be reused
* across simulator runs.
*/
readonly managedVolumes?: Record<string, string>;
}

export class Container implements IContainerClient, ISimulatorResourceInstance {
private readonly imageTag: string;
private readonly containerName: string;
private _context: ISimulatorContext | undefined;
private managedVolumes: Record<string, string>;

public constructor(private readonly props: ContainerSchema) {
this.imageTag = props.imageTag;

this.containerName = `wing-container-${Util.ulid()}`;
this.managedVolumes = {};
}

private get context(): ISimulatorContext {
Expand All @@ -31,6 +47,14 @@ export class Container implements IContainerClient, ISimulatorResourceInstance {

public async init(context: ISimulatorContext): Promise<ContainerAttributes> {
this._context = context;

// Check for a previous state file to see if there was a port that was previously being used
// if so, try to use it out of convenience
const state: StateFileContents = await this.loadState();
if (state.managedVolumes) {
this.managedVolumes = state.managedVolumes;
}

// if this a reference to a local directory, build the image from a docker file
if (isPath(this.props.image)) {
// check if the image is already built
Expand Down Expand Up @@ -79,7 +103,21 @@ export class Container implements IContainerClient, ISimulatorResourceInstance {

for (const volume of this.props.volumes ?? []) {
dockerRun.push("-v");
dockerRun.push(volume);

// if the user specified an anonymous volume
if (volume.startsWith("/") && !volume.includes(":")) {
// check if we have a managed volume for this path from a previous run
if (this.managedVolumes[volume]) {
const volumeName = this.managedVolumes[volume];
dockerRun.push(`${volumeName}:${volume}`);
} else {
const volumeName = `wing-volume-${Util.ulid()}`;
dockerRun.push(`${volumeName}:${volume}`);
this.managedVolumes[volume] = volumeName;
}
} else {
dockerRun.push(volume);
}
}

dockerRun.push(this.imageTag);
Expand All @@ -91,12 +129,7 @@ export class Container implements IContainerClient, ISimulatorResourceInstance {
this.log(`starting container from image ${this.imageTag}`);
this.log(`docker ${dockerRun.join(" ")}`);

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

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

Expand Down Expand Up @@ -137,7 +170,31 @@ export class Container implements IContainerClient, ISimulatorResourceInstance {
await runCommand("docker", ["rm", "-f", this.containerName]);
}

public async save(): Promise<void> {}
public async save(): Promise<void> {
await this.saveState({ managedVolumes: this.managedVolumes });
}

private async loadState(): Promise<StateFileContents> {
const stateFileExists = await exists(
join(this.context.statedir, STATE_FILENAME)
);
if (stateFileExists) {
const stateFileContents = await fs.promises.readFile(
join(this.context.statedir, STATE_FILENAME),
"utf-8"
);
return JSON.parse(stateFileContents);
} else {
return {};
}
}

private async saveState(state: StateFileContents): Promise<void> {
fs.writeFileSync(
join(this.context.statedir, STATE_FILENAME),
JSON.stringify(state)
);
}

public async plan() {
return UpdatePlan.AUTO;
Expand Down
8 changes: 4 additions & 4 deletions libs/wingsdk/src/target-sim/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,19 @@ 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`:
To retain the state of a container across console restarts, you can mount an anonymous volume:

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

`$WING_STATE_DIR` is a directory that is unique to that `sim.Container` instance.
Wing will automatically name each unnamed volume in `volumes`, and reuse the named
volumes across console restarts.

## API

Expand Down
20 changes: 13 additions & 7 deletions libs/wingsdk/test/target-sim/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,14 @@ test("simple container with a volume", async () => {
await sim.stop();
});

test("container can mount a volume to the state directory", async () => {
test("anonymous volume can be reused across restarts", 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"],
volumes: ["/tmp"],
});

new Function(
Expand All @@ -204,11 +204,17 @@ test("container can mount a volume to the state directory", async () => {

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"]);
expect(response?.split("\n").filter((s) => s.endsWith(".txt"))).toEqual([
"hello.txt",
]);

await sim.stop();
await sim.start();

const fn2 = sim.getResource("root/Function") as IFunctionClient;
const response2 = await fn2.invoke();
expect(response2?.split("\n").filter((s) => s.endsWith(".txt"))).toEqual([
"hello.txt",
"world.txt",
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ const server = http.createServer((req, res) => {
res.end(fs.readdirSync("/tmp").join("\n"));
});

fs.writeFileSync("/tmp/hello.txt", "Hello, World!", "utf8");
if (!fs.existsSync("/tmp/hello.txt")) {
// on the first run, create a file
fs.writeFileSync("/tmp/hello.txt", "Hello, World!", "utf8");
} else {
// on the second run, create a different file
fs.writeFileSync("/tmp/world.txt", "Hello, World!", "utf8");
}

console.log("listening on port 3000");
server.listen(3000);
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# [mount.test.w](../../../../../../examples/tests/sdk_tests/container/mount.test.w) | test | sim

## stderr.log
```log
Warning: unable to clean up test directory: Error: EACCES: permission denied, scandir 'target/test/mount.test.wsim/.state/<STATE_FILE>'
```

## stdout.log
```log
[INFO] my test | dummy test
Expand Down

0 comments on commit ce4184b

Please sign in to comment.