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
77 changes: 77 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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`.
Comment thread
kuudori marked this conversation as resolved.

## 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)
264 changes: 1 addition & 263 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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:

- `<param_name>` - 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
41 changes: 41 additions & 0 deletions docs/conventions/cel.md
Original file line number Diff line number Diff line change
@@ -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`
Loading