Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: CI

on:
push:
branches:
- main
pull_request:

jobs:
test:
name: Test (Helm ${{ matrix.helm-version }}, kubectl ${{ matrix.kubectl-version }})
runs-on: ubuntu-latest

strategy:
matrix:
helm-version: ['v4.0.4']
kubectl-version: ['latest']

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true

- name: Install kubectl
uses: azure/setup-kubectl@v4
with:
version: ${{ matrix.kubectl-version }}

- name: Install Helm
uses: azure/setup-helm@v4
with:
version: ${{ matrix.helm-version }}

- name: Run all tests with coverage
run: make test-all

- name: Display coverage summary
if: always()
run: make coverage-report

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ matrix.helm-version }}-${{ matrix.kubectl-version }}
path: coverage/
retention-days: 30
if-no-files-found: ignore

lint:
name: Lint
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: 'v2.7.2'
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ helm-kustomize-plugin

# Code coverage profiles and other test artifacts
*.out
coverage/
coverage.*
*.coverprofile
profile.cov
Expand All @@ -31,6 +32,8 @@ go.work.sum
.env

# Editor/IDE
# .idea/
# .vscode/
.idea/
.vscode/
.claude
dist
helm-kustomize-*.tgz
147 changes: 147 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# AGENTS.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a Helm v4 post-renderer plugin that integrates Kustomize transformations into Helm chart deployments. The plugin runs as a subprocess during Helm rendering, extracting embedded Kustomize files from a special Kubernetes resource, applying them to the rendered manifests, and returning the transformed output.

## Requirements

- Helm v4 with subprocess runtime support for post-renderer plugins
- kubectl (for `kubectl kustomize` command)
- Go 1.21+ for development

## Build and Test Commands

```bash
# Build the plugin binary (automatically runs go fmt)
make build

# Format code manually
go fmt ./...

# Run unit tests (automatically runs golangci-lint)
make test
go test -v ./...

# Run linter manually
golangci-lint run

# Run a specific test
go test -v ./internal/parser -run TestParseManifests

# Run integration tests (requires Helm v4)
make test-integration

# Run all tests (unit + integration) with coverage check and HTML report generation
make test-all

# Display coverage summary (requires coverage.out from prior test run)
make coverage-report

# Install plugin into Helm
make install

# Uninstall plugin from Helm
make uninstall

# Development cycle: rebuild and reinstall
make reinstall

# Clean build artifacts and coverage files
make clean
```

## Architecture

### Data Flow

1. **Input**: Helm renders a chart to YAML manifests containing:
- Regular Kubernetes resources
- A special `KustomizePluginData` resource (apiVersion: `helm.plugin.kustomize/v1`, kind: `KustomizePluginData`)

2. **Processing Pipeline** (`main.go:Run()`):
- **Parse** (`parser` package): Separates `KustomizePluginData` from other resources
- **Extract** (`extractor` package): Creates temporary directory and extracts embedded files
- **Generate**: Writes remaining Helm resources to `all.yaml`
- **Patch** (`kustomize` package): Ensures `all.yaml` is referenced in `kustomization.yaml`
- **Transform**: Runs `kubectl kustomize` on the temporary directory
- **Output**: Returns transformed manifests to Helm

3. **Cleanup**: Temporary directory is automatically removed via defer

### Package Structure

- **`main.go`**: Entry point implementing Helm's `PostRenderer` interface. Orchestrates the entire pipeline.

- **`internal/parser`**: YAML document parsing and resource separation
- Identifies `KustomizePluginData` resources by apiVersion/kind
- Validates the `files` field structure (must be `map[string]string`)
- Enforces single `KustomizePluginData` resource per chart
- Marshals remaining resources back to YAML

- **`internal/extractor`**: Temporary filesystem management
- Uses `os.OpenRoot()` for path-constrained file operations (security feature)
- Creates directory structures from file paths (e.g., `patches/deployment.yaml`)
- Handles cleanup with graceful error reporting

- **`internal/kustomize`**: Kustomization file manipulation and execution
- Parses `kustomization.yaml` preserving all fields via `map[string]any`
- Adds `all.yaml` to `resources` array if not present
- Executes `kubectl kustomize` command

### Key Design Decisions

1. **YAML Parsing Strategy**: Parse once into `map[string]any` to preserve all fields, then manually extract typed fields. This avoids double-parsing overhead while maintaining round-trip fidelity.

2. **Type Conversions**: YAML unmarshaling into `map[string]any` always produces `[]any` for arrays, never typed slices like `[]string`. Manual iteration with type assertions is required.

3. **Security**: Uses `os.OpenRoot()` to constrain all file operations to the temporary directory, preventing path traversal attacks from malicious file paths.

4. **Reserved Filename**: The name `all.yaml` is reserved for Helm-rendered manifests and cannot appear in `KustomizePluginData.files`.

5. **Error Handling**: All type conversions and validations fail fast with descriptive errors rather than silently skipping invalid data.

## KustomizePluginData Resource

The special resource format that triggers kustomize processing:

```yaml
apiVersion: helm.plugin.kustomize/v1
kind: KustomizePluginData
files:
kustomization.yaml: |
resources:
- all.yaml
patches:
- path: patch.yaml

patch.yaml: |
apiVersion: apps/v1
kind: Deployment
# ... patch content
```

**Important constants** (`internal/parser/parser.go`):
- `APIVersion = "helm.plugin.kustomize/v1"`
- `Kind = "KustomizePluginData"`

## Testing

- Unit tests use table-driven patterns with `t.Run()` subtests
- Integration tests in `test-integration.sh` test the full plugin with actual Helm charts
- Example charts in `examples/` directory serve as test fixtures and documentation
- **YAML Output Assertions**: When testing functions that produce YAML output, assert against the final YAML string directly rather than parsing and checking individual fields. This ensures exact output validation and catches formatting issues.

## Kustomize API Preferences

- **NEVER** use deprecated `commonLabels` (deprecated in Kustomize v5.3+). Use `labels` field instead:
```yaml
labels:
- includeSelectors: true
includeTemplates: true
pairs:
app: myapp
```
- `commonAnnotations` is NOT deprecated and can still be used
48 changes: 40 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,28 +1,60 @@
.PHONY: build clean test test-integration test-all install uninstall reinstall
.PHONY: build clean test test-integration test-all install uninstall reinstall \
coverage-report coverage-clean

BINARY_NAME=helm-kustomize-plugin
BUILD_DIR=.
BUILD_DIR=dist
COVERAGE_THRESHOLD=80
COVERAGE_PROFILE=coverage.out
COVERAGE_HTML=coverage.html
COVERAGE_DIR=coverage

build:
go fmt ./...
mkdir -p $(BUILD_DIR)
go build -o $(BUILD_DIR)/$(BINARY_NAME) .
cp plugin.yaml $(BUILD_DIR)/

clean:
rm -f $(BUILD_DIR)/$(BINARY_NAME)
clean: coverage-clean uninstall
rm -rf $(BUILD_DIR)
go clean

test:
go test -v ./...
ifndef GITHUB_ACTIONS
golangci-lint run
endif
go test -v -coverpkg=./... -coverprofile=$(COVERAGE_PROFILE) -covermode=atomic ./...

test-integration: reinstall
./test-integration.sh

test-all: test test-integration
@echo "Checking coverage threshold (${COVERAGE_THRESHOLD}%)..."
@bash -c 'coverage=$$(go tool cover -func=$(COVERAGE_PROFILE) | tail -1 | awk "{print int(\$$3)}"); \
if [ $$coverage -lt $(COVERAGE_THRESHOLD) ]; then \
echo "Coverage $$coverage% is below threshold $(COVERAGE_THRESHOLD)%"; \
exit 1; \
else \
echo "Coverage $$coverage% meets threshold $(COVERAGE_THRESHOLD)%"; \
fi'
@mkdir -p $(COVERAGE_DIR)
@go tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_DIR)/$(COVERAGE_HTML)
@echo "Coverage HTML report generated at $(COVERAGE_DIR)/$(COVERAGE_HTML)"

coverage-report:
@echo "=== Coverage Summary ==="
@go tool cover -func=$(COVERAGE_PROFILE) | tail -1 | awk '{print "Overall Coverage: " $$3}'
@echo ""
@echo "=== Coverage by Package ==="
@go tool cover -func=$(COVERAGE_PROFILE) | grep -E "^github.com/owhelm" | grep -v "total"

coverage-clean:
rm -rf $(COVERAGE_DIR) $(COVERAGE_PROFILE) $(COVERAGE_HTML) helm-kustomize-*.tgz

install: build
helm plugin install .
helm plugin install $(BUILD_DIR)

uninstall:
helm plugin uninstall kustomize
helm plugin uninstall helm-kustomize 2>/dev/null || true

# Development: uninstall, rebuild, and reinstall
reinstall: uninstall build install
reinstall: uninstall build install
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,13 @@ files:
resources:
- all.yaml

commonLabels:
team: platform
cost-center: engineering
compliance: pci
labels:
- includeSelectors: true
includeTemplates: true
pairs:
team: platform
cost-center: engineering
compliance: pci

commonAnnotations:
managed-by: platform-team
Expand Down
37 changes: 4 additions & 33 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
# TODO List - Helm Kustomize Plugin

## Error Handling & Edge Cases

- [ ] Handle invalid or malformed special resources with helpful error messages
- [ ] Improve kustomize execution error messages
- [ ] Add timeout handling for kustomize operations
- [ ] Test error cases: empty KustomizePluginData, missing kustomization.yaml
- [ ] Test error cases: kustomize build failures with clear output
- [ ] Add structured error messages with context
- [ ] Validate KustomizePluginData structure before processing
- [ ] Add error recovery suggestions in messages
- [ ] Test all error paths

## Testing

- [ ] Makefile: use custom `HELM_PLUGINS` path when testing
- [ ] Makefile: add shortcut to verify the example simple-app
- [ ] Add test case: chart without KustomizePluginData (pass-through)
- [ ] Add test case: malformed KustomizePluginData resource
- [ ] Add test case: nested directory structures
- [ ] 100% coverage
- [ ] Add more edge case tests
- [ ] Add performance tests for large charts
- [ ] Test cleanup on error conditions
- [ ] Add test for concurrent plugin usage
- [ ] Document test strategy
- [ ] Test in multiple kubectl versions (broken due to https://github.com/Azure/setup-kubectl/issues/88)

## Documentation

Expand All @@ -33,26 +13,17 @@
- [ ] Installation steps
- [ ] Basic usage example
- [ ] Link to examples
- [ ] Add development guide
- [ ] How to build from source
- [ ] How to run tests
- [ ] How to contribute

## Examples & Samples

- [ ] Add example: multiple patches
- [ ] Add example: labels (not commonLabels, which is deprecated!) usage
- [ ] Add example: image transformations
- [ ] Add example: modify all resources of certain `Kind` with annotations

## Build & Distribution

- [ ] Set up CI/CD pipeline
- [x] Set up CI/CD pipeline
- [ ] Create release automation
- [ ] Add version management
- [ ] Set up renovate for ci.yaml

## Future Enhancements

- [ ] Support for multiple kustomization files
- [ ] Configurable resource naming (alternative to `all.yaml`)
- [ ] Performance optimization for large charts
- [ ] Support helm v3
Loading