Add/change an API resource (agent-manager-service)
Spec-first, layered, per-route-authz workflow for the Go control plane. The request/response types and mocks are generated — never hand-write them.
Read first: agent-manager-service/AGENTS.md → "Golden path", "Layering", "Permissions (RBAC)", "Code generation". This skill is the executable checklist; that guide has the why.
Steps (do in order)
- Edit the OpenAPI spec —
agent-manager-service/docs/api_v1_openapi.yaml. Add/modify the path, operation, and schemas. This is the source of truth for request/response shapes. make spec(needs Docker) — regeneratesspec/from the YAML. The wholespec/dir is deleted and rebuilt, so never hand-edit a struct there; your edits would be lost.- Add permission(s) in
rbac/permissions.go— name themresource:verb(:create/:read/:update/:delete, or:manageonly where peers already do). - Implement controller → service → repository (
controllers/→services/→repositories/):- Controller: HTTP only — parse/validate, map result→status, translate sentinel errors→HTTP.
- Service: business logic; depend on repo/client interfaces, never concrete types.
- Repository: persistence only (interface + impl). Add an interface if you introduce a new one.
- Register the route with authz in
api/<resource>_routes.gousingHandleFuncWithValidationAndAuthz(pattern, rbac.<Perm>, ctrl.<Handler>). Every route declares its own permission. - Grant the permission to at least one role in
rbac/predefined_roles.go(PredefinedRolePermissions). An ungranted permission is unreachable. make codegen— only if you added/changed an interface (regenerates wire DI + mocks; needsmoqon PATH). A new repo interface needs a//go:generate moq ...directive above its declaration — copy an existing one.- Write a service unit test — use the
add-service-unit-testskill.
Audit logging
Step 5 audits the route automatically — a mutating route cannot ship unaudited, and api/audit_coverage_test.go fails the build if one does. Two cases need you:
- The build says
cannot derive an action for audited route, or the route'srbac.Permissiondoes not describe what it does (one permission gating several operations, or naming a different resource). Add one line toactionOverridesinaudit/policy.go. - The operation touches credentials, privileges, membership, deployment or deletion. The envelope record cannot say what changed, so add a semantic event — use the
add-audit-eventskill.
Never read a request or response body into a record; pass named fields via audit.Detail, which takes scalars and []string only.
Guardrails
- Multi-tenancy: the service must validate the caller's org against the target resource it loads, not just the path (
RequireOrgMatchis a first-pass filter, not the enforcement layer). - Errors: map
gorm.ErrRecordNotFound→ the specificutils.ErrXxxNotFound; wrap everything else with%w. Never flatten an unexpected error into not-found; never silently fall back to a default on a real error. Compare witherrors.Is. - Context: every I/O method takes
context.Contextfirst and propagates it. - Don't re-fetch a resource already loaded earlier in the request path — pass it down.
Done checklist
-
spec/regenerated (make spec) and committed if the YAML changed. - New permission exists in
rbac/permissions.goand is granted inrbac/predefined_roles.go. - Route registered with an authz registrar (not plain
HandleFuncWithValidationunless deliberate). -
make codegenrun + mocks committed if an interface changed. - Service unit test added;
make test-unitpasses (includesTestEveryMutatingRouteIsAudited). - Semantic audit event added if the operation is security-critical (
add-audit-eventskill). -
go build -tags=integration ./...compiles. - CI lint clean:
golangci-lint run --config .github/linters/.golangci.yaml ./...