diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..12f0faa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Git +.git +.gitignore + +# CI/CD (not needed in image) +.github/ +.goreleaser.yaml +.golangci.yml +justfile +release-please-config.json +.release-please-manifest.json + +# Documentation +docs/ +*.md + +# Build artifacts +dist/ +bin/ +coverage.* +profiles/ + +# Test data +testdata/ + +# Nix +flake.* + +# IDE/Editor +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Claude/AI +.claude/ +CLAUDE.md +AGENTS.md + +# Demo/examples +demo.tape +install.sh diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..d08601d --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,100 @@ +name: Docker CI + +on: + push: + branches: [master] + paths: + - "Dockerfile" + - ".dockerignore" + - ".github/workflows/docker-ci.yml" + - "**.go" + - "go.mod" + - "go.sum" + pull_request: + branches: [master] + paths: + - "Dockerfile" + - ".dockerignore" + - ".github/workflows/docker-ci.yml" + - "**.go" + - "go.mod" + - "go.sum" + +permissions: + contents: read + security-events: write + +jobs: + hadolint: + name: Dockerfile Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + failure-threshold: warning + + build-and-scan: + name: Build and Scan + runs-on: ubuntu-latest + needs: hadolint + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata + id: meta + run: | + echo "version=$(git describe --tags --always --dirty)" >> $GITHUB_OUTPUT + echo "commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT + + - name: Build image for scanning + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: blobber:ci-${{ github.sha }} + build-args: | + VERSION=${{ steps.meta.outputs.version }} + COMMIT=${{ steps.meta.outputs.commit }} + DATE=${{ steps.meta.outputs.date }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: blobber:ci-${{ github.sha }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH,MEDIUM + + - name: Upload Trivy scan results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif + + - name: Fail on critical vulnerabilities + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: blobber:ci-${{ github.sha }} + format: table + exit-code: "1" + severity: CRITICAL,HIGH + + - name: Test image + run: | + docker run --rm blobber:ci-${{ github.sha }} version + docker run --rm blobber:ci-${{ github.sha }} --help diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..cbdc307 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,143 @@ +name: Docker Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to release (e.g., v1.0.0)" + required: true + type: string + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +env: + GHCR_IMAGE: ghcr.io/meigma/blobber + DOCKERHUB_IMAGE: index.docker.io/meigma/blobber + +jobs: + build-and-push: + name: Build and Push + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.tag || github.ref }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine tag + id: tag + run: | + if [ -n "${{ inputs.tag }}" ]; then + echo "value=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "value=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_IMAGE }} + ${{ env.DOCKERHUB_IMAGE }} + tags: | + type=semver,pattern={{version}},value=${{ steps.tag.outputs.value }} + type=semver,pattern={{major}}.{{minor}},value=${{ steps.tag.outputs.value }} + type=semver,pattern={{major}},value=${{ steps.tag.outputs.value }},enable=${{ !startsWith(steps.tag.outputs.value, 'v0.') }} + type=sha,prefix= + type=raw,value=latest + + - name: Extract version info + id: version + run: | + if [ -n "${{ inputs.tag }}" ]; then + VERSION="${{ inputs.tag }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + fi + VERSION="${VERSION#v}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT + + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ steps.version.outputs.version }} + COMMIT=${{ steps.version.outputs.commit }} + DATE=${{ steps.version.outputs.date }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Build provenance attestations + - name: Attest GHCR build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.GHCR_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + - name: Attest Docker Hub build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.DOCKERHUB_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + # SBOM generation and attestation + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + image: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }} + output-file: sbom.spdx.json + upload-artifact: false + + - name: Attest GHCR SBOM + uses: actions/attest-sbom@v2 + with: + subject-name: ${{ env.GHCR_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom.spdx.json + push-to-registry: true + + - name: Attest Docker Hub SBOM + uses: actions/attest-sbom@v2 + with: + subject-name: ${{ env.DOCKERHUB_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom.spdx.json + push-to-registry: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9172a9f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1 + +# ============================================================================= +# Stage 1: Builder +# ============================================================================= +FROM golang:1.25-alpine AS builder + +# Install git for version info and ca-certificates for HTTPS +# hadolint ignore=DL3018 +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy dependency files first for better layer caching +COPY go.mod go.sum ./ +COPY sigstore/go.mod sigstore/go.sum ./sigstore/ +RUN go mod download + +# Copy source code +COPY . . + +# Build arguments for version information +ARG VERSION=dev +ARG COMMIT=unknown +ARG DATE=unknown + +# Build the binary with security flags +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags="-s -w \ + -X github.com/meigma/blobber/cmd/blobber/cli.version=${VERSION} \ + -X github.com/meigma/blobber/cmd/blobber/cli.commit=${COMMIT} \ + -X github.com/meigma/blobber/cmd/blobber/cli.date=${DATE}" \ + -trimpath \ + -o /blobber \ + ./cmd/blobber + +# ============================================================================= +# Stage 2: Runtime +# ============================================================================= +FROM gcr.io/distroless/static-debian12:nonroot + +# OCI Image Labels +# https://github.com/opencontainers/image-spec/blob/main/annotations.md +LABEL org.opencontainers.image.title="blobber" \ + org.opencontainers.image.description="Push and pull files to OCI registries" \ + org.opencontainers.image.url="https://github.com/meigma/blobber" \ + org.opencontainers.image.source="https://github.com/meigma/blobber" \ + org.opencontainers.image.documentation="https://blobber.meigma.dev" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.vendor="Meigma" + +# Copy the binary (distroless already includes CA certificates) +COPY --from=builder /blobber /usr/local/bin/blobber + +# nonroot tag already runs as non-root user (65532) +ENTRYPOINT ["/usr/local/bin/blobber"]