Prechádzať zdrojové kódy

docs: add AGENTS.md and AI setup guide for ESO (#6141)

* docs: add AGENTS.md and AI setup guide for ESO

Signed-off-by: Rowan Ruseler <rowanruseler@gmail.com>

* docs: coderabbitai[bot] adjustment

Signed-off-by: Rowan Ruseler <rowanruseler@gmail.com>

* docs: address review feedback on AGENTS.md and AI setup guide

Addresses Skarlso's review feedback on PR #6141. Simplified AGENTS.md build section to reference the Makefile instead of listing targets. Removed the incomplete directory tree and the curated doc list. Fixed the provider doc path reference in the setup guide. Replaced the helm install block with a pointer to the getting-started guide, kept the CRD annotation limit note.

Signed-off-by: Rowan Ruseler <rowanruseler@gmail.com>

* chore: add generator and provider creation instructions

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* add comments from gustavo

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Rowan Ruseler <rowanruseler@gmail.com>
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Rowan Ruseler 2 týždňov pred
rodič
commit
e153797229
3 zmenil súbory, kde vykonal 426 pridanie a 0 odobranie
  1. 223 0
      AGENTS.md
  2. 1 0
      CLAUDE.md
  3. 202 0
      docs/guides/ai-setup-guide.md

+ 223 - 0
AGENTS.md

@@ -0,0 +1,223 @@
+# External Secrets Operator
+
+Kubernetes operator that synchronizes secrets from external providers (AWS Secrets Manager, Vault, GCP Secret Manager, Azure Key Vault, etc.) into Kubernetes Secrets.
+
+## Build and Test
+
+Use `make` targets — refer to the Makefile for available commands. Do not run `go test`, `golangci-lint`, or `helm` directly.
+
+## Project Layout
+
+Single binary built from `main.go`. The **controller** reconciles ExternalSecrets into K8s Secrets. The **webhook** (validates and defaults CRDs) and **certcontroller** (manages webhook TLS) are subcommands registered via `rootCmd.AddCommand()`.
+
+Multi-module repo: `apis/`, `runtime/`, `e2e/`, and each `providers/v1/*/` have their own `go.mod`.
+
+## Non-Obvious Patterns
+
+- `make reviewable` is the gate for PRs. Run it, not individual checks.
+- Helm chart is the source of truth for deploy manifests. `make manifests` generates static YAML from it.
+- Provider docs use MkDocs snippet transclusion (`--8<--`). Auth docs are shared across providers via `docs/snippets/`.
+- CRD tests use snapshot testing. Run `make test.crds.update` to update snapshots after CRD changes.
+- `make update-deps` updates dependencies across all modules at once.
+- Add a `git notes add HEAD` entry on every non-trivial commit. Record key design decisions, trade-offs, and gotchas. Queryable via `git notes show <sha>`.
+- If you discover a non-obvious pattern while implementing, add it here before the PR is merged. Keep entries general — applicable across the codebase, not specific to one provider or feature.
+- Never edit `zz_generated.*` files by hand. They are owned by controller-gen. Modify the source types and run `make generate` (included in `make reviewable`).
+- After everything is committed - **ALWAYS RUN `make check-diff`** - this is the first step where PRs fall apart that LLMs forget - there are a lot of generated code outside of the main `make reviewable` spec like helm chart tests, docs, etc.
+
+## Adding a Provider
+
+A provider is its own Go module under `providers/v1/<name>/` with no build tags on the package itself.
+Build tags live in `pkg/register/<name>.go`.
+
+### API types
+
+- New spec goes in `apis/externalsecrets/v1/secretstore_<name>_types.go`.
+- Add a one-line slot to the discriminator union in `apis/externalsecrets/v1/secretstore_types.go`
+  (the `SecretStoreProvider` struct). The JSON tag is the provider name; `apis/externalsecrets/v1/provider_schema.go`
+  resolves it from the first JSON key of the marshaled union.
+- Auth: nested `*<Name>Auth` struct. Multi-method auth uses `+kubebuilder:validation:MaxProperties=1`. Selector types
+  are `esmeta.SecretKeySelector` and `esmeta.ServiceAccountSelector`.
+- CA: include `CABundle []byte` and `CAProvider *CAProvider` if the backend speaks TLS.
+- v1 API is frozen by default. Net-new provider slots are fine
+
+### Runtime helpers (use these, do not roll your own)
+
+- `runtime/esutils/resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, ref)` for credential resolution. It enforces
+  `ClusterSecretStore` vs `SecretStore` namespace scoping. Pass `store.GetKind()` and the ES namespace.
+- `runtime/esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{...})` for CA bundles.
+- `runtime/esutils.ValidateSecretSelector` / `ValidateReferentSecretSelector` / `ValidateServiceAccountSelector` for spec validation.
+- `runtime/esutils/metadata` for parsing `PushSecretMetadata` into a typed spec.
+- `runtime/constants` for metric label values.
+
+### `SecretsClient` contract
+
+Defined at `apis/externalsecrets/v1/provider.go`. All eight methods are mandatory; `Close` may be a no-op.
+
+- Return `esv1.NoSecretErr` from `GetSecret` when the secret is missing. The reconciler depends on this for `deletionPolicy`.
+- Set `Capabilities()` honestly: `SecretStoreReadOnly`, `SecretStoreWriteOnly`, or `SecretStoreReadWrite`. Read-only
+  providers still implement Push/Delete but return a sentinel error! Do _NOT_ return `nil`!
+- `gjson` is the conventional path extractor for `ref.Property` on JSON payloads.
+
+### Caching (skip unless construction is expensive)
+
+- Per-Provider client cache: `runtime/cache.Must[T](size, cleanup)`. Keyed by `cache.Key{Name, Namespace, Kind}`,
+  versioned by `store.GetObjectMeta().ResourceVersion`. Use this for OIDC, vault leases, token exchange, etc. Default to no cache.
+- Per-secret cache (in the SecretsClient): `expirable.LRU[string, []byte]` with a user-facing `CacheConfig{TTL, MaxSize}`
+  field on the spec.
+
+### Feature flags
+
+Pipeline: helm value to deployment `extraArgs` to cmd flag to `feature.Register` to `Initialize()`.
+
+- Register flags from the provider's `init()` using `runtime/feature.Feature{Flags, Initialize}`.
+- `cmd/controller/root.go` collects them and runs `Initialize` after manager startup.
+- Helm wiring is `extraArgs` in `deploy/charts/external-secrets/values.yaml`, rendered by `templates/deployment.yaml`.
+  Out-of-process SDKs (e.g. bitwarden) ship as a sidecar subchart.
+
+### Registration
+
+Provider package exports three symbols: `NewProvider() esv1.Provider`, `ProviderSpec() *esv1.SecretStoreProvider`,
+`MaintenanceStatus() esv1.MaintenanceStatus`. `ProviderSpec()` must set exactly one field on the union.
+
+Registration lives in `pkg/register/<name>.go`:
+
+```go
+//go:build <name> || all_providers
+package register
+
+import (
+    esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+    foo "github.com/external-secrets/external-secrets/providers/v1/foo"
+)
+
+func init() {
+    esv1.Register(foo.NewProvider(), foo.ProviderSpec(), foo.MaintenanceStatus())
+}
+```
+
+Maintenance values: `MaintenanceStatusMaintained`, `NotMaintained`, `Deprecated` (`apis/externalsecrets/v1/provider_schema_maintenance.go`).
+
+### Wiring
+
+- Add `providers/v1/<name> => ./providers/v1/<name>` to root `go.mod` (alphabetized).
+- `Makefile` honors `PROVIDER ?= all_providers` and passes it as `go build -tags`.
+
+### Documentation
+
+- Write `docs/provider/<slug>.md`. Conventional sections: intro, Authentication or Store Configuration, External Secret Spec / GetSecret, optional PushSecret.
+- YAML examples live in `docs/snippets/<name>-secret-store.yaml`, `<name>-external-secret.yaml`, `<name>-push-secret.yaml`.
+  Pull them in via `{% include '<name>-secret-store.yaml' %}`.
+- Add nav entry to the `Provider:` block in `hack/api-docs/mkdocs.yml`. Order is historical; append at the bottom.
+
+## Adding a Generator
+
+A generator is its own Go module under `generators/v1/<name>/`. Generators are **v1alpha1 only** and are
+**unconditionally compiled** into the binary (no build tags, unlike providers).
+
+The repo ships a scaffold: `esoctl bootstrap generator --name <Name>` (`cmd/esoctl/generator/bootstrap.go`).
+Run it first; the manual steps below are the audit checklist for what it produced and what it skipped.
+
+### What `esoctl bootstrap generator` wires for you
+
+- Creates `apis/generators/v1alpha1/types_<pkg>.go` (CRD types).
+- Creates `generators/v1/<pkg>/{<pkg>.go,<pkg>_test.go,go.mod,go.sum}` from templates in `cmd/esoctl/generator/templates/`.
+- Patches `pkg/register/generators.go` with the import and `genv1alpha1.Register(<pkg>.Kind(), <pkg>.NewGenerator())`.
+- Patches `apis/generators/v1alpha1/types_cluster.go`: enum value, `GeneratorKind<Name>` const, and a field on
+  `GeneratorSpec` (the discriminator union).
+- Adds the `replace` directive to root `go.mod`.
+- Patches `runtime/esutils/resolvers/generator.go` `clusterGeneratorToVirtual` switch.
+- Patches `apis/generators/v1alpha1/register.go` (`<Name>Kind` var + `SchemeBuilder.Register`).
+- Patches `apis/externalsecrets/v1/externalsecret_types.go` `GeneratorRef.Kind` enum. This is the one v1 enum write the
+  bootstrap performs; it is documentation-class, not behavioral.
+
+What it does **NOT** do: ClusterRole RBAC, mkdocs nav, docs, snippets, helm.
+
+### API types
+
+- All generators live in `apis/generators/v1alpha1/`. No v1beta1, no v1.
+- Per-generator file is `types_<name>.go`. Standard shape: `<Name>Spec`, `<Name>` (TypeMeta + ObjectMeta + Spec), `<Name>List`.
+  Most generators have no Status field.
+- Standard markers: `+kubebuilder:object:root=true`, `+kubebuilder:storageversion`, `+kubebuilder:subresource:status`,
+  `+kubebuilder:metadata:labels="external-secrets.io/component=controller"`,
+  `+kubebuilder:resource:scope=Namespaced,categories={external-secrets, external-secrets-generators}`.
+- All concrete generators are `scope=Namespaced`. Cluster-scoped use is delivered by the single `ClusterGenerator`
+  umbrella type (`apis/generators/v1alpha1/types_cluster.go`) which embeds a `GeneratorSpec` discriminator union with
+  `MaxProperties=1` / `MinProperties=1`. Do **NOT** write a `Cluster<Name>` type. Add one field to that union and one
+  `GeneratorKind` enum value.
+
+### `Generator` interface
+
+Defined at `apis/generators/v1alpha1/generator_interfaces.go`. Two methods:
+
+```go
+Generate(ctx, obj *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, GeneratorProviderState, error)
+Cleanup(ctx, obj *apiextensions.JSON, status GeneratorProviderState, kube client.Client, namespace string) error
+```
+
+- Spec arrives as raw `apiextensions.JSON`. YAML-unmarshal it inside `Generate`.
+- Returns the full `map[string][]byte` of generated keys at once. There is no per-key `GetSecret`.
+- `GeneratorProviderState` is `*apiextensions.JSON`, an opaque blob persisted between `Generate` and `Cleanup`.
+- `Cleanup` MUST be idempotent.
+
+### Runtime helpers
+
+- `runtime/esutils/resolvers.SecretKeyRef(ctx, kube, resolvers.EmptyStoreKind, ns, ref)` for credential refs. Generators
+  pass `EmptyStoreKind` because they have no SecretStore; namespace scoping does not apply.
+- `runtime/esutils.FetchServiceAccountToken` for SA-token auth, `esutils.ExtractJWTExpiration` for JWT parsing.
+- AWS-family generators reuse the provider's auth path:
+  `awsauth "github.com/external-secrets/external-secrets/providers/v1/aws/auth"` then `awsauth.NewGeneratorSession(...)`.
+  Vault generator imports `providers/v1/vault` and calls `provider.NewGeneratorClient`. Cross-module imports of providers
+  are normal; wire via `replace` in the generator's own `go.mod`.
+
+### State and lifecycle
+
+Stateless **by default**. Return `nil` for `GeneratorProviderState` from `Generate` and a no-op `Cleanup` (uuid, password,
+ecr, sts all do this).
+
+Stateful generators return a non-nil state. `runtime/statemanager` persists it to a `GeneratorState` CR
+(`apis/generators/v1alpha1/generator_state_types.go`). The `generatorstate` controller runs a finalizer that calls
+`Cleanup` on deletion. If state persistence fails post-Generate, statemanager invokes Cleanup as rollback; if Cleanup
+itself errors, it creates a `GeneratorState` with an immediate `GarbageCollectionDeadline`.
+
+No generator currently uses `runtime/cache.Must` style client caching.
+
+### Registration
+
+Generator package exports two symbols: `NewGenerator() genv1alpha1.Generator` and `Kind() string`. Registration lives in
+`pkg/register/generators.go`:
+
+```go
+import (
+    genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+    foo "github.com/external-secrets/external-secrets/generators/v1/foo"
+)
+
+func init() {
+    genv1alpha1.Register(foo.Kind(), foo.NewGenerator())
+}
+```
+
+`Register` panics on duplicate kinds. Scheme registration is separate, in `apis/generators/v1alpha1/register.go`:
+`<Name>Kind = reflect.TypeFor[<Name>]().Name()` and `SchemeBuilder.Register(&<Name>{}, &<Name>List{})`.
+
+The runtime resolver (`runtime/esutils/resolvers/generator.go`) loads the typed object via the scheme then dispatches to
+the registered `Generator` by kind. ClusterGenerator goes through `clusterGeneratorToVirtual` which materializes a
+synthetic namespaced object from the union spec; every generator must have a case there.
+
+### Feature flags
+
+No precedent. None of the existing generators register `runtime/feature` flags. If you need one, follow the provider
+pattern, but expect to be the first.
+
+### Documentation
+
+- Generator docs live at `docs/api/generator/<name>.md`.
+- YAML snippets in `docs/snippets/<name>-...yaml`, transcluded via `--8<--`.
+- Nav entry goes under `Reference: -> API: -> Generators:` in `hack/api-docs/mkdocs.yml`. Append at the bottom.
+
+### Manual checklist after bootstrap
+
+- ClusterRole rules in `deploy/charts/external-secrets/templates/rbac.yaml` for any new resources the generator reads.
+- Docs page + snippets.
+- mkdocs nav entry.
+- After adding the module to `go.work`, run `go work use` to reconcile the `go` directive version.

+ 1 - 0
CLAUDE.md

@@ -0,0 +1 @@
+@AGENTS.md

+ 202 - 0
docs/guides/ai-setup-guide.md

@@ -0,0 +1,202 @@
+# AI Agent Guide: External Secrets Operator Setup
+
+AI agents: walk the user through each decision below before generating manifests. All decisions apply across providers and platforms.
+
+## Decision Flow
+
+Ask these three questions in order. Each answer narrows the setup.
+
+**1. "How many teams share this cluster?"**
+
+- Multiple teams or tenants → SecretStore per namespace (Option A below)
+- Single team, or dev/staging cluster → ClusterSecretStore (Option B below)
+
+**2. "Which platform runs your cluster, and which secret provider do you use?"**
+
+- The platform determines the auth method. The provider determines which doc to reference.
+- Look up `docs/provider/<provider>.md` for provider-specific configuration. Exception: AWS uses `docs/provider/aws-access.md` for auth and separate docs per service (`aws-secrets-manager.md`, `aws-parameter-store.md`).
+- Default rule: pick platform-native short-lived credentials over static secrets.
+
+**3. "What path or prefix do your secrets follow in the provider?" (e.g., `prod/team-a/*`, `secret/data/myapp/*`)**
+
+- Use the answer to scope credentials. Grant access to those paths only.
+- Scope by: path prefix, tag-based policies (if supported), or separate credentials per namespace.
+- Provider docs under `docs/provider/` include IAM/policy examples.
+
+## SecretStore vs ClusterSecretStore
+
+Store scope controls the blast radius of a compromised namespace.
+
+### Option A: SecretStore per namespace
+
+Each namespace gets its own `SecretStore` with dedicated credentials scoped to that namespace's secrets.
+
+**Choose when:**
+- Multiple teams share the cluster
+- Secrets must stay isolated between namespaces
+
+**What you get:**
+- A compromised namespace can only reach its own secrets
+- Provider-level policies scope per namespace (IAM roles, Vault policies, GCP IAM bindings)
+- Enforcement is structural, not label-based
+
+**Tradeoff:** one credential set per namespace to manage.
+
+### Option B: ClusterSecretStore
+
+One cluster-wide store. All permitted namespaces share it.
+
+**Choose when:**
+- Single team runs the cluster
+- Dev/staging environment
+- Setup speed matters more than isolation
+
+**What you get:**
+- One credential covers all secret access
+- `conditions.namespaceSelector` restricts which namespaces can reference the store, but labels can be misconfigured
+- No per-namespace IAM separation
+
+**Always add namespace conditions when using ClusterSecretStore.**
+
+See `docs/guides/multi-tenancy.md` and `docs/guides/security-best-practices.md`.
+
+## Installation
+
+See `docs/introduction/getting-started.md` for installation instructions. When applying CRDs manually (not via Helm), use `kubectl apply --server-side` — ESO CRDs exceed the 256KB annotation limit.
+
+## What to Generate
+
+Generate these resources once the user answers all three questions. Adapt the provider, region, and paths to match their answers.
+
+### If Option A (SecretStore per namespace):
+
+```yaml
+# 1. ServiceAccount per namespace (auth method dependent)
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: external-secrets
+  namespace: <namespace>
+  annotations:
+    # EKS IRSA example — replace with provider-appropriate annotation
+    eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/eso-<namespace>
+---
+# 2. SecretStore per namespace
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: aws-secrets-manager
+  namespace: <namespace>
+spec:
+  provider:
+    aws:                          # replace with user's provider
+      service: SecretsManager
+      region: <region>
+      auth:
+        jwt:
+          serviceAccountRef:
+            name: external-secrets
+```
+
+### If Option B (ClusterSecretStore):
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ClusterSecretStore
+metadata:
+  name: aws-secrets-manager
+spec:
+  conditions:
+    - namespaceSelector:
+        matchLabels:
+          external-secrets: "enabled"
+  provider:
+    aws:                          # replace with user's provider
+      service: SecretsManager
+      region: <region>
+      auth:
+        jwt:
+          serviceAccountRef:
+            name: external-secrets
+            namespace: external-secrets   # recommended for ClusterSecretStore
+```
+
+**Key difference:** `ClusterSecretStore` should specify `namespace` in `serviceAccountRef` and `secretRef`. Both fields are optional in the API schema, but omitting them in a cluster-scoped store can cause ambiguous resolution.
+
+### ExternalSecret (same for both options):
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: <secret-name>
+  namespace: <namespace>
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: aws-secrets-manager
+    kind: SecretStore              # or ClusterSecretStore
+  target:
+    name: <k8s-secret-name>
+  data:
+    - secretKey: <local-key>
+      remoteRef:
+        key: <provider-path>
+        property: <json-field>      # omit if the secret is a plain string
+```
+
+## How the Sync Loop Works
+
+The controller polls the external provider on the `refreshInterval` cadence and writes the result to the target K8s Secret. `refreshInterval: 1h` polls once per hour. `refreshInterval: 0` disables polling; the controller syncs the secret once at creation and never revisits it.
+
+## Verification
+
+```bash
+# Check store health
+kubectl get secretstore -A          # or clustersecretstore
+
+# Sync status should show SecretSynced
+kubectl get externalsecret -A
+
+# Inspect errors on a specific ExternalSecret
+kubectl describe externalsecret <name> -n <namespace>
+
+# Confirm the K8s Secret exists
+kubectl get secret <name> -n <namespace>
+```
+
+### Troubleshooting
+
+```bash
+# Controller logs show provider errors and sync failures
+kubectl logs -n external-secrets deploy/external-secrets -f
+
+# Events surface auth and permission errors
+kubectl get events -n <namespace> --field-selector involvedObject.name=<externalsecret-name>
+
+# Force a re-sync by annotating the ExternalSecret
+kubectl annotate externalsecret <name> -n <namespace> force-sync=$(date +%s) --overwrite
+```
+
+## Common Pitfalls
+
+1. **Missing `namespace` in refs.** `ClusterSecretStore` should specify `namespace` in `serviceAccountRef` and `secretRef`. Both are `+optional` in the API schema, but omitting them in a cluster-scoped store can cause ambiguous resolution.
+2. **Auth misconfiguration.** Most sync failures trace back to auth. Check the provider-specific doc under `docs/provider/`.
+3. **Secret path mismatch.** Provider key names are exact. A trailing slash, wrong case, or missing path segment returns "not found."
+### AWS-specific pitfalls
+
+4. **IRSA OIDC trust policy mismatch.** The IAM role's trust policy must reference the correct OIDC provider URL for the EKS cluster. A mismatch causes silent auth failure with "unable to create session." Verify with `aws iam get-role --role-name <role> | jq '.Role.AssumeRolePolicyDocument'`.
+5. **PushSecret needs extra permissions.** Syncing secrets *back* to AWS requires `CreateSecret`, `PutSecretValue`, `TagResource`, and `DeleteSecret` in addition to read permissions. See `docs/provider/aws-secrets-manager.md`.
+
+## Security Hardening Checklist
+
+From `docs/guides/security-best-practices.md`:
+
+- [ ] Scope provider credentials to specific secret paths or prefixes
+- [ ] Add `conditions.namespaceSelector` to ClusterSecretStores
+- [ ] Disable unused CRDs and reconcilers in Helm values (`processClusterStore: false`, etc.)
+- [ ] Add NetworkPolicies restricting ESO egress to provider endpoints and kube-apiserver
+- [ ] Grant SecretStore/ClusterSecretStore creation only to cluster admins
+- [ ] Set `scopedRBAC: true` and `scopedNamespace` for high-security namespaces
+- [ ] Use Kyverno or OPA to deny unused providers and restrict `remoteRef.key` patterns
+- [ ] Disable unused providers to shrink the data exfiltration surface