diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 8608a49..4dd7528 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [master] pull_request: workflow_dispatch: @@ -45,7 +46,7 @@ jobs: run: uv sync --locked --dev - name: Lint - run: uv run --locked ruff check src tests + run: uv run --locked ruff check src tests ops/gitops - name: Run offline and PostgreSQL integration tests run: timeout 10m uv run --locked python -m unittest discover -s tests -v @@ -58,3 +59,54 @@ jobs: uv venv /tmp/car-tracker-wheel uv pip install --python /tmp/car-tracker-wheel/bin/python --no-deps dist/*.whl /tmp/car-tracker-wheel/bin/python -c 'from importlib.resources import files; assert files("car_tracker").joinpath("migrations", "001_silver.sql").is_file(), "SQL migration missing from wheel"' + + publish: + name: Build and publish tested image + needs: test + # PRs and the gitops state branch must never publish images. + if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + runs-on: python-ci + env: + REGISTRY: gitea.vandeplanque.com + IMAGE_REPOSITORY: tonio/car-tracker + IMAGE_SHA: ${{ github.sha }} + DOCKER_CONFIG: /tmp/car-tracker-docker-auth + steps: + - name: Check out tested revision + uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Install Docker client + run: sh ops/ci/install-docker-cli.sh + + - name: Authenticate to Gitea registry + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -eu + test -n "$REGISTRY_USER" && test -n "$REGISTRY_TOKEN" + umask 077 + mkdir -p "$DOCKER_CONFIG" + printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" --username "$REGISTRY_USER" --password-stdin + + - name: Build and publish SHA-tagged image + run: | + set -eu + image="$REGISTRY/$IMAGE_REPOSITORY:$IMAGE_SHA" + # Do not overwrite a previously published SHA tag on a workflow rerun. + # Digest pinning, not this mutable registry tag, identifies a run image. + if docker manifest inspect "$image" >/dev/null 2>&1; then + echo "Image already published: $image" + exit 0 + fi + timeout 35m docker build --label "org.opencontainers.image.revision=$IMAGE_SHA" -t "$image" . + docker run --rm --network none "$image" car-tracker-db --help + timeout 5m docker push "$image" + echo "Published: $image" + + - name: Remove job registry credentials + if: always() + run: rm -rf /tmp/car-tracker-docker-auth diff --git a/.gitea/workflows/run-app.yaml b/.gitea/workflows/run-app.yaml new file mode 100644 index 0000000..d7049dc --- /dev/null +++ b/.gitea/workflows/run-app.yaml @@ -0,0 +1,62 @@ +name: Run app + +on: + workflow_dispatch: + inputs: + release: + description: "Published full commit SHA or Git tag (not a branch or latest)" + required: true + type: string + +jobs: + request: + name: Queue one production run through Git + # Execute trusted control-plane code, never a user-selected release workflow. + if: github.ref == 'refs/heads/master' + runs-on: python-ci + env: + REGISTRY: gitea.vandeplanque.com + IMAGE_REPOSITORY: tonio/car-tracker + DOCKER_CONFIG: /tmp/car-tracker-docker-auth + RELEASE: ${{ github.event.inputs.release }} + GITOPS_URL: https://gitea.vandeplanque.com/tonio/car-tracker.git + GITOPS_BRANCH: gitops + steps: + - name: Check out trusted master workflow revision + uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Install Docker client + run: sh ops/ci/install-docker-cli.sh + + - name: Install Python + uses: https://github.com/astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e + with: + version: "0.9.3" + python-version: "3.13" + enable-cache: false + add-problem-matchers: false + github-token: "" + + - name: Authenticate to registry + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -eu + test -n "$REGISTRY_USER" && test -n "$REGISTRY_TOKEN" + umask 077 + mkdir -p "$DOCKER_CONFIG" + printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" --username "$REGISTRY_USER" --password-stdin + + - name: Resolve published release and commit run request + env: + GITOPS_USER: ${{ secrets.GITOPS_USER }} + GITOPS_TOKEN: ${{ secrets.GITOPS_TOKEN }} + run: uv run --no-project --python 3.13 python ops/gitops/request.py + + - name: Remove job registry credentials + if: always() + run: rm -rf /tmp/car-tracker-docker-auth diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index e8ecaec..daffab1 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -28,6 +28,10 @@ The manual homelab job is now packaged in `Dockerfile` and `ops/app/compose.yaml `car-tracker-pipeline` performs a database preflight, bounded collection, and an idempotent import of the exact returned manifest, retaining partial coverage and separate operational outcomes. There is no scheduler or automatic deployment. +The optional Gitea Actions/GitOps request workflow is documented in +`docs/GITOPS.md`: tests/build/publish occur on a dedicated runner VM; explicit +requests are pulled and consumed once on the production VM. It is not a daily +collection schedule, and migrations remain manual. See `docs/DEPLOYMENT.md`; container tests use isolated PostgreSQL 17 and synthetic browser responses. Actual live access from the homelab still needs verification. diff --git a/README.md b/README.md index d28c2e1..27209e2 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,11 @@ and validation issues. Generation/trim inference, redaction, and scheduling are ## Manual Homelab Job +For a manual **Run app** button in Gitea Actions, see [GitOps jobs](docs/GITOPS.md). +CI publishes tested images; the button queues a digest-pinned request, and the +production host consumes it once using host-local DB credentials. No automatic +migrations or retries of failed scrapes are introduced. + `car-tracker-pipeline` checks database connectivity/schema, collects raw observations, and imports that exact finalized run. Partial/failed collections remain marked as such even when their import succeeds. A lock prevents overlapping pipeline jobs on diff --git a/deploy/prod.tag b/deploy/prod.tag deleted file mode 100644 index 4b17464..0000000 --- a/deploy/prod.tag +++ /dev/null @@ -1,6 +0,0 @@ -# Desired deployment state for the homelab, consumed by -# ~/containers/car-tracker/reconcile.sh. The value must be a full 40-character -# commit SHA of a revision whose image has been pushed to the Gitea registry as -# tonio/car-tracker:. To roll back, revert the commit that changed this file. -# Lines beginning with # are ignored. -2c58234fd9f360f262fde255fae5273ec8f57f6c diff --git a/docs/CI.md b/docs/CI.md index 50c39a8..41f97dc 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -2,8 +2,9 @@ This repository targets **Gitea 1.23.8**. The workflow is in `.gitea/workflows/ci.yaml`; it runs on pushes, pull requests, and manual dispatch. -It adds continuous integration only: no application deployment, scheduled scraping, -production migrations, or access to the homelab application database. +It tests and publishes images on master. Manual **Run app** dispatch queues a +GitOps job; see [GITOPS.md](GITOPS.md) for credentials and production installation. +No live scraping, production migration, or production DB access occurs in CI. ## What Runs @@ -34,7 +35,7 @@ action commits, uv, and the Python dependency lockfile are explicitly pinned. Gitea 1.23 ignores several GitHub workflow keys, including job-level `timeout-minutes`, `permissions`, and `concurrency`; the workflow does not pretend -they provide protections. Runner configuration enforces capacity 1 and a 30-minute +they provide protections. Runner configuration enforces capacity 1 and a 45-minute job timeout; the test command also has a 10-minute timeout. ## Enable Repository Actions @@ -59,8 +60,8 @@ with your Gitea version. The runner daemon needs Docker socket access, which is effectively root access to the CI host. Do not put production database credentials, backups, or raw listing -data on that host. Job containers do not receive the socket or arbitrary host bind -mounts. These restrictions reduce exposure but do not make self-hosted execution +data on that host. Job containers now receive the socket for builds, but arbitrary +workflow host bind mounts are not allowlisted. This does not make self-hosted execution safe for arbitrary hostile workflows. Register at repository scope, accept only trusted contributors/workflows, and review fork pull requests before allowing them to run. Runner and server updates should be planned independently of this initial @@ -82,7 +83,7 @@ from `tonio/car-tracker` -> **Settings -> Actions -> Runners**. Enter: | --- | --- | | Instance URL | Your Gitea **HTTP(S) web URL**, including its port if needed | | Registration token | The repository token, entered locally; do not send it in chat | -| Runner name | For example `car-tracker-ci` | +| Runner name | For example `gitea-runner` (the name does not affect job routing) | | Labels | `python-ci:docker://node:22-bookworm` | The instance URL is **not** the Git SSH URL ending in port 2222. It must resolve @@ -92,8 +93,9 @@ verify DNS and routing inside Docker as well as on the CI host. HTTPS certificat verification remains enabled; configure trust for private certificates rather than disabling it. -Registration writes credentials to `/data/.runner` in the persistent Docker volume. -Treat that volume as a secret. No registration token is needed in Compose's +Registration writes credentials to `ops/ci-runner/data/.runner` via the `./data` bind. +The deployed copy on the new VM is `~/containers/gitea-runner/data/.runner`. +Keep this directory private and outside Git. No registration token is needed in Compose's environment. Start the registered daemon: ```sh @@ -108,7 +110,7 @@ not need your application PostgreSQL credentials or an HBA rule for that databas Restrict CI-host network access to production services where practical. Do not run `docker compose down -v` unless you intend to erase runner registration. -Normal container recreation preserves the registration volume. +Normal container recreation preserves the registration directory. ## First Run Checks diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index e95b687..870b352 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -8,7 +8,9 @@ Use the existing database and role, currently both `car_tracker`. Do not provision another production PostgreSQL server or recreate those objects. `ops/app/compose.yaml` is the one-shot application stack, separate from -`ops/ci-runner`. There is no scheduling, restart loop, or automatic CI deployment. +`ops/ci-runner`. This section describes direct manual operation, with no scheduling +or restart loop. For the optional Gitea button → GitOps request → host-run flow, +see [GITOPS.md](GITOPS.md). Migrations remain manual in both modes. Run commands from the homelab checkout's root with Docker Engine and Compose available. Review the revision and `searches.toml`; do not run these in CI. After these changes are published, update the homelab checkout before building. diff --git a/docs/GITOPS.md b/docs/GITOPS.md new file mode 100644 index 0000000..983e996 --- /dev/null +++ b/docs/GITOPS.md @@ -0,0 +1,194 @@ +# Gitea-triggered application jobs + +## Flow + +The app is a one-shot collector/importer, not a continuously running service. + +1. Push to **master** (the actual default branch). CI runs lint, offline tests, + disposable PostgreSQL 17 integration tests and package checks. +2. After tests pass, CI builds and publishes + `gitea.vandeplanque.com/tonio/car-tracker:`. Reruns skip an + existing SHA tag. Tags are still mutable at the registry; requests pin a digest. +3. Choose **Actions → Run app → Run workflow**, select workflow branch **master**, + and enter a published full SHA or a Git tag pointing to it. Branch names and + `latest` are not release selectors. The release must be in master's history. +4. The workflow pulls the image on the builder VM, verifies its revision label, + and commits `deploy/request.json` to the dedicated **gitops** branch. It creates + that branch from master on the first request. Every dispatch gets a new UUID, + even for the same image. Concurrent non-fast-forward pushes fail; no force push. +5. A production user-systemd timer reads the request history using a separate bare + Git checkout and runs requests serially, once each. It uses the image digest and + its bounded default command. Searches come from that image, not a host checkout. + +**Green Run app means queued, not successfully collected/imported.** The production +journal, local request state and raw manifest/sidecar report the actual outcome. +There is no SSH deployment, incoming deployment API, automatic migration, daily +collection schedule, or automatic retry after collection starts. + +## Runner and network configuration + +`ops/ci-runner/compose.yaml` and `config.yaml` match the new setup at +`~/containers/gitea-runner/`: project `gitea-runner`, `./data` bind, +`python-ci:docker://node:22-bookworm`, capacity 1, timeout 45 minutes, and +`container.docker_host: ""` for job socket access. The build job installs Docker +CLI/Buildx; the daemon runs on the builder VM. No privileged jobs or arbitrary +workflow host bind mounts are enabled. + +Socket access grants jobs root-equivalent control of the **builder VM**, including +other jobs and runner registration. Run only trusted workflows. A label is routing, +not a security boundary. No production passwords, backups or raw data on that VM. +GitOps avoids exposing production credentials to CI, but does not make arbitrary +images trustworthy: the approved image can read the mounted DB password in production. + +Keep runner configuration deployment manual. Inspect before copying repository +files over deployed files; preserve `data/.runner`. Do not copy the old runner's +named volume, unregister the working new runner, or use `down -v` during migration. + +Runner/job `extra_hosts` map Gitea to `192.168.1.56` with HTTPS verification intact. +**Docker Engine also needs working registry DNS on the builder VM**; container +`extra_hosts` does not affect that daemon. If DNS still returns the wrong public +address, carefully add this entry to the builder VM's `/etc/hosts`: + +```text +192.168.1.56 gitea.vandeplanque.com +``` + +Reserve that LAN address in DHCP. Production Docker also needs a working registry +path. Never configure an insecure registry or disable TLS verification. + +## Gitea Actions secrets + +Create these repository secrets in Settings → Actions → Secrets: + +| Secret | Purpose | +| --- | --- | +| `REGISTRY_USER` | `tonio`: owner of the current package namespace | +| `REGISTRY_TOKEN` | Token with `write:package` (includes package reads) | +| `GITOPS_USER` | Account allowed to write the repository's gitops branch | +| `GITOPS_TOKEN` | Separate token with `write:repository` | + +For the current user-owned `tonio/car-tracker` package, publishing must authenticate +as `tonio`. A bot added as a repository collaborator cannot publish to that user's +package namespace. Use separate narrowly scoped tokens; Gitea 1.23 token scopes +are category-wide, not restricted to one repository. Private user-owned packages +also require the owner's identity for reads; production uses a separate +`read:package` token. Public user profiles can expose their packages publicly, +regardless of linked repository visibility. + +Gitea 1.23 does not enforce GitHub-style +job `permissions` as a security boundary: set token scopes and account/branch access +in Gitea itself. Protect master; restrict the state writer to gitops through branch +protection where possible. Never rewrite the gitops branch's audit log/queue. +Verify the package owner's visibility; packages do not inherit repository +access controls. + +Enter tokens only in the Gitea UI, not chat, commands, committed files or logs. +The workflows use stdin for registry login and a temporary Git askpass helper, +not tokens in command arguments or URLs. The built-in Actions token is not assumed +to have write privileges on this older release. No production password or SSH +deployment key belongs in Actions secrets. + +## Install the production consumer (manual, no run yet) + +Follow [host runtime configuration](DEPLOYMENT.md) first: private directories, +`app.env`, `pgpass`, TLS and HBA rule must exist. Migrations stay manual. The chosen +stack root here is `~/containers/car-tracker`, not the `/srv` example. + +**Disable the old SHA-polling agent before installing this version.** It checked +out releases in the development repository and retried failed scrapes. Neither +behavior is used here. The obsolete `deploy/prod.tag` is removed; do not activate +the old agent. It will see no desired tag after these changes are published. + +```sh +systemctl --user disable --now car-tracker-reconcile.timer +systemctl --user status car-tracker-reconcile.service +``` + +If a service/job is active, let it finalize before replacing it; investigate uncertain +imports. From the updated repository root, inspect destination files first, then: + +```sh +install -m 0700 ops/gitops/reconcile.py ~/containers/car-tracker/reconcile.py +install -m 0600 ops/app/compose.runtime.yaml ~/containers/car-tracker/compose.runtime.yaml +mkdir -p ~/.config/systemd/user +install -m 0644 ops/gitops/car-tracker-reconcile.service ~/.config/systemd/user/ +install -m 0644 ops/gitops/car-tracker-reconcile.timer ~/.config/systemd/user/ +systemctl --user daemon-reload +``` + +Create a **separate** bare checkout only if `desired.git` doesn't already exist: + +```sh +git clone --bare ssh://git@debian.tailf52e5b.ts.net:2222/tonio/car-tracker.git \ + ~/containers/car-tracker/desired.git +chmod 0700 ~/containers/car-tracker/desired.git +``` + +Prefer a read-only repository deploy key. Verify SSH host trust/access interactively +before enabling the timer. The agent never checks out or resets the development repo. + +For private registry pulls, run `docker login gitea.vandeplanque.com` interactively +on production with a separate **read-only package token**, not the publisher token. +Protect Docker's credential file or use a credential helper. Credentials remain local. + +Both Compose stacks use `car-tracker-app` and the same subnet/storage. If the old +pre-rename `car-traker-app` network occupies the subnet, remove it through the old +project only when no job is active; do not delete unrelated networks. + +Before dispatch, validate with a real published digest (never this placeholder): + +```sh +CAR_TRACKER_IMAGE='gitea.vandeplanque.com/tonio/car-tracker@sha256:' \ + docker compose --env-file ~/containers/car-tracker/app.env \ + -f ~/containers/car-tracker/compose.runtime.yaml config --quiet +``` + +When ready: + +```sh +sudo loginctl enable-linger tonio +systemctl --user enable --now car-tracker-reconcile.timer +``` + +Review gitops history first: enabling can execute all outstanding requests. If the +branch does not exist yet, fetching fails and no app starts; later polls try again. +Repository updates do not automatically replace deployed agent/Compose files. + +## Outcomes and recovery + +The timer polls approximately every minute after the preceding cycle finishes. +All request commits are processed in order, so two button presses between polls +are not silently reduced to the latest image. + +```sh +systemctl --user list-timers car-tracker-reconcile.timer +journalctl --user -u car-tracker-reconcile.service -n 100 --no-pager +ls ~/containers/car-tracker/state/requests/ +``` + +Each `state/requests/.json` records a request and pipeline exit code. `finished` +means exited, **not** complete source coverage. See [pipeline exit meanings](DEPLOYMENT.md#outcomes-and-recovery). +Raw files and `pipeline.json` stay private on production, never CI artifacts. + +An attempt is atomically recorded **before launching** the collector. It is never +rerun automatically, regardless of exit code. A crash/timeout may leave +`status: attempted`, exit unknown. This blocks further requests until investigated. +This favors a missed run over duplicate scraping; it is not an exactly-once guarantee. + +Inspect `car-tracker-run-`, the raw manifest and database outcome. Recover an +uncertain import by replaying the exact finalized manifest, not scraping again. +After investigation, retain the record and explicitly set its local status to +`resolved` to unblock future requests. Do not delete state to force retries. Back +up `state/requests`, raw files and the DB: losing the ledger can replay old jobs. + +Registry/config failures **before** recording an attempt can retry because no +collection started. No retries after the marker. Never overlap standalone CLI or +recovery operations with a pipeline on the same output root. + +For an intentional repeat or an older image, dispatch **Run app again**: a new UUID +is recorded. Reverting a state commit containing a consumed UUID does not repeat +it. "Rollback" here selects an older version for a new run; it does not undo DB data. + +Build/image retention remains manual on the builder VM. Check `docker system df` +and prune unused build cache deliberately; never prune volumes to free build space. +No live collection is performed while implementing/testing these files. diff --git a/ops/app/compose.runtime.yaml b/ops/app/compose.runtime.yaml new file mode 100644 index 0000000..29c944c --- /dev/null +++ b/ops/app/compose.runtime.yaml @@ -0,0 +1,52 @@ +# Pull-only production job: no source checkout, build, or searches.toml host bind. +# Searches and application code come from the same digest-pinned image. +name: car-tracker-app + +services: + app: + image: "${CAR_TRACKER_IMAGE:?Set a digest-pinned image}" + restart: "no" + init: true + stop_grace_period: 30s + user: "${APP_UID:-1000}:${APP_GID:-1000}" + read_only: true + cap_drop: [ALL] + security_opt: + - no-new-privileges:true + cpus: 1.5 + mem_limit: 2g + pids_limit: 256 + shm_size: 512m + tmpfs: + - /tmp:rw,nosuid,nodev,size=512m,mode=1777 + - /tmp/.X11-unix:rw,nosuid,nodev,size=1m,mode=1777 + environment: + PGHOST: "${PGHOST:?Set the existing PostgreSQL hostname or IP}" + PGPORT: "${PGPORT:-5432}" + PGDATABASE: "${PGDATABASE:?Set the application database name}" + PGUSER: "${PGUSER:?Set the application database role}" + PGSSLMODE: "${PGSSLMODE:-require}" + PGPASSFILE: /run/db/pgpass + PGSSLROOTCERT: /run/db/root.crt + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - type: bind + source: "${RAW_DATA_DIR:?Set an existing private raw storage directory}" + target: /data + bind: + create_host_path: false + - type: bind + source: "${DB_CONFIG_DIR:?Set an existing private DB configuration directory}" + target: /run/db + read_only: true + bind: + create_host_path: false + networks: + app: + +networks: + app: + ipam: + config: + - subnet: "${APP_SUBNET:-172.30.17.0/28}" diff --git a/ops/ci-runner/.gitignore b/ops/ci-runner/.gitignore new file mode 100644 index 0000000..8fce603 --- /dev/null +++ b/ops/ci-runner/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/ops/ci-runner/compose.yaml b/ops/ci-runner/compose.yaml index 159f75b..670b324 100644 --- a/ops/ci-runner/compose.yaml +++ b/ops/ci-runner/compose.yaml @@ -1,4 +1,6 @@ -# CI infrastructure only; this does not deploy the application or PostgreSQL. +# Install on the dedicated CI/build VM, not the application/database VM. +name: gitea-runner + services: runner: image: gitea/act_runner:0.2.13 @@ -6,16 +8,16 @@ services: entrypoint: ["act_runner"] command: ["--config", "/config.yaml", "daemon"] working_dir: /data - # The Gitea instance is published on loopback only, and its public name still - # points at an IPv4 address this host no longer has. Resolve the name to the - # host's LAN address so the runner can reach the Gitea API. Update this together - # with the `options` entry in config.yaml if the LAN address changes. + # Preserve HTTPS/SNI while reaching Gitea via its existing LAN listener. + # Keep this mapping in sync with container.options in config.yaml. extra_hosts: - "gitea.vandeplanque.com:192.168.1.56" volumes: - - runner-data:/data + - ./data:/data - ./config.yaml:/config.yaml:ro - /var/run/docker.sock:/var/run/docker.sock - -volumes: - runner-data: + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/ops/ci-runner/config.yaml b/ops/ci-runner/config.yaml index 112094f..a4375c0 100644 --- a/ops/ci-runner/config.yaml +++ b/ops/ci-runner/config.yaml @@ -4,7 +4,7 @@ log: runner: file: /data/.runner capacity: 1 - timeout: 30m + timeout: 45m shutdown_timeout: 30s insecure: false env_file: "" @@ -22,6 +22,7 @@ container: # extra_hosts entry in compose.yaml. options: "--add-host=gitea.vandeplanque.com:192.168.1.56" valid_volumes: [] - # The daemon uses Docker, but jobs/services do not receive the Docker socket. - docker_host: "-" + # Build jobs need Docker. Socket access grants root-equivalent access to the + # dedicated CI VM; never register this configuration on the production VM. + docker_host: "" force_pull: true diff --git a/ops/ci/install-docker-cli.sh b/ops/ci/install-docker-cli.sh new file mode 100644 index 0000000..c82e5aa --- /dev/null +++ b/ops/ci/install-docker-cli.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Job containers are Debian-based Node images; install only the client/plugins. +# Docker Engine runs on the separate builder VM, accessed through its socket. +set -eu +apt-get update +apt-get install -y --no-install-recommends ca-certificates curl +install -d -m 0755 /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc +chmod a+r /etc/apt/keyrings/docker.asc +cat > /etc/apt/sources.list.d/docker.sources <