diff --git a/.dockerignore b/.dockerignore index 6bc022e..3c3e3b4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,11 @@ -node_modules -.dockerignore -Dockerfile - /.cache +/.github +/.react-router +/node_modules /build +/docs +/nix + +.dockerignore .env +Dockerfile diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 47610b5..a201aee 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -55,7 +55,7 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod', '**/go.sum') }} - name: CI pipeline - run: mise run ci + run: ./build.sh --skip-pnpm-prune nix: name: nix diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index aad9e35..0000000 --- a/.zed/settings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "formatter": { - "language_server": { - "name": "biome" - } - }, - "code_actions_on_format": { - "source.fixAll.biome": true, - "source.organizeImports.biome": true, - "source.organizeImports": true - }, - "languages": { - "YAML": { - "tab_size": 2, - "hard_tabs": false - }, - "Go": { - "formatter": { - "language_server": { - "name": "gopls" - } - } - } - } -} diff --git a/Dockerfile b/Dockerfile index b5b163f..7160ba2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,7 @@ -FROM --platform=$BUILDPLATFORM jdxcode/mise:latest AS mise-context -COPY mise.toml ./ -RUN --mount=type=secret,id=gh_token,env=MISE_GITHUB_TOKEN mise install +FROM --platform=$BUILDPLATFORM golang:1.25.1 AS go-base +WORKDIR /run -FROM --platform=$BUILDPLATFORM mise-context AS go-build -WORKDIR /build/ - -COPY go.mod go.sum ./ +COPY go.mod go.sum build.sh ./ RUN go mod download COPY cmd/ ./cmd/ @@ -14,51 +10,57 @@ COPY internal/ ./internal/ ARG TARGETOS ARG TARGETARCH ARG IMAGE_TAG -RUN mkdir -p /build/app/ && \ - GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 IMAGE_TAG=$IMAGE_TAG \ - mise run wasm ::: agent ::: fake-shell +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 IMAGE_TAG=$IMAGE_TAG \ + ./build.sh --wasm --agent --fake-shell \ + --wasm-output /bin/hp_ssh.wasm \ + --agent-output /bin/hp_agent \ + --fake-shell-output /bin/fake-sh -RUN chmod +x /build/build/hp_agent -RUN chmod +x /build/build/sh +RUN chmod +x /bin/hp_ssh.wasm +RUN chmod +x /bin/hp_agent +RUN chmod +x /bin/fake-sh -FROM --platform=$BUILDPLATFORM mise-context AS js-build -WORKDIR /build +# Folder needs to exist for later stages +RUN mkdir -p /var/lib/headplane/agent + +FROM --platform=$BUILDPLATFORM node:22.16-slim AS js-base +WORKDIR /run + +RUN corepack enable COPY patches ./patches -COPY package.json pnpm-lock.yaml ./ -RUN pnpm install --frozen-lockfile +COPY package.json pnpm-lock.yaml build.sh ./ + +COPY --from=go-base /bin/hp_ssh.wasm /run/app/hp_ssh.wasm +COPY --from=go-base /bin/wasm_exec.js /run/app/wasm_exec.js +RUN ./build.sh --app --app-install-only COPY . . -RUN mise trust -COPY --from=go-build /build/app/hp_ssh.wasm /build/app/hp_ssh.wasm -COPY --from=go-build /build/app/wasm_exec.js /build/app/wasm_exec.js - -ARG IMAGE_TAG -RUN IMAGE_TAG=$IMAGE_TAG pnpm run build -RUN mkdir -p /var/lib/headplane/agent -RUN pnpm prune --prod +RUN ./build.sh --app FROM gcr.io/distroless/nodejs22-debian12:latest AS final -COPY --from=js-build /build/build/ /app/build/ -COPY --from=js-build /build/drizzle /app/drizzle/ -COPY --from=js-build /var/lib/headplane /var/lib/headplane -COPY --from=js-build /build/node_modules/ /app/node_modules/ -COPY --from=go-build /build/build/hp_agent /usr/libexec/headplane/agent +COPY --from=js-base /run/build /app/build +COPY --from=js-base /run/drizzle /app/drizzle +COPY --from=js-base /run/node_modules /app/node_modules + +COPY --from=go-base /bin/hp_agent /usr/libexec/headplane/agent +COPY --from=go-base /var/lib/headplane /var/lib/headplane # Fake shell to inform the user that they should use the debug image -COPY --from=go-build /build/build/sh /bin/sh -COPY --from=go-build /build/build/sh /bin/bash +COPY --from=go-base /bin/fake-sh /bin/sh +COPY --from=go-base /bin/fake-sh /bin/bash WORKDIR /app CMD [ "/app/build/server/index.js" ] FROM node:22-alpine AS debug-shell -RUN apk add --no-cache bash curl git +RUN apk add --no-cache bash curl -COPY --from=js-build /build/build/ /app/build/ -COPY --from=js-build /build/drizzle /app/drizzle/ -COPY --from=js-build /var/lib/headplane /var/lib/headplane -COPY --from=js-build /build/node_modules/ /app/node_modules/ -COPY --from=go-build /build/build/hp_agent /usr/libexec/headplane/agent +COPY --from=js-base /run/build /app/build +COPY --from=js-base /run/drizzle /app/drizzle +COPY --from=js-base /run/node_modules /app/node_modules + +COPY --from=go-base /bin/hp_agent /usr/libexec/headplane/agent +COPY --from=go-base /var/lib/headplane /var/lib/headplane WORKDIR /app CMD [ "node", "/app/build/server/index.js" ] diff --git a/agent.Dockerfile b/agent.Dockerfile deleted file mode 100644 index 5609c2f..0000000 --- a/agent.Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM golang:1.23 AS builder -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY agent/ ./agent -RUN CGO_ENABLED=0 GOOS=linux go build \ - -trimpath \ - -ldflags "-s -w" \ - -o /app/hp_agent ./agent/cmd/hp_agent - -FROM scratch -COPY --from=builder /app/hp_agent /hp_agent -ENTRYPOINT ["/hp_agent"] diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..38fcb05 --- /dev/null +++ b/build.sh @@ -0,0 +1,165 @@ +#!/bin/sh +# This is a general purpose build script for Headplane to be used across many +# different environments such as CI, Docker, Nix, and locally. For specific +# usage instructions, run `./build.sh --help`. + +set -eu + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT_DIR" || exit 1 + +APP_DIR="$ROOT_DIR/app" +BUILD_DIR="$ROOT_DIR/build" + +BUILD_WASM=0 +BUILD_APP=0 +BUILD_AGENT=0 +BUILD_FAKE_SHELL=0 +SKIP_PATH_CHECKS=0 +SKIP_PNPM_PRUNE=0 +APP_INSTALL_ONLY=0 + +WASM_OUTPUT="$APP_DIR/hp_ssh.wasm" +AGENT_OUTPUT="$BUILD_DIR/hp_agent" +FAKE_SHELL_OUTPUT="$BUILD_DIR/hp_fake_sh" + +die() { echo "error: $*" >&2; exit 1; } +run() { echo ">> $*"; "$@"; } + +while [ $# -gt 0 ]; do + case "$1" in + --wasm) BUILD_WASM=1 ;; + --app) BUILD_APP=1 ;; + --agent) BUILD_AGENT=1 ;; + --fake-shell) BUILD_FAKE_SHELL=1 ;; + --skip-path-checks) SKIP_PATH_CHECKS=1 ;; + --skip-pnpm-prune) SKIP_PNPM_PRUNE=1 ;; + --app-install-only) APP_INSTALL_ONLY=1 ;; + + --wasm-output) + shift + [ $# -gt 0 ] || die "--wasm-output requires a path" + WASM_OUTPUT=$1 + ;; + + --agent-output) + shift + [ $# -gt 0 ] || die "--agent-output requires a path" + AGENT_OUTPUT=$1 + ;; + + --fake-shell-output) + shift + [ $# -gt 0 ] || die "--fake-shell-output requires a path" + FAKE_SHELL_OUTPUT=$1 + ;; + + --help) + cat < override wasm output path + --agent-output override agent output path + --fake-shell-output override fake shell output path +EOF + exit 0 + ;; + *) + die "unknown flag: $1" + ;; + esac + shift +done + +# By default build everything except for the fake shell +if [ "$BUILD_WASM" -eq 0 ] && [ "$BUILD_APP" -eq 0 ] && \ + [ "$BUILD_AGENT" -eq 0 ] && [ "$BUILD_FAKE_SHELL" -eq 0 ]; then + BUILD_WASM=1 + BUILD_APP=1 + BUILD_AGENT=1 +fi + +if [ "$SKIP_PATH_CHECKS" -eq 0 ]; then + [ -d "$ROOT_DIR" ] || die "missing project root" + + need_go=0 + need_pnpm=0 + + [ "$BUILD_WASM" -eq 1 ] && need_go=1 + [ "$BUILD_APP" -eq 1 ] && need_pnpm=1 + [ "$BUILD_AGENT" -eq 1 ] && need_go=1 + [ "$BUILD_FAKE_SHELL" -eq 1 ] && need_go=1 + + if [ $need_go -eq 1 ]; then + echo "==> Checking for Go toolchain" + command -v go >/dev/null 2>&1 || die "go not installed" + go version >/dev/null 2>&1 || die "go not working" + fi + + if [ $need_pnpm -eq 1 ]; then + echo "==> Checking for pnpm" + command -v pnpm >/dev/null 2>&1 || die "pnpm not installed" + pnpm --version >/dev/null 2>&1 || die "pnpm not working" + fi +fi + +build_wasm() { + echo "==> Building SSH WASM module → $WASM_OUTPUT" + mkdir -p "$(dirname "$WASM_OUTPUT")" + echo "// $(go version)" > "$(dirname "$WASM_OUTPUT")/wasm_exec.js" + + # This depends on Go 1.23+ since the path is different in earlier versions + cat "$(go env GOROOT)/lib/wasm/wasm_exec.js" >> \ + "$(dirname "$WASM_OUTPUT")/wasm_exec.js" + + GOOS=js GOARCH=wasm go build -o "$WASM_OUTPUT" ./cmd/hp_ssh +} + +build_app() { + echo "==> Building React Router app → $BUILD_DIR" + [ -f "$WASM_OUTPUT" ] || echo "warning: Building without SSH WASM module" + pnpm install --frozen-lockfile + + if [ "$APP_INSTALL_ONLY" -eq 1 ]; then + echo "==> Skipping app build (install only)" + return + fi + + pnpm run build + + if [ "$SKIP_PNPM_PRUNE" -eq 0 ]; then + echo "==> Pruning devDependencies from node_modules" + pnpm prune --prod + fi +} + +build_agent() { + echo "==> Building Tailscale agent → $AGENT_OUTPUT" + mkdir -p "$(dirname "$AGENT_OUTPUT")" + go build -o "$AGENT_OUTPUT" ./cmd/hp_agent +} + +build_fake_shell() { + [ -n "${IMAGE_TAG:-}" ] || die \ + "\$IMAGE_TAG is required to build fake shell binary" + + echo "==> Building fake shell binary → $FAKE_SHELL_OUTPUT" + mkdir -p "$(dirname "$FAKE_SHELL_OUTPUT")" + go build -ldflags="-s -w -X main.imageTag=${IMAGE_TAG}" \ + -o "$FAKE_SHELL_OUTPUT" ./cmd/fake_sh +} + +[ "$BUILD_FAKE_SHELL" = 1 ] && build_fake_shell + +[ "$BUILD_WASM" = 1 ] && build_wasm +[ "$BUILD_APP" = 1 ] && build_app +[ "$BUILD_AGENT" = 1 ] && build_agent +[ "$BUILD_FAKE_SHELL" = 1 ] && build_fake_shell + +echo "✅ Build complete." diff --git a/mise.toml b/mise.toml index 68ed043..e58ca4f 100644 --- a/mise.toml +++ b/mise.toml @@ -3,37 +3,6 @@ go = "1.25.1" pnpm = "10.4.0" node = "22.16" -[tasks.copy-wasm-shim] -alias = ["gojs"] -description = "Copies Go's wasm_exec.js to the public directory" -run = [ - "echo // $(go version) > app/wasm_exec.js", - "cat $(go env GOROOT)/lib/wasm/wasm_exec.js >> app/wasm_exec.js", -] - -[tasks.build-go-wasm] -alias = ["wasm"] -depends = ["copy-wasm-shim"] -description = "Builds the Go WebAssembly module for Tailscale SSH" -env = { GOOS = "js", GOARCH = "wasm" } -run = "go build -o app/hp_ssh.wasm ./cmd/hp_ssh" - -[tasks.build-go-agent] -alias = ["agent"] -description = "Builds the Go agent for HostInfo" -run = "go build -o build/hp_agent ./cmd/hp_agent" -# env = { CGO_ENABLED = "1" } -# run = "go build -o build/hp_agent.so -buildmode=c-shared ./cmd/hp_agent" - - -[tasks.build-fake-shell] -alias = ["fake-shell"] -description = "Builds the fake shell for Distroless docker images" -run = [ - 'test -n "$IMAGE_TAG" || (echo "IMAGE_TAG is not set" && exit 1)', - 'go build -ldflags="-s -w -X main.imageTag=$IMAGE_TAG" -o build/sh ./cmd/fake_sh' -] - [tasks.build-nixos-docs] alias = ["nixos-docs"] description = "Builds NixOS module documentation" diff --git a/package.json b/package.json index e37331f..9b8bab2 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,7 @@ "type": "module", "scripts": { "preinstall": "npx only-allow pnpm", - "prebuild": "npx @jdxcode/mise run build-go-wasm", "build": "react-router build", - "postbuild": "npx @jdxcode/mise run build-go-agent", "dev": "HEADPLANE_LOAD_ENV_OVERRIDES=true HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev", "start": "node build/server/index.js", "typecheck": "react-router typegen && tsgo",