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/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..5a8d62b 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: @@ -26,9 +27,45 @@ jobs: 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 + 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 }} - run: npx semantic-release@^24.2.0 + + - 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: 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 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - 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)