This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the main repository for Red Hat OpenShift on Azure (ARO) using the Hosted Control Planes (HCP) architecture. It contains some of the code for the required microservices along with most of the configuration and pipeline definition necessary to deploy it.
make test # Run all unit tests across the Go workspace (requires envtest setup, done automatically)
make lint # Run golangci-lint across all modules
make lint-fix # Run lint with --fix
make fmt # Run goimports across all modules
make verify # Run all verification checks (deepcopy, json-format, yamlfmt, materialize, gomega, schema) — this is what CI runs
make verify-kql # Validate KQL (Kusto Query Language) files
make generate # Regenerate deepcopy, mocks, format, tidy
make generate-kiota # Regenerate the ARM SDK (under test/sdk/), then run licenses + fmt
make tidy # Run go mod tidy across all modules + go work sync
make install-tools # Install all dev tools via bingoThere is no workspace-wide single-test shortcut. cd into the module directory and run:
cd frontend && go test -run TestMyFunction -v ./...
cd internal && go test -run TestSomething -v ./path/to/package/...For integration tests (require Cosmos emulator):
cd test-integration && make test # Runs all integration tests with Cosmos emulator
go test ./test-integration/frontend/... # Frontend integration tests only
go test ./test-integration/backend/... # Backend integration tests onlymake e2e/local # Full local E2E suite
make e2e-local/run-test TEST_NAME="TestName" # Single E2E testmake -C frontend build # Build frontend binary
make -C backend build # Build backend binary
make build-services # Build and push all in-repo service images in parallel (requires ACR login)After modifying config/config*.yaml or schema files:
cd config && make materialize # Re-render configs and update helm fixturesRendered config changes under config/rendered/ must be committed with your PR. Run make verify-materialize to check.
make personal-dev-env # Build, push images, deploy infrastructure (DEPLOY_ENV=pers or swft required)make rebase # Rebase on upstream and re-materialize config (hack/rebase-n-materialize.sh)
make envtest-setup # Download kubebuilder binaries (etcd, kube-apiserver) for controller tests
make record-nonlocal-e2e # Regenerate nonlocal-e2e-specs.txt after adding/removing/renaming E2E tests
make validate-config-pipelines # Validate all pipeline.yaml files against topology and configNote: make test runs envtest-setup automatically, but if you run go test directly in a module with controller tests (e.g. kube-applier), you need KUBEBUILDER_ASSETS set. Use: export KUBEBUILDER_ASSETS=$(make -s envtest-setup)
There's a multi-layered build and deploy system supporting multiple target environments with configs under config/):
- Personal dev
- cloud: "dev"; environment: "pers" (
DEPLOY_ENV=pers) - Local development using
makeand properly-configuredazcommand - Changes must be manually applied by the user by running proper
makecommands - Note: The integrated/shared dev environment (
DEPLOY_ENV=dev) has been decommissioned. Only global infrastructure (shared ACRs, DNS zones) is still deployed to the dev environment via theglobal-pipeline-postsubmitProw job.
- cloud: "dev"; environment: "pers" (
- Production systems
- cloud: "public"; environments: "int", "stg", "prod"
- Deployed via Microsoft ADO and EV2 into Microsoft Int, Stage and Prod environments (hosted in Microsoft Azure tenants)
- For changes to be applied, a PR must be created for
sdp-pipelinesrepo and updated pipelines run manually - After making changes, remind user about this and point at aka.ms/arohcp-pipelines for next steps
- "Dev/*" environments can be accessed with a @redhat.com account and are largely read/write. It's likely that
azis currently logged into it. - "Public/Int" environment can be accessed with a @microsoft account and is mostly read-only. It's possible to
az logininto, but you need to ask the user explicitly to do that. - "Public/Stage" and "Public/Prod" require SRE-level access.
Loosely defined categories of deployed objects:
- Native Azure objects (using bicep files in
dev-infrastructure/).- This includes databases, roles, access definitions, automations, monitoring stack "plumbing", etc. (e.g. global from
dev-infrastructure/global-pipeline.yaml, regional fromdev-infrastructure/region-pipeline.yaml, etc.) - But also two types of AKS clusters: Service Clusters and Management Clusters (
dev-infrastructure/mgmt-pipeline.yaml)
- This includes databases, roles, access definitions, automations, monitoring stack "plumbing", etc. (e.g. global from
- Service Clusters
– One per supported Azure region
- Hosts the Resource Provider (frontend and backend) and Cluster Service
services_svc_pipelinesinMakefilehas a list of deployed services (on top of what's indev-infrastructure/svc-pipeline.yaml)
- Management Clusters
- Multiple per Azure region (number depends on how many HCPs are running in that region)
- Run all the things required to actually run HCPs
services_mgmt_pipelinesinMakefilehas a list of deployed services (on top of what's indev-infrastructure/mgmt-pipeline.yaml)
Loose categorization:
- Deployment pipelines (dirs have
*pipeline.yamlfiles present)dev-infrastructure/has all the bicep files- The rest deploy services to management and service clusters. Most of these do not host the code of the service, just reference already-released images.
- ARO-HCP-specific microservices, like the Resource Provider's
frontend/andbackend/:- These contain Go code in addition to pipelines configs.
- Additional/helper (
test/,hack/,tooling/, etc.)
Incomplete list:
- Frontend: ARM REST API endpoint (
frontend/) - Go service handling Azure ARM API calls - Backend: Internal processing service (
backend/) - Go service for async operations - Cluster Service: Core cluster management (
cluster-service/) - Wraps OCM's Clusters Service with ARO-HCP-specific configuration; manages HCP cluster lifecycle - Maestro: Multi-cluster orchestration (
maestro/) - Handles communication between service and management clusters - Kube-Applier: Kubernetes resource applier (
kube-applier/) - Reconciles desired-state manifests on management clusters - Fleet: Fleet controller (
fleet/) - Service cluster controller for fleet-level operations - Mgmt-Agent: Management cluster agent (
mgmt-agent/) - Agent running on each management cluster - SessionGate: Session management (
sessiongate/) - Handles session gating for ARM requests - Admin: Admin API (
admin/) - Internal admin interface (split intoadmin/clientandadmin/servermodules) - Infrastructure: Azure infrastructure as code (
dev-infrastructure/) - Bicep templates for all Azure resources - Internal: Shared libraries (
internal/) - Common APIs, database, OCM client, tracing utilities - demo: helper scripts to quickly spin up and tear down an HCP cluster (see
demo/README.mdfor usage)
The github.com/Azure/ARO-Tools repo is also a dependency and changes can be suggested for it.
- The frontend must never access kube-applier,
ReadDesireLister, or management cluster APIs directly. All management-cluster state needed by the frontend or admission code must be mirrored throughServiceProviderClusterby the backend. This architectural boundary prevents the frontend from having credentials or network access to management clusters. Concretely: the backend (which legitimately watches management clusters) observes the needed state and writes a distilled form onto theServiceProviderClusterdocument in Cosmos; the frontend prefetchesServiceProviderClusterand admission reads it from the admission context — never from a live management-cluster client. For example,admitClusterVersionIDvalidates a version change againstServiceProviderCluster.Status.DesiredVersionChannels, which the backendControlPlaneActiveVersionscontroller mirrors from the observed HostedCluster. Seeinternal/admission/CLAUDE.mdfor the full rule.
The project uses Go workspaces (see go.work for the Go version and full module list):
- Services:
admin/client,admin/server,backend,frontend,fleet,kube-applier,mgmt-agent,sessiongate - Libraries:
internal,test,test-integration - Tooling:
tooling/hcpctl,tooling/templatize,tooling/secret-sync, etc. - Infrastructure scripts:
dev-infrastructure/scripts/
After adding a module or changing cross-module deps, run make tidy (which includes go work sync).
- Main config:
config/config.yamland overlays - Environment-specific settings in
dev-infrastructure/configurations/ - Service configs use Helm charts in each
deploy/directory
Uses a custom templatize system (tooling/templatize/) that processes:
pipeline.yamlfiles define deployment pipelines- Bicep templates for Azure resource deployment
- Helm charts for Kubernetes service deployment
topology.yaml defines the entire deployment graph as a tree of service groups, each referencing a pipeline.yaml. Entrypoints (Global, Region, Monitoring) are the top-level deployment targets. Run make entrypoint/Region or make pipeline/RP.Frontend to execute specific parts of the tree locally.
config/config.yamlis the main config with Go template variables ({{ .ctx.region }}, etc.)config/config.schema.jsonvalidates the config — update the schema when adding new parameters.make verify-schemachecks that objects useadditionalProperties: false(with explicit exceptions likeextraVarsandfeatures.items), so new fields must be added to the schema or validation will failconfig/config.msft.clouds-overlay.yamlprovides overrides for Microsoft cloud environments- After any config change:
cd config && make materializeto re-render, then commitconfig/rendered/changes - See
docs/configuration.mdfor full details
Each service follows consistent patterns:
Makefile- Build, deploy, and run targetsdeploy/- Helm charts and Kubernetes manifestspipeline.yaml- Deployment pipeline definition- Go services include standard
main.go,go.mod
- Lint tags:
LINT_GOTAGS='${GOTAGS},E2Etests'
-
Every
go func(...)spawned in non-test code mustdefer utilruntime.HandleCrash()(aliask8s.io/apimachinery/pkg/util/runtime) as its first deferred call, so an unhandled panic in the goroutine respectsutilruntime.ReallyCrashand the binary's crash-on-panic policy instead of silently taking down the process. Goroutines whose body is passed to a kube wait helper that already wraps the call (e.g.wait.Until,wait.UntilWithContext,wait.JitterUntil) do not need it — those helpers callHandleCrashinternally. When the goroutine invokes a named function viago fn(...), put thedefer utilruntime.HandleCrash()at the top offnrather than wrapping the call site in a closure. Test code (*_test.go,test/,test-integration/, generated SDK fakes) is exempt. -
Import aliases are enforced by linter. The
.golangci.ymlimportasconfig requires specific aliases. Key ones:arohcpv1alpha1forgithub.com/openshift-online/ocm-sdk-go/aro_hcp/v1alpha1cmv1forgithub.com/openshift-online/ocm-sdk-go/clustersmgmt/v1azcorearmforgithub.com/Azure/azure-sdk-for-go/sdk/azcore/armhcpsdk20240610previewforgithub.com/Azure/ARO-HCP/test/sdk/v20240610preview/...azureclientforgithub.com/Azure/ARO-HCP/backend/pkg/azure/client- OpenShift API packages use
{group}{version}pattern (e.g.configv1)
-
Import ordering is enforced by
gci(configured in.golangci.yml): standard, blank, dot, default,k8s.io,sigs.k8s.io,github.com/Azure,github.com/openshift,github.com/Azure/ARO-HCP. Runmake lint-fixto fix. -
Gomega assertions must have descriptive annotations. CI runs
verify-gomega-assertionsontest/e2e/andtest/util/, which rejects bareExpect().To()/NotTo()calls. Always add a context string:Expect(err).NotTo(HaveOccurred(), "failed to create HCP cluster")— notExpect(err).NotTo(HaveOccurred())Expect(resp.Properties).NotTo(BeNil(), "cluster response Properties was nil")— notExpect(resp.Properties).NotTo(BeNil())
-
Every controller has a name constant and seeds its logger from it. Each controller package defines
const XxxControllerName = "..."and uses that single value for: thenamefield on the controller struct, the workqueueName(which surfaces as a Prometheus label),utils.ContextWithControllerName(ctx, name), andutils.LogValues{}.AddControllerName(name). Hardcoded string literals in those four call sites are a review-blocker — they cause silent drift between metrics labels, ctx values, and log fields. -
Workqueue keys implement
utils.LoggableKey. Any struct used as a controller workqueue key must implementAddLoggerValues(logger logr.Logger) logr.Logger(declared ininternal/utils/context.goalongsideLogValues). The standard implementation builds the key's*azcorearm.ResourceIDand callsutils.LogValues{}.AddLogValuesForResourceID(...)so every log line from a reconcile carries the samesubscription_id/resource_group/resource_id/hcp_cluster_nameset — uniform across backend and kube-applier, and consistent with the Kusto indexes. Backend examples:HCPClusterKey,HCPNodePoolKey,OperationKeyinbackend/pkg/controllers/controllerutils/util.go. Kube-applier examples:ApplyDesireKey,DeleteDesireKey,ReadDesireKeyinkube-applier/pkg/controllers/keys/keys.go. -
Worker loops seed per-key logger via
utils.AddLoggerValues. Every controller'sprocessNext(or equivalent) must calllogger := utils.AddLoggerValues(utils.LoggerFromContext(ctx), key)andctx = utils.ContextWithLogger(ctx, logger)after pulling a key off the queue but before invokingSyncOnce. The backend'sgenericWatchingControllerdoes this once; kube-applier controllers, which run their own worker loops, must do it themselves.
ARM API versions live under internal/api/v<YYYYMMDD>preview/ (e.g. v20240610preview, v20251223preview). Each version directory has a generated/ subdirectory with auto-generated types and a register.go that wires the version into the API registry. Conversion between API versions and internal types happens in the *_methods.go files. The internal (versionless) types live in internal/api/.
E2E tests live in test/e2e/ and use Ginkgo/Gomega. Key conventions:
- E2E test files must not use the
_test.gosuffix (excepte2e_test.goentry point) — the Ginkgo OpenShift extension excludes_test.gofiles during imports. - Every
It()block must acceptcontext.Contextas its first parameter. - Every test must have labels: environment (
labels.RequireNothingorlabels.RequireHappyPathInfra), importance (labels.Critical/High/Medium/Low), and positivity (labels.Positive/labels.Negative). - Framework helpers use explicit API version suffixes:
CreateHCPClusterFromParam20240610,GetHCPCluster20251223, etc. - See
test/AGENTS.mdfor the full E2E design guide and code review standards.
The test-integration/ directory uses a declarative artifact-driven test framework. Tests are defined as numbered step directories (00-load-initial-state/, 01-httpCreate-resource/, etc.) under artifacts/ trees — no Go code changes needed to add a new test case. See test-integration/claude.md for the full step type reference.
Services have zz_fixture_TestHelmTemplate_*.yaml files that are auto-generated by cd config && make materialize. These are test fixtures for Helm chart validation — they must be committed with your PR. If you change a service's Helm chart or config values, re-run materialize to update them.
All .go files require Apache 2.0 license headers. Run make licenses to add them via the addlicense tool. CI will fail if headers are missing.
- Bicep templates in
dev-infrastructure/templates/ - Reusable modules in
dev-infrastructure/modules/ - Parameter files:
*.bicepparam(with.tmpl.bicepparamtemplates)
- PR titles must use Conventional Commits format:
feat:,fix:,docs:,refactor:,test:,chore:, etc. - PR descriptions must explain the "Why", not just the "What", and link to a Jira/GitHub issue.
- Tide is a merge-automation bot — its status is not a CI check; ignore it when evaluating CI.
- See
CONTRIBUTING.mdfor the full PR standards.
Key development tools (installed via make install-tools):
golangci-lint- Go lintingmockgen- Mock generationaddlicense- License header managementyamlfmt- YAML formattingoras- OCI registry interaction
Custom tools in tooling/:
hcpctl- CLI for HCP management and accesstemplatize- Pipeline template processingsecret-sync- Secret management utilitiesprometheus-rules- Monitoring rule generation
docs/generated/service-provider-control-flow.md documents every controller, endpoint writes, external resource effects, and cluster/node-pool/external-auth lifecycles. The ARO Ops bot keeps the reference and its Graphviz sources/PNGs in sync with the code.
Everything under docs/generated/ is generated output, regenerated and committed
separately by the ARO Ops bot on an automated hourly cadence. Do not regenerate
these files inside feature PRs unless specifically requested, to avoid merge conflicts.
The separate regeneration uses the generation prompt
to update docs/generated/service-provider-control-flow.md when source changes touch:
frontend/pkg/frontend/— any handler that writes to Cosmos- Controller implementations or startup registration in backend, fleet, kube-applier, mgmt-agent, sessiongate, or shared informer management — including controllers without Cosmos writes
- Azure/Cluster Service/Kubernetes effects, reconciliation gates, or resource lifecycle dependencies
internal/api/— any struct field that is stored in Cosmosinternal/database/— any change to CRUD operations or precondition logic
Every leaf struct field in internal/api/types_*.go files (the types stored in Cosmos) must have a // Written by: comment listing the actors (frontend endpoints and/or backend controllers) that set it. For example:
// Written by: Frontend PUT Cluster (Create), ClusterClusterServiceCreate
ClusterServiceID *InternalID `json:"clusterServiceID,omitempty"`When adding or modifying a field, update this comment. When adding or modifying a controller or frontend endpoint that writes a field, update the comment on the field it writes.
Several subdirectories have their own AI guidance files with domain-specific detail:
test/AGENTS.md— E2E test design principles, review standards, label reference, and anti-patternstest-integration/claude.md— Step type reference for the declarative integration test frameworkconfig/CLAUDE.md— Config rendering workflow and example commits
Key documentation files: