Skip to content
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
68 changes: 68 additions & 0 deletions .github/workflows/test-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,71 @@ jobs:
EOF
docker buildx build --platform linux/amd64 .
continue-on-error: true

test-driver-opts:
name: Test Driver Options Support
runs-on: blacksmith
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- uses: bufbuild/buf-setup-action@v1
with:
github_token: ${{ github.token }}

- name: Configure npm for buf registry
env:
BUF_TOKEN: ${{ secrets.BUF_TOKEN }}
run: |
npm config set @buf:registry https://buf.build/gen/npm/v1/
npm config set //buf.build/gen/npm/v1/:_authToken $BUF_TOKEN

- name: Build action
run: |
pnpm install
pnpm run build

- name: Test Docker Builder Setup with Driver Options
uses: ./
with:
buildx-version: "v0.23.0"
driver-opts: |
env.TEST_VAR_1=value1
env.TEST_VAR_2=value with spaces
env.BUILDKIT_STEP_LOG_MAX_SIZE=10485760
env.BUILDKIT_STEP_LOG_MAX_SPEED=10485760
continue-on-error: true # Allow failure since we may not have Blacksmith env vars

- name: Verify buildkitd received environment variables
run: |
# Check if buildkitd is running and try to verify environment
if pgrep buildkitd > /dev/null; then
echo "buildkitd is running"
# Try to check the buildkitd log for environment variables
if [ -f /tmp/buildkitd.log ]; then
echo "Checking buildkitd log for configuration..."
# Just verify the log exists and has content
tail -n 50 /tmp/buildkitd.log || true
fi
else
echo "buildkitd is not running (may be expected in test environment)"
fi

- name: Test Docker build with driver-opts configured
run: |
cat > Dockerfile <<EOF
FROM alpine:latest
RUN echo "Testing setup-docker-builder with driver-opts"
EOF
docker buildx build --platform linux/amd64 .
continue-on-error: true
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ inputs:
description: "If true, skip the bbolt database integrity check"
required: false
default: "false"
driver-opts:
description: "List of additional driver-specific options (e.g., env.VARIABLE=value)"
required: false
runs:
using: node20
main: dist/index.js
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions examples/with-driver-opts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Build with OTEL Tracing

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Builder with OTEL
uses: useblacksmith/setup-docker-builder@main
id: setup-builder
with:
driver-opts: |
env.OTEL_TRACES_EXPORTER=otlp
env.OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
env.OTEL_EXPORTER_OTLP_ENDPOINT=https://${{ secrets.DEV_OTEL_AUTH_TOKEN }}-otel.jumpapp.dev
env.OTEL_SERVICE_NAME=buildkitd
env.OTEL_RESOURCE_ATTRIBUTES=workflow.name=deploy_prod,dockerfile=multiple,build.target=release,runner.name=blacksmith-8vcpu-ubuntu-2404

- name: Build Docker image
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myapp:latest \
--push \
.
182 changes: 182 additions & 0 deletions src/driver-opts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import * as core from "@actions/core";
import { startBuildkitd } from "./setup_builder";
import { execa } from "execa";

vi.mock("@actions/core");
vi.mock("execa");
vi.mock("fs", () => ({
promises: {
writeFile: vi.fn().mockResolvedValue(undefined),
},
createWriteStream: vi.fn().mockReturnValue({
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
}),
}));
vi.mock("child_process", () => ({
exec: vi.fn((cmd, callback) => {
// Mock pgrep to return a buildkitd PID
if (cmd.includes("pgrep buildkitd")) {
callback(null, { stdout: "12345\n", stderr: "" });
} else {
callback(null, { stdout: "", stderr: "" });
}
}),
}));

describe("driver-opts parsing", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should parse and set environment variables from driver-opts", async () => {
const mockExeca = vi.mocked(execa);
mockExeca.mockReturnValue({
on: vi.fn(),
stdout: {
pipe: vi.fn(),
},
stderr: {
pipe: vi.fn(),
},
} as unknown as ReturnType<typeof execa>);

const driverOpts = [
"env.OTEL_TRACES_EXPORTER=otlp",
"env.OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf",
"env.OTEL_EXPORTER_OTLP_ENDPOINT=https://example.com",
"env.OTEL_SERVICE_NAME=buildkitd",
];

await startBuildkitd(4, "tcp://127.0.0.1:1234", undefined, driverOpts);

// Verify that execa was called with the correct command
expect(mockExeca).toHaveBeenCalledTimes(1);
const commandCall = mockExeca.mock.calls[0][0] as string;

// Check that environment variables are included in the command with sudo env
expect(commandCall).toContain("sudo env");
expect(commandCall).toContain("OTEL_TRACES_EXPORTER='otlp'");
expect(commandCall).toContain(
"OTEL_EXPORTER_OTLP_PROTOCOL='http/protobuf'",
);
expect(commandCall).toContain(
"OTEL_EXPORTER_OTLP_ENDPOINT='https://example.com'",
);
expect(commandCall).toContain("OTEL_SERVICE_NAME='buildkitd'");
});

it("should warn about invalid driver-opt format", async () => {
const mockCoreWarning = vi.mocked(core.warning);
const mockExeca = vi.mocked(execa);
mockExeca.mockReturnValue({
on: vi.fn(),
stdout: {
pipe: vi.fn(),
},
stderr: {
pipe: vi.fn(),
},
} as unknown as ReturnType<typeof execa>);

const driverOpts = [
"env.VALID_VAR=value",
"env.INVALID_VAR", // Missing value
"unsupported.option=value", // Unsupported prefix
];

await startBuildkitd(4, "tcp://127.0.0.1:1234", undefined, driverOpts);

// Check warnings were logged
expect(mockCoreWarning).toHaveBeenCalledWith(
expect.stringContaining(
"Invalid driver-opt format (missing value): env.INVALID_VAR",
),
);
expect(mockCoreWarning).toHaveBeenCalledWith(
expect.stringContaining(
"Unsupported driver-opt (only env.* options are currently supported): unsupported.option=value",
),
);
});

it("should handle empty driver-opts array", async () => {
const mockExeca = vi.mocked(execa);
mockExeca.mockReturnValue({
on: vi.fn(),
stdout: {
pipe: vi.fn(),
},
stderr: {
pipe: vi.fn(),
},
} as unknown as ReturnType<typeof execa>);

await startBuildkitd(4, "tcp://127.0.0.1:1234", undefined, []);

// Verify that execa was called without environment variables
expect(mockExeca).toHaveBeenCalledTimes(1);
const commandCall = mockExeca.mock.calls[0][0] as string;

// Should not contain any environment variables, no env command
expect(commandCall).not.toContain("OTEL_");
expect(commandCall).not.toContain("sudo env");
expect(commandCall).toContain("nohup sudo");
});

it("should handle undefined driver-opts", async () => {
const mockExeca = vi.mocked(execa);
mockExeca.mockReturnValue({
on: vi.fn(),
stdout: {
pipe: vi.fn(),
},
stderr: {
pipe: vi.fn(),
},
} as unknown as ReturnType<typeof execa>);

await startBuildkitd(4, "tcp://127.0.0.1:1234", undefined, undefined);

// Verify that execa was called without environment variables
expect(mockExeca).toHaveBeenCalledTimes(1);
const commandCall = mockExeca.mock.calls[0][0] as string;

// Should not contain any environment variables, no env command
expect(commandCall).not.toContain("OTEL_");
expect(commandCall).not.toContain("sudo env");
expect(commandCall).toContain("nohup sudo");
});

it("should handle driver-opts with special characters in values", async () => {
const mockExeca = vi.mocked(execa);
mockExeca.mockReturnValue({
on: vi.fn(),
stdout: {
pipe: vi.fn(),
},
stderr: {
pipe: vi.fn(),
},
} as unknown as ReturnType<typeof execa>);

const driverOpts = [
"env.SPECIAL_CHARS=value with spaces",
"env.QUOTES=value'with'quotes",
"env.EQUALS=key=value",
];

await startBuildkitd(4, "tcp://127.0.0.1:1234", undefined, driverOpts);

// Verify that execa was called with properly escaped values
expect(mockExeca).toHaveBeenCalledTimes(1);
const commandCall = mockExeca.mock.calls[0][0] as string;

// Check that values are properly quoted
expect(commandCall).toContain("SPECIAL_CHARS='value with spaces'");
expect(commandCall).toContain("QUOTES='value'with'quotes'");
expect(commandCall).toContain("EQUALS='key=value'");
});
});
28 changes: 28 additions & 0 deletions src/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import * as core from "@actions/core";
import { Util } from "@docker/actions-toolkit/lib/util";

// Mock the @actions/core module
vi.mock("@actions/core", () => ({
Expand All @@ -14,6 +15,13 @@ vi.mock("@actions/core", () => ({
group: vi.fn((name, fn) => fn()),
}));

// Mock the Util module
vi.mock("@docker/actions-toolkit/lib/util", () => ({
Util: {
getInputList: vi.fn(),
},
}));

describe("setup-docker-builder", () => {
beforeEach(() => {
vi.clearAllMocks();
Expand All @@ -35,6 +43,7 @@ describe("setup-docker-builder", () => {
it("should handle inputs correctly", async () => {
const mockGetInput = vi.mocked(core.getInput);
const mockGetBooleanInput = vi.mocked(core.getBooleanInput);
const mockGetInputList = vi.mocked(Util.getInputList);

mockGetInput.mockImplementation((name: string) => {
switch (name) {
Expand All @@ -56,8 +65,27 @@ describe("setup-docker-builder", () => {
}
});

mockGetInputList.mockImplementation((name: string) => {
switch (name) {
case "driver-opts":
// Simulate the parsing with ignoreComma and quote options
return [
"env.OTEL_TRACES_EXPORTER=otlp",
"env.OTEL_SERVICE_NAME=buildkitd",
];
case "platforms":
return [];
default:
return [];
}
});

// Verify mocks are called correctly
expect(mockGetInput("buildx-version")).toBe("v0.23.0");
expect(mockGetBooleanInput("nofallback")).toBe(false);
expect(mockGetInputList("driver-opts")).toEqual([
"env.OTEL_TRACES_EXPORTER=otlp",
"env.OTEL_SERVICE_NAME=buildkitd",
]);
});
});
6 changes: 6 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export interface Inputs {
nofallback: boolean;
"github-token": string;
"skip-integrity-check": boolean;
"driver-opts": string[];
}

async function getInputs(): Promise<Inputs> {
Expand All @@ -171,6 +172,10 @@ async function getInputs(): Promise<Inputs> {
nofallback: core.getBooleanInput("nofallback"),
"github-token": core.getInput("github-token"),
"skip-integrity-check": core.getBooleanInput("skip-integrity-check"),
"driver-opts": Util.getInputList("driver-opts", {
ignoreComma: true,
quote: false,
}),
};
}

Expand Down Expand Up @@ -303,6 +308,7 @@ async function startBlacksmithBuilder(
const buildkitdAddr = await startAndConfigureBuildkitd(
parallelism,
buildkitdPath,
inputs["driver-opts"],
);
const buildkitdDurationMs = Date.now() - buildkitdStartTime;
await reporter.reportMetric(
Expand Down
Loading