This guide covers the development workflow, tooling, and testing setup for the StorageGrid Operator.
| Tool | Version | Purpose |
|---|---|---|
| Go | 1.25+ | Build and test |
| Docker | — | Container image builds |
| kubectl | — | Cluster interaction |
| yq | 4.x | Chainsaw values sanitization |
| Make | — | Build automation |
Most development tools (controller-gen, kustomize, golangci-lint, chainsaw, etc.) are downloaded automatically into bin/ by Make targets.
api/v1alpha1/ # CRD type definitions (StorageGrid, S3Tenant, S3Bucket, etc.)
cmd/ # Operator entrypoint
internal/
controller/ # Reconciliation logic for all CRDs
webhook/ # Admission webhooks
pkg/
grid/ # StorageGrid API client and business logic
kube/ # Reusable Kubernetes utility functions
s3/ # S3 client operations
config/ # Kustomize manifests (CRDs, RBAC, deployment, webhooks)
docs/architecture/ # Architecture docs (controller patterns, separation of concerns, etc.)
test/e2e/ # End-to-end tests (Kind-based and Chainsaw)
hack/ # Development scripts
See docs/architecture/ for detailed design documentation.
# Install all tool dependencies
make kustomize controller-gen envtest golangci-lint chainsaw
# Generate CRDs and deepcopy methods
make manifests generate
# Run unit tests
make test
# Build the operator binary
make build
# Build and push the Docker image
make docker-build-and-pushAfter modifying any *_types.go file under api/v1alpha1/:
make manifests generateThis regenerates:
- CRD manifests in
config/crd/bases/ - RBAC role in
config/rbac/role.yaml - DeepCopy methods in
zz_generated.deepcopy.go
# Install CRDs into the current cluster
make install
# Run the operator against the current kubeconfig
make runmake lint # Check for issues
make lint-fix # Auto-fix where possible
make lint-config # Verify linter configurationThe default registry is bedag/storagegrid-operator. Most developers won't have push access to it. Override IMAGE_REGISTRY to use your own:
# Build and push to your own registry
make docker-build-and-push IMAGE_REGISTRY=my-registry.example.com/storagegrid-operator
# Build only (no push)
make docker-build IMAGE_REGISTRY=my-registry.example.com/storagegrid-operatorThe image is automatically tagged with :latest, :$GIT_COMMIT, and :$GIT_BRANCH or :$GIT_TAG.
When deploying to a cluster, set IMG to match:
make deploy IMG=my-registry.example.com/storagegrid-operator:latestmake testUses envtest for controller tests with a real API server but no real cluster.
The project uses Kyverno Chainsaw for declarative e2e tests. Tests are located under test/e2e/chainsaw/.
-
Configure git filters (one-time, prevents committing real cluster values):
make setup-git-filters
This registers a git clean filter that automatically sanitizes
values.yamlandvalues-existing.yamlwhen staging. Your local copies keep real values; committed versions containREPLACE_MEplaceholders. Requiresyq. -
Configure test values — values are prompted interactively the first time you run a test target. You can also edit directly:
test/e2e/chainsaw/values.yaml— for fresh infrastructuretest/e2e/chainsaw/values-existing.yaml— for pre-existing StorageGrid with namespace prefix
Key fields:
Field Description namespacePrefixPrefix for test namespaces (empty for no prefix) storageGrid.nameName of the StorageGrid CR in the cluster tenantClass.nameName of the S3TenantClass CR alternateTenantClass.nameSecond S3TenantClass for class-change tests (optional)
# All chainsaw tests — fresh infrastructure
make test-chainsaw
# All chainsaw tests — existing StorageGrid
make test-chainsaw-existing
# S3Tenant tests only — fresh infrastructure
make e2e-s3tnt
# S3Tenant tests only — existing StorageGrid
make e2e-s3tnt-existingIf your values file still contains REPLACE_ME placeholders, the Make target will launch an interactive prompt to configure them before running tests.
Pass extra flags to Chainsaw via CHAINSAW_ARGS:
# Pause on failure for interactive debugging
make e2e-s3tnt-existing CHAINSAW_ARGS="--pause-on-failure"
# Or export for the whole session
export CHAINSAW_ARGS="--pause-on-failure"
make e2e-s3tnt-existingtest/e2e/chainsaw/
.chainsaw.yaml # Config for fresh infrastructure
.chainsaw-existing.yaml # Config with namespace prefix support
values.yaml # Test values (git-sanitized)
values-existing.yaml # Test values for existing infra (git-sanitized)
s3tenant/
_step-templates/ # Reusable step templates
verify-account-set-delete-policy.yaml
lifecycle/ # Full create → update → delete cycle
deletion-protection/ # Annotation-based deletion protection
... other test scenarios
Each test is a chainsaw-test.yaml in its own directory. Key conventions:
- Top-level bindings: Define
tenantName(and any other test-scoped names) inspec.bindingsand reference with($tenantName)in YAML resources or$tenantNamein scripts. - Values references: Use
($values.storageGrid.name),($values.tenantClass.name), etc. for cluster-specific values. - Step templates: Reuse shared logic via
use.templatereferencing files in_step-templates/. - Cleanup: If a test creates resources with deletion protection or other guards, add a
cleanupblock to remove the guard before Chainsaw's auto-cleanup runs. - Scripts vs native operations: Prefer native Chainsaw operations (
assert,patch,deletewithexpect) over scripts. Use scripts only when there's no native equivalent (e.g., capturing secret values, polling for deletion).
Example binding pattern:
spec:
bindings:
- name: tenantName
value: my-test
steps:
- name: create
try:
- create:
resource:
apiVersion: s3.bedag.ch/v1alpha1
kind: S3Tenant
metadata:
name: ($tenantName) # JMESPath — for YAML resources
- name: check-secret
try:
- script:
content: |
kubectl get secret $tenantName-admin-credentials -n $NAMESPACE # env var — for scriptsTwo configuration files support different environments:
| File | Use Case |
|---|---|
.chainsaw.yaml |
Fresh infrastructure, no namespace prefix |
.chainsaw-existing.yaml |
Existing cluster with namespace prefix (join('-', [$values.namespacePrefix, $namespace])) |
Timeouts: apply 30s, assert 2m, delete 2m, cleanup 2m. Tests use failFast mode.
The chainsaw values files contain environment-specific names that should not be committed. A git clean filter handles this automatically:
Working copy (real values) ──git add──▶ Clean filter (yq) ──▶ Staged with REPLACE_ME
How it works:
.gitattributesassigns thechainsaw-valuesfilter to both values filesmake setup-git-filtersregisters the filter in your local.git/config- On
git add,hack/sanitize-chainsaw-values.shreplaces all values withREPLACE_ME - Your local files are never modified — only the staged version is sanitized
Setup: make setup-git-filters (required once per clone)
Requires: yq — if not installed, the filter exits with an error and the commit is blocked.
Run make help for the full list. Key targets:
| Target | Description |
|---|---|
make manifests generate |
Regenerate CRDs, RBAC, and DeepCopy after type changes |
make test |
Unit tests with envtest |
make lint |
Lint with golangci-lint |
make build |
Build operator binary |
make docker-build-and-push |
Build and push container image |
make install |
Install CRDs into cluster |
make run |
Run operator locally against current kubeconfig |
make deploy / undeploy |
Deploy/remove operator in cluster |
make setup-git-filters |
Configure git clean filter for values sanitization |
make e2e-s3tnt-existing |
Run S3Tenant e2e tests against existing StorageGrid |