Skip to content

Commit

Permalink
Support for hostIp
Browse files Browse the repository at this point in the history
  • Loading branch information
cleve-fauna committed Dec 12, 2024
1 parent 391d12e commit 527b3e1
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 14 deletions.
6 changes: 6 additions & 0 deletions src/commands/local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ async function startLocal(argv) {
await ensureContainerRunning({
imageName: argv.image,
containerName: argv.name,
hostIp: argv.hostIp,
hostPort: argv.hostPort,
containerPort: argv.containerPort,
pull: argv.pull,
Expand Down Expand Up @@ -38,6 +39,11 @@ function buildLocalCommand(yargs) {
type: "number",
default: 8443,
},
hostIp: {
describe: `The IP address to bind the container's exposed port on the host.`,
type: "string",
default: "0.0.0.0",
},
interval: {
describe:
"The interval (in milliseconds) between health check attempts. Determines how often the CLI checks if the Fauna container is ready.",
Expand Down
2 changes: 2 additions & 0 deletions src/config/setup-container.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs";
import * as fsp from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { exit } from "node:process";
Expand Down Expand Up @@ -60,6 +61,7 @@ export const injectables = {
fetch: awilix.asValue(fetchWrapper),
fs: awilix.asValue(fs),
fsp: awilix.asValue(fsp),
net: awilix.asValue(net),
dirname: awilix.asValue(path.dirname),
normalize: awilix.asValue(path.normalize),
homedir: awilix.asValue(os.homedir),
Expand Down
3 changes: 3 additions & 0 deletions src/config/setup-test-container.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import { PassThrough } from "node:stream";

Expand Down Expand Up @@ -43,6 +44,7 @@ export function setupTestContainer() {

const thingsToManuallyMock = automock(container);
const customfs = stub({ ...fs });
const customNet = stub({ ...net });
// this is a mock used by the default profile behavior
customfs.readdirSync.withArgs(process.cwd()).returns([]);

Expand All @@ -58,6 +60,7 @@ export function setupTestContainer() {
// real implementation
parseYargs: awilix.asValue(spy(parseYargs)),
fs: awilix.asValue(customfs),
net: awilix.asValue(customNet),
homedir: awilix.asValue(
stub().returns(path.join(__dirname, "../../test/test-homedir")),
),
Expand Down
56 changes: 51 additions & 5 deletions src/lib/docker-containers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const IMAGE_NAME = "fauna/faunadb:latest";
* Ensures the container is running
* @param {string} imageName The name of the image to create the container from
* @param {string} containerName The name of the container to start
* @param {string} hostIp The IP address to bind the container's exposed port on the host.
* @param {number} hostPort The port on the host machine mapped to the container's port
* @param {number} containerPort The port inside the container Fauna listens on
* @param {boolean} pull Whether to pull the latest image
Expand All @@ -17,6 +18,7 @@ const IMAGE_NAME = "fauna/faunadb:latest";
*/
export async function ensureContainerRunning({
containerName,
hostIp,
hostPort,
containerPort,
pull,
Expand All @@ -30,14 +32,15 @@ export async function ensureContainerRunning({
const logStream = await startContainer({
imageName: IMAGE_NAME,
containerName,
hostIp,
hostPort,
containerPort,
});
logger.stderr(
`[StartContainer] Container '${containerName}' started. Monitoring HealthCheck for readiness.`,
);
await waitForHealthCheck({
url: `http://localhost:${hostPort}`,
url: `http://${hostIp}:${hostPort}`,
logStream,
interval,
maxAttempts,
Expand Down Expand Up @@ -134,35 +137,75 @@ function writePullProgress(layers, numLines) {
async function findContainer(containerName) {
const docker = container.resolve("docker");
const logger = container.resolve("logger"); // Dependency injection for logger
logger.stderr(
`[GetContainerState] Checking state for container '${containerName}'...`,
);
logger.stderr(`[FindContainer] Looking for container '${containerName}'...`);
const filters = JSON.stringify({ name: [containerName] });
const containers = await docker.listContainers({ all: true, filters });
return containers.length > 0 ? containers[0] : null;
}

/**
* Checks if a port is occupied.
* @param {number} hostPort The port to check
* @param {string} hostIp The IP address to bind the container's exposed port on the host.
* @returns {Promise<boolean>} a promise that resolves to true if the port is occupied, false otherwise.
*/
async function isPortOccupied({ hostPort, hostIp }) {
const net = container.resolve("net");
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once("error", (err) => {
if (err.code === "EADDRINUSE") {
resolve(true); // Port is occupied
} else {
reject(err); // Some other error occurred
}
});

server.on("listening", () => {
server.close(() => {
resolve(false); // Port is free
});
});

server.listen(hostPort, hostIp);
});
}

/**
* Creates a container
* @param {string} imageName The name of the image to create the container from
* @param {string} containerName The name of the container to start
* @param {string} hostIp The IP address to bind the container's exposed port on the host.
* @param {number} hostPort The port on the host machine mapped to the container's port
* @param {number} containerPort The port inside the container Fauna listens on
* @returns {Promise<Object>} The container object
*/
async function createContainer({
imageName,
containerName,
hostIp,
hostPort,
containerPort,
}) {
const docker = container.resolve("docker");
if (await isPortOccupied({ hostIp, hostPort })) {
throw new CommandError(
`The hostPort '${hostPort}' on IP '${hostIp}' is already occupied. \
Please pass a --hostPort other than '${hostPort}'.`,
{ hideHelp: false },
);
}
const dockerContainer = await docker.createContainer({
Image: imageName,
name: containerName,
HostConfig: {
PortBindings: {
[`${containerPort}/tcp`]: [{ HostPort: `${hostPort}` }],
[`${containerPort}/tcp`]: [
{
HostPort: `${hostPort}`,
HostIp: hostIp,
},
],
},
AutoRemove: true,
},
Expand All @@ -177,13 +220,15 @@ async function createContainer({
* Starts a container and returns a log stream if the container is not yet running.
* @param {string} imageName The name of the image to create the container from
* @param {string} containerName The name of the container to start
* @param {string} hostIp The IP address to bind the container's exposed port on the host.
* @param {number} hostPort The port on the host machine mapped to the container's port
* @param {number} containerPort The port inside the container Fauna listens on
* @returns {Promise<Object>} The log stream
*/
async function startContainer({
imageName,
containerName,
hostIp,
hostPort,
containerPort,
}) {
Expand Down Expand Up @@ -226,6 +271,7 @@ async function startContainer({
const dockerContainer = await createContainer({
imageName,
containerName,
hostIp,
hostPort,
containerPort,
});
Expand Down
47 changes: 38 additions & 9 deletions test/local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { run } from "../src/cli.mjs";
import { setupTestContainer } from "../src/config/setup-test-container.mjs";
import { f } from "./helpers.mjs";

describe("ensureContainerRunning", () => {
describe.only("ensureContainerRunning", () => {
let container,
fetch,
logger,
stderrStream,
docker,
logsStub,
serverMock,
startStub,
unpauseStub;

Expand All @@ -26,6 +27,21 @@ describe("ensureContainerRunning", () => {
logsStub = stub();
startStub = stub();
unpauseStub = stub();
// Requested port is free
serverMock = {
close: sinon.stub(),
once: sinon.stub(),
on: sinon.stub(),
listen: sinon.stub(),
};
serverMock.on.withArgs("listening").callsFake((event, callback) => {
callback();
});
serverMock.close.callsFake((callback) => {
if (callback) callback();
});
const net = container.resolve("net");
net.createServer.returns(serverMock);
});

it("Creates and starts a container when none exists", async () => {
Expand Down Expand Up @@ -58,7 +74,12 @@ describe("ensureContainerRunning", () => {
name: "faunadb",
HostConfig: {
PortBindings: {
"8443/tcp": [{ HostPort: "8443" }],
"8443/tcp": [
{
HostPort: "8443",
HostIp: "0.0.0.0",
},
],
},
AutoRemove: true,
},
Expand All @@ -71,7 +92,7 @@ describe("ensureContainerRunning", () => {
);
});

it("The user can control the hostPort and containerPort", async () => {
it("The user can control the hostIp, hostPort, containerPort, and name", async () => {
docker.pull.onCall(0).resolves();
docker.modem.followProgress.callsFake((stream, onFinished) => {
onFinished();
Expand All @@ -87,13 +108,21 @@ describe("ensureContainerRunning", () => {
logs: logsStub,
unpause: unpauseStub,
});
await run("local --hostPort 10 --containerPort 11", container);
await run(
"local --hostPort 10 --containerPort 11 --name Taco --hostIp 127.0.0.1",
container,
);
expect(docker.createContainer).to.have.been.calledWith({
Image: "fauna/faunadb:latest",
name: "faunadb",
name: "Taco",
HostConfig: {
PortBindings: {
"11/tcp": [{ HostPort: "10" }],
"11/tcp": [
{
HostPort: "10",
HostIp: "127.0.0.1",
},
],
},
AutoRemove: true,
},
Expand Down Expand Up @@ -185,7 +214,7 @@ https://support.fauna.com/hc/en-us/requests/new`,
expect(written).to.contain("with HTTP status: '503'");
expect(written).to.contain("with error:");
expect(written).to.contain(
"[HealthCheck] Fauna at http://localhost:8443 is not ready after 3 attempts. Consider increasing --interval or --maxAttempts.",
"[HealthCheck] Fauna at http://0.0.0.0:8443 is not ready after 3 attempts. Consider increasing --interval or --maxAttempts.",
);
expect(written).not.to.contain("An unexpected");
expect(written).not.to.contain("fauna local"); // help text
Expand Down Expand Up @@ -341,10 +370,10 @@ https://support.fauna.com/hc/en-us/requests/new`,
"[StartContainer] Container 'faunadb' started. Monitoring HealthCheck for readiness.",
);
expect(logger.stderr).to.have.been.calledWith(
"[HealthCheck] Waiting for Fauna to be ready at http://localhost:8443...",
"[HealthCheck] Waiting for Fauna to be ready at http://0.0.0.0:8443...",
);
expect(logger.stderr).to.have.been.calledWith(
"[HealthCheck] Fauna is ready at http://localhost:8443",
"[HealthCheck] Fauna is ready at http://0.0.0.0:8443",
);
expect(logger.stderr).to.have.been.calledWith(
"[ContainerReady] Container 'faunadb' is up and healthy.",
Expand Down

0 comments on commit 527b3e1

Please sign in to comment.