Skip to content

feat(clusters): add generic kubeconfig-overrides annotation for ClusterKubeconfig - #2249

Open
onuryilmaz wants to merge 14 commits into
mainfrom
feat/generic-kubeconfig-overrides
Open

onuryilmaz wants to merge 14 commits into
mainfrom
feat/generic-kubeconfig-overrides

Conversation

@onuryilmaz

@onuryilmaz onuryilmaz commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #2248.

The kubeconfig controller fully overwrites ClusterKubeconfig.spec.kubeconfig on every reconcile (server URL + CA from the cluster secret, OIDC config from the org). Until now there was no way to override individual fields per cluster — needed e.g. for new-tenant OIDC setups.

This PR introduces greenhouse.sap/kubeconfig-overrides: an annotation on the Cluster resource that lets operators patch any kubeconfig field after the controller builds the spec.

Annotation format

annotations:
  greenhouse.sap/kubeconfig-overrides: |
    {
      "clusters[0].cluster.server": "https://new-tenant-api.example.com:6443",
      "users[0].user.auth-provider.config.client-id": "new-tenant-client-id",
      "users[0].user.auth-provider.config.idp-issuer-url": "https://new-tenant-issuer.example.com",
      "users[0].user.auth-provider.config.client-secret": {
        "secretRef": {"name": "my-tenant-secret", "key": "client-secret"}
      }
    }
  • Keys are dot-separated JSON field names matching the kubeconfig structure; use [0] for array entries (there is always exactly one cluster, user, and context).
  • Values can be either a plain JSON string or a {"secretRef": {"name": "…", "key": "…"}} object — the controller resolves secretRefs at reconcile time, keeping secrets out of the annotation.
  • Applied after the main spec patch, so it wins over greenhouse.sap/oidc-override on any overlapping field.
  • The existing greenhouse.sap/oidc-override annotation is kept for backwards compatibility.

Error handling

Condition Reason
Annotation value is not valid JSON OverrideResolveError
secretRef target secret / key does not exist OverrideResolveError
Dot-path does not exist in the kubeconfig structure OverrideError
k8s patch fails OverridePatchError

Removing the bad annotation causes the controller to self-heal on the next reconcile.

Changes

  • api/well_known.go — new AnnotationKeyKubeconfigOverrides constant
  • internal/controller/cluster/kubeconfig_controller.go — resolveKubeconfigOverrides (secretRef + plain string resolution), applyKubeconfigOverrides (dot-path walker, now takes pre-resolved map), wired into Reconcile after the main spec patch
  • internal/controller/cluster/kubeconfig_controller_test.go — 4 new envtest specs covering OIDC field overrides, secretRef resolution (including missing-secret error), server URL override, and invalid-JSON error handling

Demo

image

Test plan

  • go build ./... passes
  • New envtest specs pass:
    KUBEBUILDER_ASSETS=$(bin/setup-envtest use 1.36.0 -p path) \
      go test ./internal/controller/cluster/ \
      --ginkgo.focus="generic kubeconfig|secretRef values|ReconcileFailedCondition on invalid" \
      --ginkgo.v --ginkgo.no-color -timeout 180s -v
    
  • Full ClusterKubeconfig suite (17 specs) passes with no regressions

@onuryilmaz
onuryilmaz requested a review from a team as a code owner October 11, 2026 21:17
Copilot AI balanced review requested due to automatic review settings October 11, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Separate spec patches cause continuous reconciliation, while nonexistent paths can silently succeed.

2 open findings
What changed in this PR

Adds per-cluster kubeconfig field overrides, including values resolved from Secrets.

Changes:

  • Defines the new annotation.
  • Resolves and applies dot-path overrides.
  • Adds envtest coverage for common override and error scenarios.
File Description
api/​well_known.go Defines the annotation constant.
internal/​controller/​cluster/​kubeconfig_controller.go Implements override resolution and patching.
internal/​controller/​cluster/​kubeconfig_controller_test.go Tests overrides and error handling.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread api/well_known.go Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unknown paths are silently ignored, patch errors use the wrong reason, and one error-path test can pass on stale status.

1 open finding
2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Reject unknown JSON fields during override decoding

internal/​controller/​cluster/​kubeconfig_controller.go:492

json.Unmarshal silently ignores unknown object fields, so a typo such as clusters[0].cluster.serve is accepted, discarded during this decode, and reported as successfully applied instead of producing the documented OverrideError. Decode with unknown-field rejection; map entries under auth-provider.config will remain extensible because that node is a map.

Medium severity Assert the invalid-JSON diagnostic after asynchronous reconciliation

internal/​controller/​cluster/​kubeconfig_controller_test.go:624

This assertion can pass on the missing-secret condition left by the preceding ordered spec: deleting that annotation only schedules an asynchronous reconciliation, while both old and expected messages contain kubeconfig-overrides. Match the invalid-JSON diagnostic so this spec waits for and verifies its own reconciliation.

🧠 Review effort: Balanced

…erKubeconfig

Adds annotation greenhouse.sap/kubeconfig-overrides that accepts a flat
JSON object of dot-path → string overrides applied to the ClusterKubeconfig
spec after the controller builds it. This enables per-cluster overrides of
any kubeconfig field (server URL, CA, OIDC client-id/secret, issuer, namespace)
without requiring a new secret reference per field.

The existing greenhouse.sap/oidc-override annotation is kept for backwards
compatibility; the new annotation is applied after it and wins on conflict.

Closes #2248

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
…tation

Extends greenhouse.sap/kubeconfig-overrides so each value can be either
a plain JSON string or a {"secretRef": {"name": "...", "key": "..."}}
object. The controller resolves secretRefs at reconcile time via the
existing clientutil helper, keeping secrets out of the annotation itself.

A missing or unreadable secret sets KubeconfigReconcileFailedCondition
with reason OverrideResolveError so operators can identify the problem.

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
…cile loops

Resolve overrides before CreateOrPatch and apply them inside the single
mutation closure so the controller writes one stable desired state. The
previous two-patch approach caused the ClusterKubeconfig watch to
re-enqueue the resource on every reconcile.

Also document the secretRef value form in the AnnotationKeyKubeconfigOverrides
constant comment.

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
- Check strconv.Atoi error in setDotPath (errcheck)
- Combine same-type consecutive params in resolveKubeconfigOverrides and
  setDotPath signatures (gocritic paramTypeCombine)
- Use Expect().To(Succeed()) in DeferCleanup instead of discarding error (errcheck)
- Suppress G101 false positive on resource name constant (gosec)

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
@onuryilmaz
onuryilmaz force-pushed the feat/generic-kubeconfig-overrides branch from a9c9d41 to 5dcceaa Compare October 12, 2026 10:01
…s at API level

When resolvedOverrides participated in the desired spec and CreateOrPatch
fails at the Kubernetes API level, classify the failure as OverridePatchError
rather than the generic PatchError so operators can distinguish it from a
plain reconcile patch failure.

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Invalid paths can succeed silently, and some overrides persist after annotation removal.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

…y reconcile

Contexts, CurrentContext, Kind, and APIVersion were only set during
initial creation. If an override wrote to those fields, removing the
annotation would leave the overridden values in place because the
mutation closure only rebuilt Clusters and AuthInfo.

Reset the complete baseline on every CreateOrPatch so that removing
greenhouse.sap/kubeconfig-overrides fully restores controller-managed values.

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>

@abhijith-darshan abhijith-darshan left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion Specing annotations with index and dot paths and merging them would be flaky.
This mirrors the drawbacks of ingress-nginx annotations sprawls and we should avoid it.

A declarative approach while verbose is a better fit due to strong typing and kubernetes CRD validation.

Example - A configOverrides in spec can be done the following way

type ClusterKubeconfigSpec struct {
	....
	ConfigOverrides *ClusterKubeconfigOverrides `json:"configOverrides,omitempty"`
}

type ClusterKubeconfigOverrides struct {
	Clusters       []ClusterItemOverride `json:"clusters,omitempty"`
	Users          []AuthInfoOverride    `json:"users,omitempty"`
	Contexts       []ContextItemOverride `json:"contexts,omitempty"`
	CurrentContext string                `json:"currentContext,omitempty"`
}

type ClusterItemOverride struct {
	Name                        string              `json:"name"`
	Server                      string              `json:"server,omitempty"`
	CertificateAuthorityDataRef *SecretKeyReference `json:"certificateAuthorityDataRef,omitempty"`
}

type AuthInfoOverride struct {
	Name string              `json:"name"`
	OIDC *OIDCConfigOverride `json:"oidc,omitempty"`
}

type OIDCConfigOverride struct {
	ClientID        string              `json:"clientID,omitempty"`
	IssuerURL       string              `json:"issuerURL,omitempty"`
	ClientIDRef     *SecretKeyReference `json:"clientIDRef,omitempty"`
	ClientSecretRef *SecretKeyReference `json:"clientSecretRef,omitempty"`
}

type ContextItemOverride struct {
	Name      string `json:"name"`
	Cluster   string `json:"cluster,omitempty"`
	User      string `json:"user,omitempty"`
	Namespace string `json:"namespace,omitempty"`
}

With this there will be a clean possibility to override by matching by name. by design this extends the possibility to upsert a context or users.

Example -

apiVersion: greenhouse.sap/v1alpha1
kind: ClusterKubeconfig
metadata:
  name: my-cluster
spec:
  configOverrides:
    clusters:
      - name: my-cluster
        server: https://new-tenant-api.example.com:6443
    users:
      - name: oidc@my-cluster
        oidc:
          clientID: new-tenant-client-id
          issuerURL: https://new-tenant-issuer.example.com
          clientSecretRef:
            name: my-tenant-secret
            key: client-secret

Appending a second user and context, a future possibility is left open by name-matched shape

spec:
  configOverrides:
    users:
      - name: admin@my-cluster
        oidc:
          clientID: admin-client-id
          clientSecretRef:
            name: admin-secret
            key: client-secret
    contexts:
      - name: my-cluster-admin
        cluster: my-cluster
        user: admin@my-cluster
        namespace: kube-system

The controller can build this during reconcile and do comparison using go-cmp for early exits. or even hash the overrides.

@github-actions github-actions Bot added size/XL documentation Improvements or additions to documentation helm-charts and removed size/L labels Oct 31, 2026
@github-actions github-actions Bot added size/XXL and removed size/XL labels Oct 31, 2026
onuryilmaz and others added 3 commits October 31, 2026 15:01
…h strongly-typed Spec field

Move kubeconfig override configuration from a dot-path JSON annotation on
Cluster to a ClusterKubeconfigOverrides struct in ClusterKubeconfig.Spec.
This gives CRD validation, strong typing, and name-matched merging instead
of fragile index-based dot paths. Removes the greenhouse.sap/kubeconfig-overrides
and greenhouse.sap/oidc-override annotations entirely.

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
Mark unused cluster parameter in getOIDCInfo as blank identifier and
run gofmt to fix struct field alignment in resolvedClusterOverride.

Signed-off-by: onuryilmaz <onur.yilmaz@sap.com>
@onuryilmaz
onuryilmaz force-pushed the feat/generic-kubeconfig-overrides branch from 5a1b9ba to 58f772b Compare October 31, 2026 14:01

@onuryilmaz onuryilmaz left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the annotation-based dot-path approach with the strongly-typed configOverrides spec field exactly as suggested — all types (ClusterKubeconfigOverrides, ClusterItemOverride, AuthInfoOverride, OIDCConfigOverride, ContextItemOverride) are implemented with name-matched merging. CreateOrPatch already handles early exits when nothing changes, so no additional go-cmp comparison is needed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Removing legacy Cluster-level OIDC annotation handling silently replaces existing custom credentials with organization defaults during reconciliation, potentially breaking authentication and violating promised backward compatibility.

4 open issues
1 resolved since last review
1 file not reviewed
  • api/v1alpha1/zz_generated.deepcopy.go: Generated file

Review effort: Balanced

// }
func (r *KubeconfigReconciler) getOIDCInfo(ctx context.Context, orgName string, cluster *v1alpha1.Cluster) (OIDCInfo, error) {
// getOIDCInfo fetches OIDC configuration from the Organization.
func (r *KubeconfigReconciler) getOIDCInfo(ctx context.Context, orgName string, _ *v1alpha1.Cluster) (OIDCInfo, error) {
Comment on lines +259 to +261
if kubeconfig.Spec.ConfigOverrides != nil {
var oErr error
resolvedOverrides, oErr = resolveSpecOverrides(ctx, r.Client, cluster.Namespace, kubeconfig.Spec.ConfigOverrides)
},
}
if resolvedOverrides != nil {
applyResolvedOverrides(&kubeconfig.Spec.Kubeconfig, resolvedOverrides)
}
r.clientID = uo.OIDC.ClientID
r.issuerURL = uo.OIDC.IssuerURL
if uo.OIDC.ClientIDRef != nil {

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-apis documentation Improvements or additions to documentation feature helm-charts size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(clusters): generic kubeconfig override annotation for ClusterKubeconfig

3 participants