From f549eb31e9c4d7fa52cc0e2052ce092222fbea15 Mon Sep 17 00:00:00 2001 From: Lakshmikanth Raju Date: Thu, 13 Aug 2026 22:05:15 +0530 Subject: [PATCH] Add sample Docker image with API server for scc commands Adds a multi-stage Dockerfile that builds the salt-config-cli wheel, installs it alongside git, and runs a small FastAPI server exposing POST /commands to execute scc commands and return their output. Documents build/run/usage in the README. --- README.md | 78 +++++++++++++++++++++++++++++++++++++ docker/Dockerfile | 63 ++++++++++++++++++++++++++++++ docker/api/app.py | 69 ++++++++++++++++++++++++++++++++ docker/api/requirements.txt | 2 + 4 files changed, 212 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/api/app.py create mode 100644 docker/api/requirements.txt diff --git a/README.md b/README.md index 4035962..bc5575e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ It provides a safe workflow for: 18. [Automation and CI/CD](#automation-and-cicd) 19. [Troubleshooting](#troubleshooting) 20. [Development and release](#development-and-release) +21. [Docker](#docker) --- @@ -1386,6 +1387,83 @@ CHANGELOG.md --- +## Docker + +A sample containerized deployment lives under `docker/`. It builds an image that: + +- builds the `salt-config-cli` wheel from this repo and installs it (`scc`/`salt-config`/`raas` entry points); +- installs the system `git` client that SCC shells out to for repo operations; +- runs a small FastAPI server (`docker/api/app.py`) with a single `POST /commands` endpoint that executes `scc` commands and returns the result. + +```text +docker/ +├── Dockerfile +└── api/ + ├── app.py + └── requirements.txt +``` + +### Build + +Build from the **repo root** (the Dockerfile copies `pyproject.toml`, `README.md`, `LICENSE`, and `salt_config_cli/` from the build context): + +```bash +docker build \ + -f docker/Dockerfile \ + -t salt-cli-api:latest \ + . +``` + +Corporate networks that block direct PyPI access can point `pip` at an internal mirror via the `PIP_INDEX_URL` build arg (defaults to Broadcom's internal Artifactory): + +```bash +docker build \ + -f docker/Dockerfile \ + --build-arg PIP_INDEX_URL=https://packages.vcfd.broadcom.net/artifactory/api/pypi/pypi-virtual/simple \ + -t salt-cli-api:latest \ + . +``` + +### Run + +```bash +docker run --rm -p 8000:8000 salt-cli-api:latest +``` + +### Use + +Health check: + +```bash +curl http://localhost:8000/healthz +# {"status":"ok"} +``` + +Trigger an `scc` command (the `scc`/`salt-config`/`raas` prefix in `command` is optional): + +```bash +curl -X POST http://localhost:8000/commands \ + -H 'Content-Type: application/json' \ + -d '{"command": "repo list --json"}' +``` + +Response shape: + +```json +{ + "command": "scc repo list --json", + "returncode": 0, + "stdout": "...", + "stderr": "" +} +``` + +An optional `timeout` field (seconds, default `60`, max `600`) bounds how long the server waits for the command before returning `504`. + +> This API executes `scc` with arguments taken directly from the request body and returns raw stdout/stderr — it is a development/demo convenience, not a hardened production service. Do not expose it on an untrusted network without adding authentication and access controls. + +--- + ## Contributing Contributions should: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c47a054 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,63 @@ +# syntax=docker/dockerfile:1.7 +# +# Build (from the raas-cli repo root): +# +# docker build \ +# -f docker/Dockerfile \ +# -t salt-cli-api:latest \ +# . +# +# Run: +# +# docker run --rm -p 8000:8000 salt-cli-api:latest +# +# Trigger a command: +# +# curl -X POST http://localhost:8000/commands \ +# -H 'Content-Type: application/json' \ +# -d '{"command": "--version"}' + +# Broadcom corp networks block direct PyPI access; point pip at the internal +# Artifactory mirror by default (override with --build-arg PIP_INDEX_URL=... elsewhere). +ARG PIP_INDEX_URL=https://packages.vcfd.broadcom.net/artifactory/api/pypi/pypi-virtual/simple + +######################## +# Stage 1: build the salt-config-cli wheel +######################## +FROM python:3.12-slim AS wheel-builder +ARG PIP_INDEX_URL +ENV PIP_INDEX_URL=${PIP_INDEX_URL} + +WORKDIR /build + +COPY pyproject.toml README.md LICENSE ./ +COPY salt_config_cli ./salt_config_cli + +RUN pip install --no-cache-dir build && \ + python -m build --wheel --outdir /dist + +######################## +# Stage 2: runtime image +######################## +FROM python:3.12-slim AS runtime +ARG PIP_INDEX_URL +ENV PIP_INDEX_URL=${PIP_INDEX_URL} + +# scc shells out to the system git client for repo operations +RUN apt-get update && \ + apt-get install -y --no-install-recommends git && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# 1. Install the salt-config-cli wheel built above +COPY --from=wheel-builder /dist/*.whl /tmp/ +RUN pip install --no-cache-dir /tmp/*.whl && rm -rf /tmp/*.whl + +# 2. Sample API server that fronts the scc CLI +COPY docker/api/requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt +COPY docker/api/app.py /app/app.py + +EXPOSE 8000 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker/api/app.py b/docker/api/app.py new file mode 100644 index 0000000..d18ef69 --- /dev/null +++ b/docker/api/app.py @@ -0,0 +1,69 @@ +import shlex +import subprocess + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field + +app = FastAPI(title="salt-config-cli API", version="1.0.0") + +SCC_BINARY = "scc" +DEFAULT_TIMEOUT_SECONDS = 60 + + +class CommandRequest(BaseModel): + command: str = Field( + ..., + description='Salt config CLI command to run, e.g. "repo list --json" or "scc repo list --json"', + min_length=1, + ) + timeout: int = Field(DEFAULT_TIMEOUT_SECONDS, ge=1, le=600) + + +class CommandResponse(BaseModel): + command: str + returncode: int + stdout: str + stderr: str + + +@app.get("/healthz") +def healthz(): + return {"status": "ok"} + + +@app.post("/commands", response_model=CommandResponse) +def run_command(request: CommandRequest) -> CommandResponse: + try: + args = shlex.split(request.command) + except ValueError as exc: + raise HTTPException(status_code=400, detail=f"could not parse command: {exc}") from exc + + if not args: + raise HTTPException(status_code=400, detail="command must not be empty") + + # Allow the command to optionally be prefixed with the binary name itself. + if args[0] in (SCC_BINARY, "salt-config", "raas"): + args = args[1:] + + command = [SCC_BINARY, *args] + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + timeout=request.timeout, + ) + except subprocess.TimeoutExpired as exc: + raise HTTPException( + status_code=504, detail=f"command timed out after {request.timeout}s" + ) from exc + except FileNotFoundError as exc: + raise HTTPException(status_code=500, detail=f"{SCC_BINARY} binary not found") from exc + + return CommandResponse( + command=shlex.join(command), + returncode=result.returncode, + stdout=result.stdout, + stderr=result.stderr, + ) diff --git a/docker/api/requirements.txt b/docker/api/requirements.txt new file mode 100644 index 0000000..39c2afd --- /dev/null +++ b/docker/api/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.111,<1 +uvicorn[standard]>=0.30,<1