diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ab5730c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +.dockerignore +Dockerfile +target +.env +*.md +.DS_Store diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..eb44d1c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,50 @@ +name: Docker + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Select Docker platforms + id: docker-platforms + shell: bash + run: | + if [ "${ACT:-}" = "true" ] || [ "${GITEA_ACTIONS:-}" = "true" ]; then + echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT" + else + echo "platforms=linux/amd64,linux/arm64" >> "$GITHUB_OUTPUT" + fi + + - name: Set up QEMU + if: ${{ steps.docker-platforms.outputs.platforms != 'linux/amd64' }} + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + + # Production hardening: add dependency and image scanning before publishing, + # for example cargo-audit plus a container scanner such as Sysdig or Trivy. + - name: Build multi-arch image + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 + with: + context: . + platforms: ${{ steps.docker-platforms.outputs.platforms }} + push: false + build-args: | + GIT_SHA=${{ github.sha }} + cache-from: type=gha,scope=docker-multiarch + cache-to: type=gha,mode=max,scope=docker-multiarch diff --git a/.github/workflows/external-contracts.yml b/.github/workflows/external-contracts.yml new file mode 100644 index 0000000..074fc2c --- /dev/null +++ b/.github/workflows/external-contracts.yml @@ -0,0 +1,30 @@ +name: External contracts + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_PROFILE_TEST_DEBUG: line-tables-only + CARGO_INCREMENTAL: 0 + +jobs: + external-contracts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + + # These tests intentionally call real third-party APIs. Unit/integration + # tests mock providers, but a nightly contract check catches external API + # drift that would otherwise only appear at runtime. + - name: Run external contract tests + run: cargo test --locked --test external_contract -- --ignored --test-threads=1 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..7377edf --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,47 @@ +name: Rust + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_PROFILE_DEV_DEBUG: line-tables-only + CARGO_PROFILE_TEST_DEBUG: line-tables-only + CARGO_INCREMENTAL: 0 + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + with: + components: clippy, rustfmt + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + + - name: cargo fmt + run: cargo fmt --all --check + + - name: cargo clippy + run: cargo clippy --locked --all-targets -- -D warnings + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + + - name: cargo test + run: cargo test --locked diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..552d302 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,84 @@ +# syntax=docker/dockerfile:1.7 +FROM --platform=$BUILDPLATFORM rust:1.94-trixie AS chef + +ARG BUILDARCH +ARG TARGETARCH + +WORKDIR /app + +RUN if [ "$TARGETARCH" = "arm64" ] && [ "$BUILDARCH" != "arm64" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + pkg-config && \ + rustup target add aarch64-unknown-linux-gnu && \ + rm -rf /var/lib/apt/lists/*; \ + elif [ "$TARGETARCH" = "amd64" ] && [ "$BUILDARCH" != "amd64" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + gcc-x86-64-linux-gnu \ + g++-x86-64-linux-gnu \ + libc6-dev-amd64-cross \ + pkg-config && \ + rustup target add x86_64-unknown-linux-gnu && \ + rm -rf /var/lib/apt/lists/*; \ + fi + +ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ + CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \ + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc \ + CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + cargo install cargo-chef --locked + +RUN if [ "$TARGETARCH" = "$BUILDARCH" ]; then \ + echo '--release --locked' > /tmp/cargo-args && \ + echo 'target/release/pokedex-api' > /tmp/binary-path; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + echo '--release --locked --target aarch64-unknown-linux-gnu' > /tmp/cargo-args && \ + echo 'target/aarch64-unknown-linux-gnu/release/pokedex-api' > /tmp/binary-path; \ + elif [ "$TARGETARCH" = "amd64" ]; then \ + echo '--release --locked --target x86_64-unknown-linux-gnu' > /tmp/cargo-args && \ + echo 'target/x86_64-unknown-linux-gnu/release/pokedex-api' > /tmp/binary-path; \ + fi + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/app/target \ + cargo chef cook $(cat /tmp/cargo-args) --recipe-path recipe.json + +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/app/target \ + cargo build $(cat /tmp/cargo-args) --bin pokedex-api && \ + cp $(cat /tmp/binary-path) /usr/local/bin/pokedex-api + +FROM debian:trixie-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system app \ + && useradd --system --gid app --home-dir /nonexistent --shell /usr/sbin/nologin app + +COPY --from=builder /usr/local/bin/pokedex-api /usr/local/bin/pokedex-api + +ARG GIT_SHA=unknown +ENV GIT_SHA=$GIT_SHA \ + BIND_ADDR=0.0.0.0:8000 + +EXPOSE 8000 +USER app +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8000/health || exit 1 + +CMD ["pokedex-api"]