From 0fcc9a14d27ef80e293e8e528fcab0ece34ab223 Mon Sep 17 00:00:00 2001 From: Seagyn Davis Date: Tue, 9 Dec 2025 12:32:10 +0200 Subject: [PATCH 1/3] ci: update release process --- .github/workflows/publish.yml | 43 ----------------------------------- .github/workflows/release.yml | 9 +++++++- .releaserc.json | 6 +++++ 3 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 3520038..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish - -on: - release: - types: [published] - -permissions: - packages: write - contents: read - -jobs: - push-image: - name: Build & Push Image - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: Containerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a153dd..99ca138 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,8 +25,15 @@ jobs: with: node-version: 'lts/*' + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies - run: npm install --no-save semantic-release@^24.2.0 @semantic-release/commit-analyzer@^13.0.0 @semantic-release/release-notes-generator@^14.0.1 @semantic-release/github@^11.0.1 + run: npm install --no-save semantic-release@^24.2.0 @semantic-release/commit-analyzer@^13.0.0 @semantic-release/release-notes-generator@^14.0.1 @semantic-release/github@^11.0.1 @semantic-release/exec@^6.0.3 - name: Semantic Release env: diff --git a/.releaserc.json b/.releaserc.json index cb38e9c..c6810f8 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -5,6 +5,12 @@ "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", + { + "publishCmd": "docker build -f Containerfile -t ghcr.io/$GITHUB_REPOSITORY:${nextRelease.version} . && docker push ghcr.io/$GITHUB_REPOSITORY:${nextRelease.version} && docker tag ghcr.io/$GITHUB_REPOSITORY:${nextRelease.version} ghcr.io/$GITHUB_REPOSITORY:latest && docker push ghcr.io/$GITHUB_REPOSITORY:latest" + } + ], "@semantic-release/github" ] } \ No newline at end of file From 543c5a6c16ff7531a9172122edc2e12e35e8739e Mon Sep 17 00:00:00 2001 From: Seagyn Davis Date: Tue, 9 Dec 2025 21:58:32 +0200 Subject: [PATCH 2/3] ci: improve publishing logic --- .github/workflows/release.yml | 37 ++++++++++++++++++++++++++++------- .releaserc.json | 6 ------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99ca138..7d467fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ permissions: contents: write issues: write pull-requests: write + packages: write jobs: release: @@ -25,17 +26,39 @@ jobs: with: node-version: 'lts/*' + - name: Install dependencies + run: npm install --no-save semantic-release@^24.2.0 @semantic-release/commit-analyzer@^13.0.0 @semantic-release/release-notes-generator@^14.0.1 @semantic-release/github@^11.0.1 @semantic-release/exec@^6.0.3 + + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v4 + id: semantic + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + if: steps.semantic.outputs.new_release_published == 'true' + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=${{ steps.semantic.outputs.new_release_version }} + type=raw,value=latest + - name: Log in to the Container registry + if: steps.semantic.outputs.new_release_published == 'true' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Install dependencies - run: npm install --no-save semantic-release@^24.2.0 @semantic-release/commit-analyzer@^13.0.0 @semantic-release/release-notes-generator@^14.0.1 @semantic-release/github@^11.0.1 @semantic-release/exec@^6.0.3 - - - name: Semantic Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release@^24.2.0 + - name: Build and push Docker image + if: steps.semantic.outputs.new_release_published == 'true' + uses: docker/build-push-action@v5 + with: + context: . + file: Containerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.releaserc.json b/.releaserc.json index c6810f8..cb38e9c 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -5,12 +5,6 @@ "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", - [ - "@semantic-release/exec", - { - "publishCmd": "docker build -f Containerfile -t ghcr.io/$GITHUB_REPOSITORY:${nextRelease.version} . && docker push ghcr.io/$GITHUB_REPOSITORY:${nextRelease.version} && docker tag ghcr.io/$GITHUB_REPOSITORY:${nextRelease.version} ghcr.io/$GITHUB_REPOSITORY:latest && docker push ghcr.io/$GITHUB_REPOSITORY:latest" - } - ], "@semantic-release/github" ] } \ No newline at end of file From 1a5b569dd1126f0e6a9d6bfacbd5df299904a5cd Mon Sep 17 00:00:00 2001 From: Seagyn Davis Date: Wed, 10 Dec 2025 11:57:28 +0200 Subject: [PATCH 3/3] ci: support multi-arch --- .github/workflows/ci.yml | 28 +++++++++++++++--- .github/workflows/release.yml | 9 +++++- .kiro/steering/product.md | 18 ++++++++++++ .kiro/steering/structure.md | 54 +++++++++++++++++++++++++++++++++++ .kiro/steering/tech.md | 54 +++++++++++++++++++++++++++++++++++ Containerfile | 10 +++++-- Makefile | 9 +++++- 7 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 .kiro/steering/product.md create mode 100644 .kiro/steering/structure.md create mode 100644 .kiro/steering/tech.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac69f5c..f6fff6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Build Docker image - run: docker build -f Containerfile . + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build multi-arch Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Containerfile + platforms: linux/amd64,linux/arm64 + push: false + cache-from: type=gha + cache-to: type=gha,mode=max container-scan: name: Container Scan @@ -77,8 +87,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Build Docker image - run: docker build -f Containerfile -t local/app:latest . + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image for scanning + uses: docker/build-push-action@v5 + with: + context: . + file: Containerfile + platforms: linux/amd64 + load: true + tags: local/app:latest + cache-from: type=gha - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.28.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d467fb..5a8d62b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,10 @@ jobs: type=raw,value=${{ steps.semantic.outputs.new_release_version }} type=raw,value=latest + - name: Set up Docker Buildx + if: steps.semantic.outputs.new_release_published == 'true' + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry if: steps.semantic.outputs.new_release_published == 'true' uses: docker/login-action@v3 @@ -53,12 +57,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image + - name: Build and push multi-arch Docker image if: steps.semantic.outputs.new_release_published == 'true' uses: docker/build-push-action@v5 with: context: . file: Containerfile + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 0000000..665bfa4 --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,18 @@ +# Product Overview + +Archy is a Kubernetes mutating admission webhook that automatically ensures Pods are scheduled on nodes with compatible architectures in multi-architecture clusters. + +## Core Functionality + +- **Architecture Detection**: Inspects container image manifests to determine supported platforms (amd64, arm64, etc.) +- **Automatic Pod Mutation**: Adds `kubernetes.io/arch` nodeSelector to Pods when a single common architecture is found +- **Multi-Arch Support**: Allows Kubernetes scheduler to handle placement when images support multiple architectures +- **Private Registry Support**: Authenticates with private registries using Pod's imagePullSecrets and ServiceAccount credentials +- **Safety First**: Rejects Pods when images have no common supported architecture + +## Key Behaviors + +- Skips Pods that already have a nodeSelector defined +- Fails closed (rejects Pod) if architecture inspection fails or no common platform exists +- Allows scheduler flexibility when multiple common architectures are available +- Excludes system namespaces (kube-system, kube-public) and self (archy-webhook) from processing \ No newline at end of file diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 0000000..e238c7e --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,54 @@ +# Project Structure + +## Directory Layout + +``` +archy/ +├── cmd/webhook/ # Application entry point +│ └── main.go # HTTP server setup, signal handling, dependency injection +├── pkg/ # Core business logic packages +│ ├── inspector/ # Container registry inspection +│ │ ├── inspector.go # Platform detection interface and implementation +│ │ └── auth.go # Kubernetes authentication for private registries +│ └── webhook/ # Admission webhook logic +│ ├── handler.go # HTTP handler, AdmissionReview processing, Pod mutation +│ └── handler_test.go # Unit tests with mock inspector +├── deploy/ # Kubernetes manifests +│ ├── deployment.yaml # Webhook deployment and service +│ └── webhook-config.yaml # MutatingWebhookConfiguration +├── certs/ # TLS certificates for webhook +├── scripts/ # Build and setup scripts +└── bin/ # Build output directory +``` + +## Code Organization Patterns + +### Package Structure +- **cmd/**: Application entry points only, minimal logic +- **pkg/**: Reusable packages with clear interfaces +- **deploy/**: Infrastructure as code, Kubernetes manifests + +### Interface Design +- `Inspector` interface in `pkg/inspector` enables testing and future extensibility +- Dependency injection pattern in main.go for clean separation + +### Error Handling +- Fail-safe approach: reject Pods when architecture cannot be determined +- Structured error messages in AdmissionResponse +- Context propagation for request timeouts + +### Testing Patterns +- Mock implementations for external dependencies (registries, Kubernetes API) +- Table-driven tests covering edge cases +- Unit tests focus on business logic, not HTTP plumbing + +### Configuration +- Command-line flags for server configuration (port, TLS certs) +- Environment-based configuration for Kubernetes client (in-cluster config) +- Kubernetes-native configuration via MutatingWebhookConfiguration + +### Naming Conventions +- Go standard naming (PascalCase for exported, camelCase for unexported) +- Package names are lowercase, single word when possible +- Interface names end with -er suffix (Inspector) +- Test files use `_test.go` suffix with same package name \ No newline at end of file diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 0000000..130cff5 --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,54 @@ +# Technology Stack + +## Language & Runtime +- **Go 1.25.0** - Primary language +- **Kubernetes API** - Built for Kubernetes admission webhook pattern + +## Key Dependencies +- `github.com/google/go-containerregistry` - Container registry inspection and authentication +- `k8s.io/api` & `k8s.io/apimachinery` - Kubernetes API types and utilities +- `k8s.io/client-go` - Kubernetes client library for accessing secrets/ServiceAccounts + +## Architecture Pattern +- **Admission Webhook**: HTTP server that receives AdmissionReview requests from Kubernetes API server +- **Interface-based Design**: `Inspector` interface allows for testing with mocks +- **Graceful Shutdown**: Proper signal handling and server shutdown + +## Build System + +### Makefile Targets +```bash +# Build for current platform +make build + +# Cross-compile for specific architectures +make build-amd64 # Linux AMD64 +make build-arm64 # Linux ARM64 + +# Clean build artifacts +make clean +``` + +### Build Configuration +- Binary output: `bin/webhook` +- CGO disabled for static binaries +- Linux target for container deployment + +## Development & Testing + +### Running Tests +```bash +go test ./... # Run all tests +go test ./pkg/webhook -v # Run webhook tests with verbose output +``` + +### Local Development +- Uses Tilt for local Kubernetes development +- TLS certificates generated via `scripts/gen-certs.sh` +- Health check endpoint at `/healthz` + +## Deployment +- **Container-based**: Containerfile for building images +- **Kubernetes native**: Deployed as Deployment + Service + MutatingWebhookConfiguration +- **TLS required**: Admission webhooks must use HTTPS +- **RBAC**: Requires access to secrets in target namespaces for private registry authentication \ No newline at end of file diff --git a/Containerfile b/Containerfile index c076202..59c5eaf 100644 --- a/Containerfile +++ b/Containerfile @@ -1,10 +1,14 @@ -FROM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETOS +ARG TARGETARCH WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . -# CGO_ENABLED=0 for static binary -RUN CGO_ENABLED=0 go build -o webhook ./cmd/webhook +# CGO_ENABLED=0 for static binary, build for target architecture +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o webhook ./cmd/webhook FROM scratch WORKDIR /app diff --git a/Makefile b/Makefile index 47bb758..a198da0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ -.PHONY: build build-amd64 build-arm64 clean +.PHONY: build build-amd64 build-arm64 build-multiarch build-multiarch-push clean BINARY_NAME=webhook BUILD_DIR=bin +IMAGE_TAG?=archy-webhook:latest build: go build -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/webhook @@ -12,5 +13,11 @@ build-amd64: build-arm64: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-arm64 ./cmd/webhook +build-multiarch: + docker buildx build --platform linux/amd64,linux/arm64 -t archy-webhook:latest . + +build-multiarch-push: + docker buildx build --platform linux/amd64,linux/arm64 -t $(IMAGE_TAG) --push . + clean: rm -rf $(BUILD_DIR)