diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..39fdb26 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,77 @@ +# HyperFleet Adapter + +Event-driven Kubernetes resource manager. Consumes CloudEvents from a message broker, executes configured actions (K8s resource apply, HyperFleet API calls, Maestro ManifestWork), and reports status back. + +Go 1.25.0 · Cobra CLI · Viper config · golangci-lint (bingo-managed) · Tekton CI (Konflux) + +## Verification Checklist + +```bash +make fmt # Format code + imports (golangci-lint fmt with gci) +make lint # golangci-lint (config: .golangci.yml) +make test # Unit tests with race detection (excludes test/ dir) +make test-integration # Integration tests via testcontainers (needs Docker/Podman) +make build # Build binary → bin/hyperfleet-adapter +``` + +`make test-all` runs all of the above plus `make test-helm`. Pre-commit hooks: `make install-hooks`. + +## CLI + +Subcommands: `adapter serve`, `adapter config-dump`, `adapter version`. Config paths via `-c`/`HYPERFLEET_ADAPTER_CONFIG` and `-t`/`HYPERFLEET_TASK_CONFIG`. All flags have env var equivalents — run `adapter serve --help`. + +Dry-run mode: `adapter serve --dry-run-event event.json` processes a single event with mock clients, no broker or cluster needed. + +## Two Config Files + +Adapter loads two configs merged at startup: deployment config (`adapter-config.yaml` — infra, clients, logging) and task config (`adapter-task-config.yaml` — params, preconditions, resources, post-actions). Override rules differ — see Gotchas. Templates in `configs/`. + +## Source of Truth + +| Topic | Location | +|-------|----------| +| Configuration reference | `docs/configuration.md` | +| Adapter authoring guide | `docs/adapter-authoring-guide.md` | +| Metrics & Prometheus queries | `docs/metrics.md` | +| Alerts | `docs/alerts.md` | +| Runbook | `docs/runbook.md` | +| Helm chart | `charts/` | +| CI pipelines | `.tekton/` (Konflux/Tekton PipelineRuns) | + +## Code Conventions + +@docs/conventions/logging.md +@docs/conventions/cel.md + +### Error Handling + +`pkg/errors` provides ServiceError constructors for API-style errors with numeric codes and HTTP status: + +```go +errors.NotFound("cluster %s not found", clusterID) // → *ServiceError +errors.KubernetesError("failed to get resource: %v", err) +``` + +IMPORTANT: These return `*ServiceError`, not `error`. Use `.AsError()` to convert. + +## Boundaries + +- Every CLI flag must have a corresponding env var (Viper convention) + +## Gotchas + +- IMPORTANT: **Two config files, different override rules.** Deployment config supports env/flag overrides via Viper. Task config is pure YAML — env vars do nothing there. Mixing them up wastes debugging time. +- IMPORTANT: **Naming conventions differ by layer.** Config YAML: `snake_case` (`subscription_id`). Go code: `CamelCase` (`SubscriptionID`). Helm values: `camelCase` (`subscriptionId`). Wrong casing silently drops values. +- **Tracing default mismatch.** Binary defaults to tracing ON. Helm chart defaults to OFF. Local `adapter serve` will attempt OTLP export unless you set `HYPERFLEET_TRACING_ENABLED=false`. +- **Integration tests build a container on first run.** `make test-integration` calls `make image-integration-test` if `INTEGRATION_ENVTEST_IMAGE` is unset. First run takes minutes. + +## Non-Obvious Packages + +- `internal/executor/` — event execution pipeline (params → preconditions → resources → post-actions) +- `internal/transportclient/` — unified apply interface abstracting K8s direct and Maestro ManifestWork + +## Links + +- [Architecture Docs](https://github.com/openshift-hyperfleet/architecture) +- [HyperFleet API Spec](https://github.com/openshift-hyperfleet/hyperfleet-api-spec) +- [Broker Library](https://github.com/openshift-hyperfleet/hyperfleet-broker) diff --git a/CLAUDE.md b/CLAUDE.md index bf29639..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,263 +1 @@ -# Claude Code Context for HyperFleet Adapter - -This file provides AI agent context for working with the HyperFleet Adapter codebase. - -## Quick Reference - -**Language:** Go 1.25.0+ -**Build System:** Make -**Test Framework:** Go testing + Testcontainers -**Linter:** golangci-lint (managed via bingo) - -## Essential Commands - -### Build and Verify - -```bash -make build # Build binary → bin/hyperfleet-adapter -make fmt # Format code with goimports -make lint # Run golangci-lint -make tidy # Tidy dependencies -make verify # Run fmt-check and vet -``` - -### Testing - -```bash -make test # Unit tests only (fast) -make test-integration # Integration tests with envtest (unprivileged, CI-safe) -make test-all # All checks: lint, unit, integration, and helm tests -make test-coverage # Generate coverage report -``` - -### Container Images - -```bash -make image # Build image -make image-push # Build and push to quay.io/openshift-hyperfleet/hyperfleet-adapter -QUAY_USER=myuser make image-dev # Build and push to personal Quay -``` - -## Validation Checklist - -Before committing code, run these in order: - -1. `make fmt` - Format code -2. `make lint` - Check linting -3. `make test` - Unit tests -4. `make test-integration` - Integration tests (or `make test-all`) -5. `make build` - Ensure binary builds - -## Project Structure - -``` -pkg/ # Exported packages (can be imported by other projects) -├── constants/ # Shared constants (annotations, labels) -├── errors/ # Error handling utilities with codes -├── health/ # Health and metrics HTTP servers -├── logger/ # Structured logging with context -├── metrics/ # Prometheus metrics recorder -├── otel/ # OpenTelemetry tracing -├── utils/ # General utilities -└── version/ # Version info - -internal/ # Internal packages (not importable) -├── configloader/ # YAML config loading + validation -├── criteria/ # Precondition and CEL evaluation -├── dryrun/ # Dry-run API client and recording transport -├── executor/ # Event execution pipeline (phases) -├── generation/ # Generation tracking -├── hyperfleetapi/ # HyperFleet API client -├── k8sclient/ # Kubernetes client wrapper -├── maestroclient/ # Maestro ManifestWork client -├── manifest/ # Manifest generation and rendering -└── transportclient/ # Unified apply interface - -cmd/adapter/ # Main entry point -test/integration/ # Integration tests -charts/ # Helm chart -configs/ # Config templates and examples -scripts/ # Build scripts -``` - -## Code Conventions - -### Logging - -Always use structured logging with context: - -```go -logger.Info(ctx, "message") -logger.Error(ctx, "error occurred") - -// Add structured fields via context or logger chaining -ctx = logger.WithLogField(ctx, "cluster_id", clusterID) -log := logger.With("resource", name).With("error", err) -``` - -Never use `fmt.Printf` or `log.Println` - use the logger package. - -### Error Handling - -Use typed error constructors from `pkg/errors`: - -```go -return errors.NotFound("cluster %s not found", clusterID) -return errors.KubernetesError("failed to get resource: %v", err) -return errors.ConfigurationError("invalid config: %v", err) -``` - -Always propagate errors up with context, don't silently ignore them. - -### Context Propagation - -All long-running operations must accept `context.Context` as first parameter: - -```go -func ProcessEvent(ctx context.Context, event cloudevents.Event) error -``` - -Use `logger.WithLogField(ctx, key, value)` or `logger.WithLogFields(ctx, fields)` to attach structured fields to the context for logging. - -### CEL Expressions - -CEL expressions in config must use exact field names from the CEL environment: - -- `` - extracted parameters are injected as top-level variables (e.g., `clusterID`, not `params.clusterID`) -- `resources.*` - discovered resources (post-phase), e.g., `resources.myCluster` -- `adapter.*` - adapter metadata (name, status, etc.) -- Custom functions: `now()` (RFC3339 timestamp), `toJson(val)`, `dig(map, "dot.path")` - -### Resource Names - -Configuration uses **snake_case** (Viper convention): - -- `adapter.name`, `clients.hyperfleet_api.base_url` - -Code uses **camelCase** (Go convention): - -- `AdapterName`, `HyperFleetAPIBaseURL` - -Helm uses camelCase (Helm convention) - -- `adapterConfig`, `baseUrl` - -## Boundaries - Do NOT Do This - -### Generated Files - -- **Do not modify** files in `.bingo/` - these are managed by bingo -- **Do not edit** `go.sum` manually - use `make tidy` - -### Testing - -- **Do not skip integration tests** in PRs - they run in CI and catch real issues -- **Do not mock Kubernetes clients** in integration tests - use envtest/K3s -- **Do not use time.Sleep** in tests - use context timeouts or test utilities - -### Configuration - -- **Do not add hardcoded values** - use configuration or environment variables -- **Do not add CLI flags** without also supporting env vars (Viper convention) -- **Do not change config field names** without migration path (breaking change) - -### Dependencies - -- **Do not add dependencies** without license check (Apache 2.0 compatible only) -- **Do not update hyperfleet-broker** without verifying metric compatibility -- **Do not vendor dependencies** - this project uses Go modules - -### Git - -- **Do not force push** to main or release branches -- **Do not amend published commits** - create new commits -- **Do not commit** `.env` files, credentials, or sensitive data - -### API Changes - -- **Do not break backward compatibility** in config schema without version bump -- **Do not change CloudEvent types** without coordinating with HyperFleet API team -- **Do not modify status payload schema** without API spec update - -## Testing Patterns - -### Table-Driven Tests - -Use subtests for multiple scenarios: - -```go -func TestFunction(t *testing.T) { - tests := []struct { - name string - input string - want string - wantErr bool - }{ - {"valid", "input", "output", false}, - {"invalid", "", "", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Function(tt.input) - if (err != nil) != tt.wantErr { - t.Errorf("unexpected error: %v", err) - } - if got != tt.want { - t.Errorf("got %v, want %v", got, tt.want) - } - }) - } -} -``` - -### Integration Test Setup - -Use testcontainers for real dependencies: - -```go -func TestIntegration(t *testing.T) { - ctx := context.Background() - env := testutil.SetupEnvTest(t, ctx) // Sets up K8s API - defer env.Teardown() - // Test with real K8s client -} -``` - -## Common Tasks - -### Adding a New Parameter Extractor - -1. Add extractor to `internal/executor/param_extractor.go` -2. Add tests in same package -3. Document in `docs/configuration.md` -4. Add example in `configs/adapter-task-config-template.yaml` - -### Adding a New Precondition Type - -1. Add evaluator to `internal/criteria/evaluator.go` -2. Add tests with mock clients -3. Update schema in `internal/configloader/types.go` -4. Document in adapter authoring guide - -### Adding Metrics - -1. Define metric in `pkg/metrics/recorder.go` -2. Instrument code with metric calls -3. Add Prometheus query to `docs/metrics.md` -4. Add recommended alert to `docs/alerts.md` - -## Release Process - -Version follows semver (MAJOR.MINOR.PATCH): - -- Version is derived from `APP_VERSION` in `Makefile` (auto-set from git tags via `git describe`) -- Update `CHANGELOG.md` with release notes -- Tag: `git tag -a v0.2.0 -m "Release v0.2.0"` -- Push: `git push origin v0.2.0` -- CI builds and pushes image with version tag - -## Links - -- [Architecture Docs](https://github.com/openshift-hyperfleet/architecture) -- [HyperFleet API Spec](https://github.com/openshift-hyperfleet/hyperfleet-api-spec) -- [Broker Library](https://github.com/openshift-hyperfleet/hyperfleet-broker) +@AGENTS.md diff --git a/docs/conventions/cel.md b/docs/conventions/cel.md new file mode 100644 index 0000000..b8af1ca --- /dev/null +++ b/docs/conventions/cel.md @@ -0,0 +1,41 @@ +# CEL Expressions + +Used in precondition expressions, lifecycle delete conditions, and post-action `when` gates. + +## Variables + +Extracted params are injected as **top-level** names — write `clusterID`, not `params.clusterID`. + +- `resources.*` — discovered resources by alias (e.g., `resources.myCluster`) +- `adapter.*` — adapter metadata (executionStatus, resourcesSkipped, skipReason, errorReason, errorMessage, executionError, resourceErrors). See `adapterMetadataToMap()` in `internal/executor/utils.go` for current fields. + +## Custom Functions + +- `now()` — current time as RFC3339 string +- `toJson(val)` — serialize any value to JSON string +- `dig(map, "dot.path")` — safe nested map access, returns null if missing + +## String Extensions + +`ext.Strings()` is registered — available on string values: + +`charAt`, `indexOf`, `lastIndexOf`, `lowerAscii`, `replace`, `split`, `substring`, `trim`, `upperAscii`, `join` + +## Examples + +```cel +// Precondition: check cluster is ready +resources.managedCluster.status.conditions.exists(c, c.type == "Ready" && c.status == "True") + +// Post-action gate: check execution status +adapter.?executionStatus.orValue("") == "success" + +// Post-action gate: skip when resources were skipped +adapter.?resourcesSkipped.orValue(false) +``` + +## Reference + +- CEL evaluator: `internal/criteria/cel_evaluator.go` +- Custom functions registered: `internal/criteria/cel_evaluator.go:71` (`ext.Strings()`) +- CEL validation at config load: `internal/configloader/validator.go` diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md new file mode 100644 index 0000000..c0a20a2 --- /dev/null +++ b/docs/conventions/logging.md @@ -0,0 +1,28 @@ +# Logging Conventions + +Uses `log/slog` wrapped in `pkg/logger`. Every log call takes `context.Context` as first parameter. + +## Patterns + +```go +// Basic +logger.Info(ctx, "message") + +// Error logging — dominant pattern: attach error to context +errCtx := logger.WithErrorField(ctx, err) +logger.Errorf(errCtx, "Operation failed") + +// Structured fields on context (carried through call chain) +ctx = logger.WithLogField(ctx, "cluster_id", clusterID) +``` + +## Additional API + +- `logger.WithLogFields(ctx, logger.LogFields{...})` — multiple fields at once +- `logger.With("key", val)` — returns new logger with field (not context-based) +- `logger.Without("key")` — returns new logger with field removed + +## Reference + +- Logger interface: `pkg/logger/logger.go` +- Context helpers: `pkg/logger/context.go`