From e14c5a01ea459a1a033625af1dcf2ad8e65b4635 Mon Sep 17 00:00:00 2001 From: Ankit Jha Date: Wed, 30 Sep 2026 20:15:33 +0530 Subject: [PATCH] Release AutoscalingRunnerSet deletion when the GitHub config secret is gone (#4688) Signed-off-by: Ankit Jha --- .../autoscalingrunnerset_controller.go | 31 ++- .../autoscalingrunnerset_controller_test.go | 179 ++++++++++++++++++ .../secretresolver/secret_resolver.go | 53 ++++-- .../secretresolver/secret_resolver_test.go | 135 +++++++++++++ 4 files changed, 379 insertions(+), 19 deletions(-) create mode 100644 controllers/actions.github.com/secretresolver/secret_resolver_test.go diff --git a/controllers/actions.github.com/autoscalingrunnerset_controller.go b/controllers/actions.github.com/autoscalingrunnerset_controller.go index 1fe9e90b..8cdb7ac3 100644 --- a/controllers/actions.github.com/autoscalingrunnerset_controller.go +++ b/controllers/actions.github.com/autoscalingrunnerset_controller.go @@ -27,6 +27,7 @@ import ( "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" "github.com/actions/actions-runner-controller/build" + "github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver" "github.com/actions/scaleset" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -1220,7 +1221,17 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex } actionsClient, err := r.GetActionsService(ctx, autoscalingRunnerSet) - if err != nil { + switch { + case errors.Is(err, secretresolver.ErrNotFound): + // A secret or config map the client is built from is gone, from Kubernetes or the vault. Retrying cannot bring it back, + // so release the autoscaling runner set instead of leaving it stuck in Terminating. + logger.Error( + err, + "A secret or config map required to reach the Actions service no longer exists in Kubernetes or the configured vault. The runner scale set cannot be deregistered and must be deleted manually from the Actions service", + "runnerScaleSetId", runnerScaleSetID, + ) + return r.removeRunnerScaleSetIDAnnotation(ctx, autoscalingRunnerSet, runnerScaleSetID, logger) + case err != nil: logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") return err } @@ -1235,11 +1246,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex return err } - original := autoscalingRunnerSet.DeepCopy() - delete(autoscalingRunnerSet.Annotations, runnerScaleSetIDAnnotationKey) - - if err := r.Patch(ctx, autoscalingRunnerSet, client.MergeFrom(original)); err != nil { - logger.Error(err, "Failed to remove runner scale set ID annotation after deleting the runner scale set", "runnerScaleSetId", runnerScaleSetID) + if err := r.removeRunnerScaleSetIDAnnotation(ctx, autoscalingRunnerSet, runnerScaleSetID, logger); err != nil { return err } @@ -1247,6 +1254,18 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex return nil } +func (r *AutoscalingRunnerSetReconciler) removeRunnerScaleSetIDAnnotation(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, runnerScaleSetID int, logger logr.Logger) error { + original := autoscalingRunnerSet.DeepCopy() + delete(autoscalingRunnerSet.Annotations, runnerScaleSetIDAnnotationKey) + + if err := r.Patch(ctx, autoscalingRunnerSet, client.MergeFrom(original)); err != nil { + logger.Error(err, "Failed to remove runner scale set ID annotation", "runnerScaleSetId", runnerScaleSetID) + return err + } + + return nil +} + func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, log logr.Logger) (ctrl.Result, error) { r.ResourceCache.ephemeralRunnerSet.Delete(autoscalingRunnerSet) desiredRunnerSet, err := r.newEphemeralRunnerSet(autoscalingRunnerSet) diff --git a/controllers/actions.github.com/autoscalingrunnerset_controller_test.go b/controllers/actions.github.com/autoscalingrunnerset_controller_test.go index a8412096..2c47261f 100644 --- a/controllers/actions.github.com/autoscalingrunnerset_controller_test.go +++ b/controllers/actions.github.com/autoscalingrunnerset_controller_test.go @@ -32,6 +32,7 @@ import ( "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient" scalefake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake" "github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver" + "github.com/actions/actions-runner-controller/vault" "github.com/actions/scaleset" ) @@ -2853,6 +2854,184 @@ var _ = Describe("Test AutoscalingRunnerSet with a stale runner scale set", Orde ).Should(Succeed(), "the finalizer should be removed even though the runner scale set is already gone") }) }) + + Context("When the GitHub config secret is gone before the AutoscalingRunnerSet is deleted", func() { + var ctx context.Context + var mgr ctrl.Manager + var autoscalingNS *corev1.Namespace + var configSecret *corev1.Secret + var autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet + + BeforeEach(func() { + ctx = context.Background() + autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) + configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) + + controller := &AutoscalingRunnerSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: logf.Log, + ControllerNamespace: autoscalingNS.Name, + DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", + ResourceBuilder: ResourceBuilder{ + ResourceCache: newTestResourceCache(), + SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient( + scalefake.WithClient( + scalefake.NewClient( + scalefake.WithGetRunnerGroupByName(&scaleset.RunnerGroup{ID: 1, Name: "testgroup"}, nil), + scalefake.WithGetRunnerScaleSetByIDFunc(func(_ context.Context, runnerScaleSetID int) (*scaleset.RunnerScaleSet, error) { + return &scaleset.RunnerScaleSet{ID: runnerScaleSetID, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil + }), + scalefake.WithGetRunnerScaleSet(nil, nil), + scalefake.WithCreateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: freshRunnerScaleSetID, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil), + ), + ), + )), + }, + } + Expect(controller.SetupWithManager(mgr)).To(Succeed(), "failed to setup controller") + startManagers(GinkgoT(), mgr) + + autoscalingRunnerSet = newAutoscalingRunnerSet(autoscalingNS.Name, configSecret.Name, nil) + Expect(k8sClient.Create(ctx, autoscalingRunnerSet)).To(Succeed(), "failed to create AutoScalingRunnerSet") + + Eventually( + func() (string, error) { + current := new(v1alpha1.AutoscalingRunnerSet) + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(autoscalingRunnerSet), current); err != nil { + return "", err + } + return current.Annotations[runnerScaleSetIDAnnotationKey], nil + }, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(Equal(strconv.Itoa(freshRunnerScaleSetID)), "the runner scale set should be registered") + }) + + arsGone := func() error { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(autoscalingRunnerSet), new(v1alpha1.AutoscalingRunnerSet)) + if errors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + return fmt.Errorf("AutoScalingRunnerSet is not deleted") + } + + It("removes the finalizer instead of retrying forever", func() { + Expect(k8sClient.Delete(ctx, configSecret)).To(Succeed(), "failed to delete the GitHub config secret") + Expect(k8sClient.Delete(ctx, autoscalingRunnerSet)).To(Succeed(), "failed to delete AutoScalingRunnerSet") + + Eventually( + arsGone, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "a missing GitHub config secret should not keep the AutoScalingRunnerSet in Terminating") + }) + + It("keeps the finalizer when the Actions client fails for any other reason", func() { + broken := configSecret.DeepCopy() + broken.Data = map[string][]byte{} + Expect(k8sClient.Update(ctx, broken)).To(Succeed(), "failed to break the GitHub config secret") + Expect(k8sClient.Delete(ctx, autoscalingRunnerSet)).To(Succeed(), "failed to delete AutoScalingRunnerSet") + + Consistently( + func() error { + return k8sClient.Get(ctx, client.ObjectKeyFromObject(autoscalingRunnerSet), new(v1alpha1.AutoscalingRunnerSet)) + }, + 3*time.Second, + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "a secret that exists but cannot be used should keep the AutoScalingRunnerSet in Terminating") + + restored := new(corev1.Secret) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(configSecret), restored)).To(Succeed(), "failed to get the GitHub config secret") + restored.Data = configSecret.Data + Expect(k8sClient.Update(ctx, restored)).To(Succeed(), "failed to restore the GitHub config secret") + + Eventually( + arsGone, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "deletion should complete once the GitHub config secret is usable again") + }) + }) + + Context("When the vault proxy credential secret is gone before the AutoscalingRunnerSet is deleted", func() { + It("removes the finalizer instead of retrying forever", func() { + ctx := context.Background() + autoscalingNS, mgr := createNamespace(GinkgoT(), k8sClient) + + controller := &AutoscalingRunnerSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: logf.Log, + ControllerNamespace: autoscalingNS.Name, + DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", + ResourceBuilder: ResourceBuilder{ + ResourceCache: newTestResourceCache(), + SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient()), + }, + } + Expect(controller.SetupWithManager(mgr)).To(Succeed(), "failed to setup controller") + startManagers(GinkgoT(), mgr) + + proxySecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "vault-proxy-credentials", Namespace: autoscalingNS.Name}, + Data: map[string][]byte{"username": []byte("user"), "password": []byte("pass")}, + } + Expect(k8sClient.Create(ctx, proxySecret)).To(Succeed(), "failed to create the vault proxy credential secret") + + // The vault is never reached: resolving the proxy credentials fails first. + autoscalingRunnerSet := newAutoscalingRunnerSet(autoscalingNS.Name, "github-config-in-vault", registeredAnnotations(freshRunnerScaleSetID)) + autoscalingRunnerSet.Spec.VaultConfig = &v1alpha1.VaultConfig{ + Type: vault.VaultTypeAzureKeyVault, + AzureKeyVault: &v1alpha1.AzureKeyVaultConfig{ + URL: "https://vault.example.com", + TenantID: "tenant", + ClientID: "client", + CertificatePath: "/nonexistent/cert.pem", + }, + Proxy: &v1alpha1.ProxyConfig{ + HTTPS: &v1alpha1.ProxyServerConfig{ + Url: "http://proxy.example.com:3128", + CredentialSecretRef: proxySecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, autoscalingRunnerSet)).To(Succeed(), "failed to create AutoScalingRunnerSet") + + Eventually( + func() (bool, error) { + current := new(v1alpha1.AutoscalingRunnerSet) + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(autoscalingRunnerSet), current); err != nil { + return false, err + } + return controllerutil.ContainsFinalizer(current, autoscalingRunnerSetFinalizerName), nil + }, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(BeTrue(), "the finalizer should be added") + + Expect(k8sClient.Delete(ctx, proxySecret)).To(Succeed(), "failed to delete the vault proxy credential secret") + Expect(k8sClient.Delete(ctx, autoscalingRunnerSet)).To(Succeed(), "failed to delete AutoScalingRunnerSet") + + Eventually( + func() error { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(autoscalingRunnerSet), new(v1alpha1.AutoscalingRunnerSet)) + if errors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + return fmt.Errorf("AutoScalingRunnerSet is not deleted") + }, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "a missing vault proxy credential secret should not keep the AutoScalingRunnerSet in Terminating") + }) + }) }) // testHoldFinalizer keeps an AutoscalingListener around after it has been diff --git a/controllers/actions.github.com/secretresolver/secret_resolver.go b/controllers/actions.github.com/secretresolver/secret_resolver.go index 5f3e9561..7cc85af3 100644 --- a/controllers/actions.github.com/secretresolver/secret_resolver.go +++ b/controllers/actions.github.com/secretresolver/secret_resolver.go @@ -4,12 +4,14 @@ import ( "context" "crypto/x509" "encoding/json" + "errors" "fmt" "log/slog" "net/http" "net/url" "strings" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig" "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient" "github.com/actions/actions-runner-controller/controllers/actions.github.com/object" @@ -17,10 +19,35 @@ import ( "github.com/actions/actions-runner-controller/vault/azurekeyvault" "golang.org/x/net/http/httpproxy" corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) +type secretResolverError string + +func (e secretResolverError) Error() string { return string(e) } + +// ErrNotFound marks a secret or config map that no longer exists, whichever driver resolved it. +const ErrNotFound = secretResolverError("not found") + +// wrapKubernetesError tags a Kubernetes NotFound so callers need no knowledge of the driver. +func wrapKubernetesError(err error) error { + if kerrors.IsNotFound(err) { + return fmt.Errorf("%w: %w", ErrNotFound, err) + } + return err +} + +// wrapAzureKeyVaultError tags only a missing Azure Key Vault secret, so a 403 or throttled read still requeues. +func wrapAzureKeyVaultError(err error) error { + var responseErr *azcore.ResponseError + if errors.As(err, &responseErr) && responseErr.StatusCode == http.StatusNotFound && responseErr.ErrorCode == "SecretNotFound" { + return fmt.Errorf("%w: %w", ErrNotFound, err) + } + return err +} + type SecretResolver struct { k8sClient client.Client multiClient multiclient.MultiClient @@ -56,12 +83,12 @@ func New(k8sClient client.Client, scalesetMultiClient multiclient.MultiClient, o func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj object.ActionsGitHubObject) (*appconfig.AppConfig, error) { resolver, err := sr.resolverForObject(ctx, obj) if err != nil { - return nil, fmt.Errorf("failed to get resolver for object: %v", err) + return nil, fmt.Errorf("failed to get resolver for object: %w", err) } appConfig, err := resolver.appConfig(ctx, obj.GitHubConfigSecret()) if err != nil { - return nil, fmt.Errorf("failed to resolve app config: %v", err) + return nil, fmt.Errorf("failed to resolve app config: %w", err) } return appConfig, nil @@ -70,12 +97,12 @@ func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj object.ActionsGi func (sr *SecretResolver) GetActionsService(ctx context.Context, obj object.ActionsGitHubObject) (multiclient.Client, error) { resolver, err := sr.resolverForObject(ctx, obj) if err != nil { - return nil, fmt.Errorf("failed to get resolver for object: %v", err) + return nil, fmt.Errorf("failed to get resolver for object: %w", err) } appConfig, err := resolver.appConfig(ctx, obj.GitHubConfigSecret()) if err != nil { - return nil, fmt.Errorf("failed to resolve app config: %v", err) + return nil, fmt.Errorf("failed to resolve app config: %w", err) } var proxyFunc func(req *http.Request) (*url.URL, error) @@ -93,7 +120,7 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj object.Acti if ref := proxy.HTTP.CredentialSecretRef; ref != "" { u.User, err = resolver.proxyCredentials(ctx, ref) if err != nil { - return nil, fmt.Errorf("failed to resolve proxy credentials: %v", err) + return nil, fmt.Errorf("failed to resolve proxy credentials: %w", err) } } @@ -109,7 +136,7 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj object.Acti if ref := proxy.HTTPS.CredentialSecretRef; ref != "" { u.User, err = resolver.proxyCredentials(ctx, ref) if err != nil { - return nil, fmt.Errorf("failed to resolve proxy credentials: %v", err) + return nil, fmt.Errorf("failed to resolve proxy credentials: %w", err) } } @@ -134,7 +161,7 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj object.Acti &configmap, ) if err != nil { - return nil, fmt.Errorf("failed to get configmap %s: %w", name, err) + return nil, fmt.Errorf("failed to get configmap %s: %w", name, wrapKubernetesError(err)) } return []byte(configmap.Data[key]), nil @@ -173,12 +200,12 @@ func (sr *SecretResolver) resolverForObject(ctx context.Context, obj object.Acti var secret corev1.Secret err := sr.k8sClient.Get(ctx, types.NamespacedName{Name: s, Namespace: obj.GetNamespace()}, &secret) if err != nil { - return nil, fmt.Errorf("failed to get secret %s: %w", s, err) + return nil, fmt.Errorf("failed to get secret %s: %w", s, wrapKubernetesError(err)) } return &secret, nil }) if err != nil { - return nil, fmt.Errorf("failed to create proxy config: %v", err) + return nil, fmt.Errorf("failed to create proxy config: %w", err) } proxy = p } @@ -225,7 +252,7 @@ func (r *k8sResolver) appConfig(ctx context.Context, key string) (*appconfig.App nsName, secret, ); err != nil { - return nil, fmt.Errorf("failed to get kubernetes secret: %q", nsName.String()) + return nil, fmt.Errorf("failed to get kubernetes secret %q: %w", nsName.String(), wrapKubernetesError(err)) } return appconfig.FromSecret(secret) @@ -239,7 +266,7 @@ func (r *k8sResolver) proxyCredentials(ctx context.Context, key string) (*url.Us nsName, secret, ); err != nil { - return nil, fmt.Errorf("failed to get kubernetes secret: %q", nsName.String()) + return nil, fmt.Errorf("failed to get kubernetes secret %q: %w", nsName.String(), wrapKubernetesError(err)) } return url.UserPassword( @@ -255,7 +282,7 @@ type vaultResolver struct { func (r *vaultResolver) appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error) { val, err := r.vault.GetSecret(ctx, key) if err != nil { - return nil, fmt.Errorf("failed to resolve secret: %v", err) + return nil, fmt.Errorf("failed to resolve secret: %w", wrapAzureKeyVaultError(err)) } return appconfig.FromJSONString(val) @@ -264,7 +291,7 @@ func (r *vaultResolver) appConfig(ctx context.Context, key string) (*appconfig.A func (r *vaultResolver) proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error) { val, err := r.vault.GetSecret(ctx, key) if err != nil { - return nil, fmt.Errorf("failed to resolve secret: %v", err) + return nil, fmt.Errorf("failed to resolve secret: %w", wrapAzureKeyVaultError(err)) } type info struct { diff --git a/controllers/actions.github.com/secretresolver/secret_resolver_test.go b/controllers/actions.github.com/secretresolver/secret_resolver_test.go new file mode 100644 index 00000000..c05b547d --- /dev/null +++ b/controllers/actions.github.com/secretresolver/secret_resolver_test.go @@ -0,0 +1,135 @@ +package secretresolver + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" + "github.com/actions/actions-runner-controller/vault" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" +) + +type fakeVault struct{ err error } + +func (v fakeVault) GetSecret(context.Context, string) (string, error) { return "", v.err } + +func TestWrapKubernetesError(t *testing.T) { + notFound := kerrors.NewNotFound(schema.GroupResource{Resource: "secrets"}, "github-config") + if err := wrapKubernetesError(notFound); !errors.Is(err, ErrNotFound) || !kerrors.IsNotFound(err) { + t.Fatalf("NotFound lost its identity: %v", err) + } + forbidden := kerrors.NewForbidden(schema.GroupResource{Resource: "secrets"}, "github-config", errors.New("denied")) + if err := wrapKubernetesError(forbidden); errors.Is(err, ErrNotFound) { + t.Fatalf("Forbidden reported as not found: %v", err) + } +} + +func TestVaultResolverTagsOnlySecretNotFound(t *testing.T) { + tests := map[string]struct { + err error + notFound bool + }{ + "404": {&azcore.ResponseError{StatusCode: http.StatusNotFound, ErrorCode: "SecretNotFound"}, true}, + "404 wrapped": {errors.Join(errors.New("failed to get secret"), &azcore.ResponseError{StatusCode: http.StatusNotFound, ErrorCode: "SecretNotFound"}), true}, + "404 other": {&azcore.ResponseError{StatusCode: http.StatusNotFound, ErrorCode: "VaultNotFound"}, false}, + "403": {&azcore.ResponseError{StatusCode: http.StatusForbidden, ErrorCode: "Forbidden"}, false}, + "network error": {errors.New("dial tcp: i/o timeout"), false}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + r := &vaultResolver{vault: fakeVault{err: tt.err}} + _, appErr := r.appConfig(context.Background(), "github-config") + _, proxyErr := r.proxyCredentials(context.Background(), "proxy") + for _, err := range []error{appErr, proxyErr} { + if err == nil { + t.Fatal("expected an error") + } + if got := errors.Is(err, ErrNotFound); got != tt.notFound { + t.Fatalf("errors.Is(err, ErrNotFound) = %v, want %v: %v", got, tt.notFound, err) + } + } + }) + } +} + +func TestGetActionsServiceTagsEveryMissingDependency(t *testing.T) { + const ns = "arc-runners" + configSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "github-config", Namespace: ns}, + Data: map[string][]byte{"github_token": []byte("token")}, + } + proxyWithCredentials := &v1alpha1.ProxyConfig{ + HTTP: &v1alpha1.ProxyServerConfig{Url: "http://proxy.example.com:3128", CredentialSecretRef: "proxy-credentials"}, + } + forbidden := interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + return kerrors.NewForbidden(schema.GroupResource{Resource: "secrets"}, key.Name, errors.New("denied")) + }, + } + + tests := map[string]struct { + spec v1alpha1.AutoscalingRunnerSetSpec + objects []client.Object + interceptor *interceptor.Funcs + notFound bool + }{ + "github config secret missing": { + spec: v1alpha1.AutoscalingRunnerSetSpec{GitHubConfigSecret: "github-config"}, + notFound: true, + }, + "proxy credential secret missing": { + spec: v1alpha1.AutoscalingRunnerSetSpec{GitHubConfigSecret: "github-config", Proxy: proxyWithCredentials}, + objects: []client.Object{configSecret}, + notFound: true, + }, + "tls config map missing": { + spec: v1alpha1.AutoscalingRunnerSetSpec{ + GitHubConfigSecret: "github-config", + GitHubServerTLS: &v1alpha1.TLSConfig{CertificateFrom: &v1alpha1.TLSCertificateSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "ca"}, Key: "ca.crt"}, + }}, + }, + objects: []client.Object{configSecret}, + notFound: true, + }, + "vault proxy credential secret missing": { + spec: v1alpha1.AutoscalingRunnerSetSpec{ + GitHubConfigSecret: "github-config", + VaultConfig: &v1alpha1.VaultConfig{Type: vault.VaultTypeAzureKeyVault, Proxy: proxyWithCredentials}, + }, + notFound: true, + }, + "forbidden is not missing": { + spec: v1alpha1.AutoscalingRunnerSetSpec{GitHubConfigSecret: "github-config"}, + interceptor: &forbidden, + notFound: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + builder := fake.NewClientBuilder().WithScheme(clientgoscheme.Scheme).WithObjects(tt.objects...) + if tt.interceptor != nil { + builder = builder.WithInterceptorFuncs(*tt.interceptor) + } + ars := &v1alpha1.AutoscalingRunnerSet{ObjectMeta: metav1.ObjectMeta{Name: "ars", Namespace: ns}, Spec: tt.spec} + + _, err := New(builder.Build(), nil).GetActionsService(context.Background(), ars) + if err == nil { + t.Fatal("expected an error") + } + if got := errors.Is(err, ErrNotFound); got != tt.notFound { + t.Fatalf("errors.Is(err, ErrNotFound) = %v, want %v: %v", got, tt.notFound, err) + } + }) + } +}