diff --git a/internal/resources/connectivities/init.go b/internal/resources/connectivities/init.go index a0bba757..9f71bc0c 100644 --- a/internal/resources/connectivities/init.go +++ b/internal/resources/connectivities/init.go @@ -28,6 +28,7 @@ import ( authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -181,6 +182,13 @@ func Reconcile(ctx Context, stack *v1beta1.Stack, connectivity *v1beta1.Connecti // namespace; connectivity-core is wired to it via spec.auth below. authKeyID, authSecretName, credReady, err := ensureLedgerCredentials(ctx, stack) if err != nil { + // Missing Credentials CRD (NoMatch) or absent RBAC (Forbidden) is a + // capability gap, not a failure: report pending so it retries once provided. + if apimeta.IsNoMatchError(err) || apierrors.IsForbidden(err) { + setCondition(connectivity, metav1.ConditionFalse, "LedgerCredentialsUnavailable", + "ledger Credentials API unavailable: "+err.Error()) + return NewPendingError().WithMessage("ledger Credentials API unavailable: %s", err.Error()) + } setCondition(connectivity, metav1.ConditionFalse, "LedgerCredentialsFailed", err.Error()) return err } diff --git a/internal/resources/connectivities/init_test.go b/internal/resources/connectivities/init_test.go index a929326d..5a49f520 100644 --- a/internal/resources/connectivities/init_test.go +++ b/internal/resources/connectivities/init_test.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" @@ -888,6 +889,59 @@ func TestTeardownDelegatedAttemptsEveryDeletion(t *testing.T) { } } +// A missing ledger Credentials CRD (NoMatch) or absent RBAC (Forbidden) must +// surface as pending, not as a reconcile failure, so the module reports +// unavailable and retries once the ledger operator provides the capability. +func TestConnectivityReconcilePendingWhenLedgerCredentialsAPIUnavailable(t *testing.T) { + previous := connectivityAvailable + connectivityAvailable = true + t.Cleanup(func() { connectivityAvailable = previous }) + + cases := []struct { + name string + err error + }{ + {"missing CRD", &apimeta.NoKindMatchError{GroupKind: ledgerCredentialsGVK.GroupKind()}}, + {"absent RBAC", apierrors.NewForbidden( + schema.GroupResource{Group: ledgerCredentialsGVK.Group, Resource: "credentials"}, + "connectivity-stack0", errors.New("forbidden by test"))}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ledger := &v1beta1.Ledger{} + ledger.Name = "stack0-ledger" + ledger.Spec.Stack = "stack0" + ledger.Spec.Version = "v3.0.0" + ledger.Status.Ready = true + + base := newReconcileTestContext(t, ledger) + failing := interceptor.NewClient(base.client.(client.WithWatch), interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if obj.GetObjectKind().GroupVersionKind() == ledgerCredentialsGVK { + return tc.err + } + return c.Get(ctx, key, obj, opts...) + }, + }) + ctx := credsTestContext{Context: context.Background(), client: failing, scheme: base.scheme} + + stack := &v1beta1.Stack{} + stack.Name = "stack0" + connectivity := &v1beta1.Connectivity{} + connectivity.Name = "stack0" + connectivity.Spec.Stack = "stack0" + + err := Reconcile(ctx, stack, connectivity, "v1.0.0") + if !core.IsApplicationError(err) { + t.Fatalf("Reconcile() returned %v, want an application (pending) error when the ledger Credentials API is unavailable", err) + } + if len(connectivity.Status.Conditions) == 0 || connectivity.Status.Conditions[0].Reason != "LedgerCredentialsUnavailable" { + t.Fatalf("expected a LedgerCredentialsUnavailable condition, got %#v", connectivity.Status.Conditions) + } + }) + } +} + // The connectivity-api Service is named after the (now fixed) delegated // resource name, so the gateway backend must point at "connectivity-api". func TestConnectivityAPIBackendRef(t *testing.T) {