# AGENTS.md

This file provides codebase guidance for AI coding agents working with this repository.

## Project Overview

O-Cloud Manager operator built on OpenShift and ACM (Red Hat Advanced Cluster Management). Implements the O-RAN O2 IMS specification for 5G infrastructure management: bare-metal inventory, cluster provisioning, firmware management, and alarm monitoring via REST APIs.

## Development Commands

Use `make help` to list all available targets. Key commands:

- `make ci-job` - Full CI pipeline locally (format, vet, lint, test, envtest, coverage, bundle-check). Run before submitting PRs. Includes `bundle-check`, which verifies the git tree is clean after code generation — a common CI failure when API or manifest changes aren't regenerated.
- `make test-e2e` - Run e2e tests separately (not included in `ci-job`). Run before submitting PRs that affect controller logic, hardware manager code, or provisioning workflows.
- `make generate && make manifests && make bundle` - Regenerate code after API changes.

### Lint by File Type

Run targeted checks instead of waiting for the full `ci-job`:

- Go files: `make golangci-lint`
- YAML files: `make yamllint`
- Shell scripts: `make shellcheck bashate`
- Markdown files: `make markdownlint` (not included in `ci-job` — run separately)
- All of the above except markdownlint: `make lint`

### Running a Single Test

Pass Ginkgo flags via `ginkgo_flags`:

```bash
make test ginkgo_flags="--focus='should handle alarm'"
make test-envtest ginkgo_flags="--focus='InventoryController'"
```

### Container Build

To build and push to a personal registry, set `IMAGE_TAG_BASE` and `VERSION`:

```bash
make IMAGE_TAG_BASE=quay.io/<your-username>/oran-o2ims VERSION=latest docker-build docker-push
```

## Architecture

### Multi-Service Binary

The project builds a single binary that runs as multiple services in separate containers. Each service has its own cmd package at `internal/service/{service}/cmd/`. The Deployment determines which subcommand each container runs.

### Service Architecture Pattern

Services follow a consistent initialization pattern in `internal/service/{service}/`:

1. Embedded OpenAPI spec for request validation
2. PostgreSQL connection pool (services with persistence)
3. Repository layer implementing a defined interface (`db/repo/`)
4. Infrastructure clients for cross-service communication
5. Authentication/authorization middleware
6. HTTP server with generated OpenAPI handlers

Shared infrastructure lives in `internal/service/common/` (middleware, DB helpers, server config).

REST API code under `generated/` is auto-generated from OpenAPI specs via `//go:generate`. Don't edit generated files — edit the `openapi.yaml` and run `make go-generate`.

### Provisioning Workflow Phases

The ProvisioningRequestController runs a multi-phase state machine:

1. **Validation** - Validate request against ClusterTemplate schema
2. **Hardware Provisioning** - Create NodeAllocationRequest, watch NAR status, create BMC secrets
3. **Cluster Installation** - Render and apply ClusterInstance, monitor ZTP progress
4. **Post-Provisioning** - Apply policy templates, monitor compliance
5. **Upgrades** - Handle cluster upgrade requests

Each phase sets typed conditions on the CR status. Condition helpers are in `internal/controllers/utils/conditions.go`. Condition types and reasons are defined in `api/provisioning/v1alpha1/conditions.go`.

### Database Schema Changes

This project has not reached GA, so there are no production databases to migrate. Do not add new incremental migration files. Instead, modify the existing baseline files in `internal/service/{service}/db/migrations/` in place.

### API Groups and Key CRDs

- `clcm.openshift.io` - ClusterTemplate, ProvisioningRequest, HardwareProfile, NodeAllocationRequest, AllocatedNode
- `ocloud.openshift.io` - Inventory, Location, OCloudSite, ResourcePool

When adding a new CRD, also add a corresponding `gather_resource` call in
`must-gather/gather` so the resource is collected in support archives.

### Design Decisions

The following are accepted design decisions. Do not flag these patterns as
issues during code review.

- **DD-002: Cluster-scoped ProvisioningRequest watch mappers.**
  Watch mappers that enqueue ProvisioningRequests (e.g.,
  `enqueueProvisioningRequestForNAR`) intentionally omit the namespace from
  `NamespacedName`. `ProvisioningRequest` is cluster-scoped (`scope: Cluster`
  in the CRD) and has no namespace.

- **DD-003: OAuth scope verification via groups-to-RBAC mapping.**
  The O-RAN O2 IMS specification requires token scope verification. This
  project does not verify the OAuth `scope` claim directly. Instead, the
  OAuth server translates granted scopes into group memberships, which
  appear in the token's `groups` claim. The OIDC authenticator
  (`auth_oauth.go`) extracts these groups, and the `Authorizer`
  (`auth.go`) maps them to Kubernetes RBAC permissions via
  `SubjectAccessReview`. This provides functionally equivalent
  authorization: a token can only access resources permitted by the
  groups derived from its scopes. The `Authorizer` logs the user's
  groups at DEBUG level on every successful authorization to make this
  chain observable for compliance auditing.

- **DD-004: RBAC role definitions must track public API endpoints.**
  The `reader-role` in `config/rbac/oran_o2ims_user_roles.yaml`
  enumerates inventory paths explicitly rather than using a wildcard,
  because the `deploymentManagers` endpoints expose sensitive credentials
  and are restricted to the `deployer-role`. When adding, removing, or
  renaming any public API endpoint, update the corresponding ClusterRole
  definitions in `config/rbac/oran_o2ims_user_roles.yaml` and the
  role bindings in `config/rbac/oran_o2ims_oauth_role_bindings.yaml`
  to ensure the new endpoint is covered by the correct role. Also update
  the `security` annotations in the relevant `openapi.yaml` to reflect
  which roles grant access.

## Logging Conventions

- Use context-aware slog calls (`slog.InfoContext(ctx, ...)`,
  `slog.ErrorContext(ctx, ...)`, etc.) whenever a `ctx` variable is in
  scope. The `sloglint` linter enforces this. Use `logging.AppendCtx()`
  to add structured attributes to the context at entry points where
  a resource is first retrieved — not at every function boundary.
- Use typed slog attribute constructors (`slog.String("key", value)`,
  `slog.Int(...)`, `slog.Any(...)`, etc.) instead of untyped
  alternating key-value pairs (`"key", value`). The `sloglint`
  `attr-only` rule enforces this at lint time.
- Log errors with `slog.Any("error", err)`, not
  `slog.String("error", err.Error())`. This preserves the error type
  for structured logging handlers and avoids eager `.Error()` calls.
  Always use `"error"` as the key — not `"err"` or `"Error"`.
- Use `slog.String` for values with known string types (resource names,
  hostnames, change types). Use `slog.String("key", uuid.String())`
  for `uuid.UUID` values — `slog.Any` renders UUIDs as base64-encoded
  byte arrays. Reserve `slog.Any` for errors, complex structs, maps,
  and interfaces.
- Use camelCase for log attribute keys with no spaces (e.g.,
  `"clusterId"`, not `"ClusterID"`, `"cluster_id"`, or
  `"cluster id"`).
- Log messages should not end with punctuation or trailing spaces.
- Choose the appropriate log level: use DEBUG for verbose/polling
  output that would clutter normal operation, INFO for significant
  state changes, WARN for recoverable issues, and ERROR for failures
  that need attention.

## Test Conventions

- Use `.ToNot()` for negated Gomega assertions, not `.NotTo()` or
  `.ShouldNot()`. The codebase is standardized on `.ToNot()`.
- Use `Expect(err).ToNot(HaveOccurred())` for asserting no error, not
  `Expect(err).To(BeNil())`. `HaveOccurred()` produces better failure
  messages (prints the error vs just "expected nil").

## Contributing Requirements

- All commits must be signed off with DCO: `git commit -s`
- Commit all files and run `make ci-job` and `make test-e2e` before submitting PRs
- After API changes: `make generate && make manifests && make bundle`
- AI-generated code must use `Co-Authored-By` or `Assisted-By` trailer
- Run lint checks before committing (see [Lint by File Type](#lint-by-file-type))
- When making code changes, ensure test coverage for new code and functional
  changes. If a bug fix or new behavior is added without a corresponding test,
  write one. If an existing scenario is discovered to be untested (e.g., during
  code review), add a test for it in the same commit or PR. Tests should verify
  the specific behavior, not just increase line coverage.
