These are invariants. Violating them requires explicit human approval.
What it is: Unified CLI for managing Grafana resources across two tiers — a K8s resource tier for dashboards, folders, and other resources via Grafana 12+'s Kubernetes-compatible API, and a Cloud provider tier with pluggable providers for Grafana Cloud products (SLO, Synthetic Monitoring, OnCall, Fleet Management, etc.) using product-specific REST APIs.
Primary values: correctness, API stability, clean layered architecture, extensible provider model
- Strict layer separation:
cmd/contains only CLI wiring (Cobra commands, flag parsing, output formatting) — no business logic. All domain logic lives ininternal/. - Unstructured resource model: Resources are
unstructured.Unstructuredobjects — no pre-generated Go types. Dynamic discovery at runtime, not compile-time. - Folder-before-dashboard ordering: Push pipeline does topological sort — folders are pushed level-by-level before any other resources.
- Config follows kubeconfig pattern: Named contexts with server/auth/namespace. Environment variable overrides follow the same precedence rules as kubectl.
- Processor pipeline is composable: Resource transformations use the
Processorinterface (Process(*Resource) error). Processors compose into ordered slices at defined pipeline points. - Format-agnostic data fetching: Commands fetch all data regardless of
--outputformat; codecs control display, not data acquisition. - Unified provider registration: Each provider has exactly one
init()function containing a singleproviders.Register()call. This atomically populates both the provider registry and the adapter registry —providers.Register()callsadapter.Register()for each entry returned byProvider.TypedRegistrations(). No separateadapter.Register()calls may exist outsideproviders.Register(). - ResourceIdentity on all domain types: Every provider domain type used in a
ResourceAdaptermust implementResourceIdentity(GetResourceName() stringandSetResourceName(string)).TypedCRUDusesGetResourceName()for name extraction andSetResourceName()for name restoration — no function pointers. - TypedCRUD for provider commands: Provider CRUD commands must use
TypedCRUD[T]typed methods (List,Get,Create,Update,Delete) for data access, not raw API clients. This ensures bug fixes to CRUD logic apply to both provider commands and theresourcespipeline automatically. - Schema/Example on Registration structs: Every
adapter.Registrationstruct (populated viaTypedRegistrations()) must include a non-nilSchemafield. These power theschemascommand via the globalSchemaForGVK/ExampleForGVKfunctions —AsAdapter()does not propagate schema or example. TheExamplefield MAY be nil for read-only resources (those without Create/Update support) since examples serve as templates for writable operations.
- Command structure follows
$AREA $NOUN $VERB. Resource and provider commands usegcx {area} {resource-type} {verb}(e.g.gcx slo definitions list,gcx resources get,gcx logs query). Tooling commands (dev,config) may use$AREA $VERBwhen there is no meaningful noun — these operate on the project or CLI itself, not on Grafana resources. - Extension commands nest under their resource type. Domain-specific
operations (
status,timeline,acknowledge) live alongside CRUD verbs, never as top-level commands. Extensions must not duplicate CRUD semantics — if it can be done with list/get/push/pull/delete, it is not an extension. - Positional arguments are the subject, flags are modifiers. The thing being acted on (resource selectors, UIDs, expressions, file paths) is positional. How to act on it (output format, concurrency, dry-run, filters) is a flag.
Every command serves both humans and agents. Agent mode switches defaults (JSON output, no color, no truncation) but does not change available functionality. Explicit flags always override agent mode defaults.
See agent-mode.md for agent mode detection, behavior changes, and opt-out mechanisms.
- All output goes through the codec system. No command writes unstructured prose as its primary output. CRUD data commands output resources. CRUD mutation commands output structured operation summaries. Extension commands output domain-specific structured data.
- Default output is proportional to what is actionable. Mutation summaries enumerate exceptions (failures, skips) and summarize successes by count. Full per-resource detail is opt-in.
- STDOUT is the result, STDERR is the diagnostic. Summary tables and resource data go to stdout. Failure details and progress feedback go to stderr. Both use structured formats (tables or JSON), not unstructured prose.
- Local manifests are clean, portable, and environment-agnostic.
pullstrips server-managed fields and writes a consistent format (default: YAML).pushis idempotent (create-or-update) and treats local files as authoritative. The same manifests can be pushed to any Grafana instance via--contextwithout modification. - Three workflows, one pipeline. Whether used as source-of-truth (edit locally, push to Grafana), backup/rollback (pull periodically, push to restore), or migration/fanout (pull from source instance, push to targets), the push/pull pipeline is the same. The workflow differs only in triggering — manual, CI, or scheduled.
- Folder-before-resource ordering on push. Folders are topologically sorted by parent-child relationships and pushed level-by-level before any non-folder resources.
- Dual CRUD access paths are permanent. Provider commands
(
slo definitions list) are ergonomic shorthands with domain-rich table output. Generic commands (resources get slos.v1alpha1.slo.ext.grafana.app) serve the push/pull pipeline and cross-resource operations. Neither path is deprecated; both are first-class. - JSON/YAML output is identical between both paths. This is enforced
structurally: provider CRUD commands must use their registered
ResourceAdapter(via TypedCRUD) for data access, not raw API clients. Table/wide codecs may diverge — provider tables show domain-specific columns, generic tables show resource-management columns. - Provider-only resources must not mimic adapter verbs. If a resource
does not obey standard list/get/create/update/delete semantics (e.g.,
composite keys, scope-required lookups, query-only endpoints), do not
register it as an adapter. Keep it in the provider command tree only, but
use alternative verbs (
show,describe,search) — neverget,list,create,update,delete. This avoids user confusion: adapter verbs (resources get) and provider verbs should not overlap for resources that behave differently across the two paths. - Sub-resources nest under their parent command. If a resource cannot
be listed or addressed without a parent ID (e.g. alerts require an
alert group), it is a sub-resource. Sub-resources must not be registered as standalone typed
adapters (no
ListFnthat ignores the parent). Instead, expose them as verbs under the parent command:$PARENT $VERB-$CHILD $PARENT_ID(e.g.alert-groups list-alerts <id>). Get-by-ID may still have a standalone adapter if the API supports direct ID lookup without a parent. - Typed resource trajectory. Provider domain types implement
ResourceIdentityfor self-describing identity and are wrapped byTypedObject[T](embeddedmetav1.ObjectMeta+TypeMeta+Spec T) for K8s metadata compliance.TypedCRUD[T]provides both typed methods (returningTypedObject[T]) and unstructured methods (viaAsAdapter()). New providers must implementResourceIdentityon domain types and useTypedCRUDfor both CLI commands and adapter registration.
cmd/may importinternal/;internal/may not importcmd/.- No circular dependencies between packages.
- Provider implementations (
internal/providers/*/) may import core resource types but not other providers. - Query clients (
internal/query/*/) bypass the k8s dynamic client — they call datasource HTTP APIs directly. - PromQL construction uses
github.com/grafana/promql-builder/go/promql, not string formatting. - Provider implementations must use
providers.ConfigLoaderfor config and auth resolution. Providers must not construct HTTP clients or load credentials independently — this ensures consistent env var precedence, secret handling, and auth behavior across all providers. ExternalHTTPClientfor external APIs. Provider clients calling APIs outside the Grafana server (K6 Cloud, OnCall, Synth, Fleet — any domain other thancfg.Host) must useproviders.ExternalHTTPClient(), neverrest.HTTPClientFor(). The k8s transport round-tripper injects the Grafana bearer token on every outgoing request, which conflicts with the product's own auth mechanism.ExternalHTTPClient()returns a shared, well-tuned*http.Clientwith no auth injection — providers set their own auth headers per request.rest.HTTPClientFor()is correct only for calls to the Grafana API itself (e.g. plugin discovery, datasource queries).
- Options pattern for every command: opts struct +
setup(flags)+Validate()+ constructor. - Table-driven tests: All Go tests follow Go wiki conventions.
- Commit format: Title (one-liner) / What (description) / Why (rationale).
- Error messages: Lowercase, no trailing punctuation.
- errgroup concurrency: Bounded parallelism (default 10) for all batch I/O operations.
- All non-trivial functions have unit tests.
make all(lint + tests + build + docs) must pass before merging.GCX_AGENT_MODE=false make allwhen running from agent environments (prevents agent-mode detection from altering doc generation).- No linter suppressions without a comment explaining why.
- CI must pass before merging.
- Architecture docs must stay current with code changes. When adding or
removing packages under
internal/orcmd/, introducing new patterns, changing core abstractions, or adding a provider — updatedocs/architecture/using the structural checks in docs/reference/doc-maintenance.md.