From bd2b6c4b49ece9741db01d3013bbe6f8f837d3de Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 5 Nov 2026 21:02:51 +0300 Subject: [PATCH] fix(backups): leave the credentials Secret alone when BucketInfo has no endpoint A BucketInfo without an endpoint differs from every credentials Secret that has one, so the gate forced the bucket release on every throttle window. That render cannot converge: an empty endpoint would be written into the Secret every driver reads, and with the key absent the template's trimPrefix fails and parks the release in Failed. Assisted-by: LLM Signed-off-by: Alexey Artamonov --- internal/backupcontroller/default_objects_gate.go | 6 ++++-- internal/backupcontroller/default_objects_gate_test.go | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/backupcontroller/default_objects_gate.go b/internal/backupcontroller/default_objects_gate.go index b99f67b41..9b6fc0ede 100644 --- a/internal/backupcontroller/default_objects_gate.go +++ b/internal/backupcontroller/default_objects_gate.go @@ -438,7 +438,9 @@ func (g *DefaultObjectsGate) Check(ctx context.Context) ([]string, bool, error) // writes for it. The COSI Secret being absent (the BucketAccess is being // re-granted) leaves nothing to compare against and is not staleness, and // neither is a source Secret the bucket chart does not render: the COSI Secret -// itself (the projector accepts one) or one without the endpoint key. +// itself (the projector accepts one) or one without the endpoint key. A +// BucketInfo without an endpoint is not a target either: rendering from it +// writes an empty endpoint, or fails the release when the key is absent. func (g *DefaultObjectsGate) staleCredentials(ctx context.Context, src *corev1.Secret) (string, error) { if g.CredentialsCOSISecretName == "" || g.CredentialsHelmRelease.Name == "" || g.CredentialsHelmRelease.Namespace == "" { return "", nil @@ -466,7 +468,7 @@ func (g *DefaultObjectsGate) staleCredentials(ctx context.Context, src *corev1.S } want := strings.TrimPrefix(strings.TrimPrefix(info.Spec.SecretS3.Endpoint, "https://"), "http://") have := string(src.Data["endpoint"]) - if have == want { + if want == "" || have == want { return "", nil } return fmt.Sprintf("Secret/%s (endpoint %q, COSI Secret has %q)", g.Config.SourceSecretName, have, want), nil diff --git a/internal/backupcontroller/default_objects_gate_test.go b/internal/backupcontroller/default_objects_gate_test.go index befaedabe..bed60053f 100644 --- a/internal/backupcontroller/default_objects_gate_test.go +++ b/internal/backupcontroller/default_objects_gate_test.go @@ -851,6 +851,10 @@ func TestCheckCredentialsEndpointComparison(t *testing.T) { {"bare host", "s3.example.com", false}, {"other host", "https://s3.new.example.com", true}, {"other port", "https://s3.example.com:8443", true}, + // A forced render from a BucketInfo with no endpoint would write an + // empty endpoint into the Secret every driver reads (or, with the key + // absent, fail the bucket release's render), never a working one. + {"no endpoint in BucketInfo", "", false}, } { t.Run(tc.name, func(t *testing.T) { objs := append([]runtime.Object{helmReleaseObject(), credentialsHelmReleaseObject()}, allDefaultObjects()...)