package bannerctl import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "regexp" "strings" "testing" "time" metricsscope "cloud.google.com/go/monitoring/metricsscope/apiv1" "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" registryAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/artifactregistry/v1beta1" computeAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/compute/v1beta1" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/iam/v1beta1" k8sAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1" loggingAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/logging/v1beta1" resourceAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/resourcemanager/v1beta1" serviceAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/serviceusage/v1beta1" storageAPI "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/storage/v1beta1" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "google.golang.org/api/option" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" kmeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/testutils/seededpostgres" bannerAPI "edge-infra.dev/pkg/edge/apis/banner/v1alpha1" syncedobjectApi "edge-infra.dev/pkg/edge/apis/syncedobject/apis/v1alpha1" capabilities "edge-infra.dev/pkg/edge/capabilities" edgeconstants "edge-infra.dev/pkg/edge/constants" bannerconstants "edge-infra.dev/pkg/edge/constants/api/banner" "edge-infra.dev/pkg/edge/controllers/dbmetrics" "edge-infra.dev/pkg/edge/controllers/util/edgedb" "edge-infra.dev/pkg/edge/gcpinfra" "edge-infra.dev/pkg/edge/registration" "edge-infra.dev/pkg/f8n/gcp/k8s/controllers/dennis" "edge-infra.dev/pkg/k8s/konfigkonnector/apis/meta" "edge-infra.dev/pkg/k8s/meta/status" "edge-infra.dev/pkg/k8s/runtime/controller" "edge-infra.dev/pkg/k8s/runtime/inventory" ff "edge-infra.dev/pkg/lib/featureflag" fftest "edge-infra.dev/pkg/lib/featureflag/testutil" "edge-infra.dev/pkg/lib/gcp/iam/roles" gcpProject "edge-infra.dev/pkg/lib/gcp/project" "edge-infra.dev/pkg/lib/logging" "edge-infra.dev/pkg/lib/uuid" "edge-infra.dev/test" "edge-infra.dev/test/framework" "edge-infra.dev/test/framework/gcp" "edge-infra.dev/test/framework/integration" "edge-infra.dev/test/framework/k8s" "edge-infra.dev/test/framework/k8s/envtest" ) const DefaultSecretValue = "mock secret value" var TestProjectNumber = "123456789000" func TestMain(m *testing.M) { framework.HandleFlags() // instantiate test logger ctrl.SetLogger(logging.NewLogger().Logger) os.Exit(m.Run()) } type Suite struct { *framework.Framework *k8s.K8s ctx context.Context timeout time.Duration tick time.Duration projectID string sMgr *mockSecretManager msc metricsScopesClient db *sql.DB } type mockSecretManager struct { clients map[string]*mockSecretManagerClient } func (sm *mockSecretManager) NewWithOptions(_ context.Context, projectID string, _ ...option.ClientOption) (secretManagerClient, error) { if sm.clients[projectID] == nil { sm.clients[projectID] = &mockSecretManagerClient{ secrets: make(map[string]*mockSecret), } } return sm.clients[projectID], nil } type mockSecretManagerClient struct { secrets map[string]*mockSecret } type mockSecret struct { value []byte } // GetLatestSecretValue returns a secret value from the mock storage, or JIT creates it if missing. The controller // under test reads from one Secret Manager and writes to another, but this mock only has one storage location. So // the first read is expected to miss func (smc *mockSecretManagerClient) GetLatestSecretValue(_ context.Context, secretID string) ([]byte, error) { s := smc.secrets[secretID] if s == nil { return nil, fmt.Errorf("secret %s not found", secretID) } return smc.secrets[secretID].value, nil } // GetLatestSecretValueInfo mocks base method. func (smc *mockSecretManagerClient) GetLatestSecretValueInfo(_ context.Context, secretID string) (*secretmanagerpb.SecretVersion, error) { s := smc.secrets[secretID] if s == nil { return nil, fmt.Errorf("secret %s not found", secretID) } return &secretmanagerpb.SecretVersion{ Name: secretID, }, nil } func (smc *mockSecretManagerClient) GetSecret(_ context.Context, secretID string) (*secretmanagerpb.Secret, error) { s := smc.secrets[secretID] if s == nil { return nil, fmt.Errorf("secret %s not found", secretID) } spb := &secretmanagerpb.Secret{ Name: secretID, } return spb, nil } func (smc *mockSecretManagerClient) AddSecret(_ context.Context, secretID string, _ []byte, _ map[string]string, _ bool, _ *time.Time, _ string) error { smc.secrets[secretID] = &mockSecret{value: []byte(DefaultSecretValue)} return nil } type mockMetricsScopeClient struct{} func (c *mockMetricsScopeClient) AddMonitoredProject(_ context.Context, _ string) (*metricsscope.CreateMonitoredProjectOperation, error) { return &metricsscope.CreateMonitoredProjectOperation{}, nil } func falsifyResourceReadiness(conds []k8sAPI.Condition) []k8sAPI.Condition { hasCond := false for _, c := range conds { if c.Type == "Ready" { hasCond = true } } if !hasCond { conds = append(conds, k8sAPI.Condition{Type: "Ready", Status: "True"}) } return conds } func isOwnedByBanner(obj client.Object, banner string) bool { for _, o := range obj.GetOwnerReferences() { if o.Name == banner { return true } } return false } func TestBannerController(t *testing.T) { testEnv := envtest.Setup() cfg, opts := controller.ProcessOptions(controller.WithCfg(testEnv.Config), controller.WithMetricsAddress("0")) opts.Scheme = createScheme() mgr, err := ctrl.NewManager(cfg, opts) test.NoError(err) // mock feature flags fftest.MustInitTestFeatureFlagsForContexts([]fftest.TestFlag{ { FlagContext: ff.NewEnvironmentContext(), Name: ff.UseBannerCTLPruning, Value: true, }, }) f := framework.New("bannerctl").Component("bannerctl") sp, err := seededpostgres.New() test.NoError(err) defer sp.Close() db, err := sp.DB() test.NoError(err) defer db.Close() // Create Suite with base values s := &Suite{ Framework: f, ctx: context.Background(), timeout: 10 * time.Second, tick: 50 * time.Millisecond, db: db, } // Update Suite based on integration test config // and initialize mock GCP services if necessary if integration.IsIntegrationTest() { s.projectID = gcp.GCloud.ProjectID } else { s.projectID = "mock gcp projectid" s.msc = &mockMetricsScopeClient{} } s.sMgr = &mockSecretManager{clients: make(map[string]*mockSecretManagerClient)} for _, secretID := range edgeconstants.PlatformSecretIDs { c, _ := s.sMgr.NewWithOptions(s.ctx, s.projectID) _ = c.AddSecret(s.ctx, secretID, []byte("mock secret value"), nil, false, nil, "") } totpSecret := "totp-secret" srv := httptest.NewServer(http.HandlerFunc( registration.GraphQLHandler(assert.New(t), registration.WithTotpSecret(totpSecret), registration.WithExistingCluster("couchdb-enabled"), ))) defer srv.Close() bslSrv, bslClient := FakeBslClient() defer bslSrv.Close() dbm := dbmetrics.New("bannerctl") reconciler := &BannerReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("bannerctl"), ForemanProjectID: s.projectID, PlatInfraProjectID: s.projectID, SecretManager: s.sMgr, MetricsScopesClient: s.msc, DefaultRequeue: 10 * time.Millisecond, Name: "bannerctl", EdgeAPI: srv.URL, TotpSecret: totpSecret, Domain: "dev0.edge-preprod.dev", DatasyncDNSName: datasyncDNSName, DatasyncDNSZone: datasyncDNSZone, DatabaseName: databaseName, Conditions: bannerConditions, EdgeDB: &edgedb.EdgeDB{DB: db}, Recorder: dbm, BSLClient: bslClient, GCPRegion: gcpRegion, GCPZone: gcpZone, GCPForemanProjectNumber: gcpForemanProjectNumber, BSLConfig: bslConfig, EdgeSecOptInCompliance: true, EdgeSecMaxLeasePeriod: "48h", EdgeSecMaxValidityPeriod: "60d", } err = reconciler.SetupWithManager(mgr) test.NoError(err) k := k8s.New(testEnv.Config, k8s.WithCtrlManager(mgr), k8s.WithKonfigKonnector()) s.K8s = k f.Register(k) suite.Run(t, s) t.Cleanup(func() { f.NoError(testEnv.Stop()) }) } func (s *Suite) TestBannerCreation_SecondaryResources() { bannerGUID := uuid.New().UUID generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix)))) testBSLID := uuid.New().UUID testOrg := uuid.New().UUID banner := &bannerAPI.Banner{ ObjectMeta: metav1.ObjectMeta{ Name: bannerGUID, }, Spec: bannerAPI.BannerSpec{ DisplayName: "testy-mcbanner", GCP: bannerAPI.GCPConfig{ ProjectID: generatedProjID, }, BSL: bannerAPI.BSLConfig{ EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{ ID: testBSLID, }, Organization: bannerAPI.BSLOrganization{ Name: testOrg, }, }, Enablements: []string{CouchDBEnablement}, }, } s.createBanner(banner) defer s.deleteBanner(banner) // Secondary resources that should be created for a Banner: // GCP Project KCC resource is created and added to Banner status project := &resourceAPI.Project{ ObjectMeta: metav1.ObjectMeta{ Name: projectName, Namespace: bannerGUID, }, } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: project.Name, Namespace: project.Namespace, }, project) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Require().True(isOwnedByBanner(project, bannerGUID), "expected (namespaced) Project to be owned by (cluster scoped) Banner") // there is no Project controller to do this for us. simulate a ready resource project.Status.Conditions = falsifyResourceReadiness(project.Status.Conditions) project.Status.Number = &TestProjectNumber s.Require().NoError(s.Client.Update(s.ctx, project)) s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && banner.Status.ProjectRef != "" }, s.timeout, s.tick, "expected Banner Status was never set") // Verify GCP Project was created and matches Banner status projectViaStatusRef := &resourceAPI.Project{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: strings.Split(banner.Status.ProjectRef, "/")[1], Namespace: strings.Split(banner.Status.ProjectRef, "/")[0], }, projectViaStatusRef) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Equal(projectViaStatusRef, project, "could not find Project obj via Banner.Status.ProjectRef") // K8s Namespace with correct project annotation namespace := &corev1.Namespace{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: bannerGUID, }, namespace) if err != nil { return false } nsAnnos := namespace.Annotations if nsAnnos != nil { return s.Equal(generatedProjID, namespace.Annotations[meta.ProjectAnnotation]) && s.Equal(meta.DeletionPolicyAbandon, namespace.Annotations[meta.DeletionPolicyAnnotation]) } return false }, s.timeout, s.tick, "expected Namespace was never found or didnt have correct annotation") //cluster := &edgeCluster.Cluster{} //s.Require().Eventually(func() bool { // err := s.Client.Get(s.ctx, types.NamespacedName{ // Name: bannerconstants.CreateBannerClusterInfraName(banner.Spec.DisplayName), // }, cluster) // return err == nil //}, s.timeout, s.tick, "expected edge cluster was never found") //s.Require().True(isOwnedByBanner(cluster, bannerGUID), "expected edge cluster to be owned by Banner") // APIs (Service object) s.Require().Eventually(func() bool { for _, api := range gcpinfra.TenantAPIs { service := &serviceAPI.Service{} err := s.Client.Get(s.ctx, types.NamespacedName{ Name: api, Namespace: bannerGUID, }, service) if errors.IsNotFound(err) { return false } s.Require().True(isOwnedByBanner(service, bannerGUID), "expected Service to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, service.Annotations[meta.DeletionPolicyAnnotation]) // if the service was created, force it to be ready, it wont ever be ready in the test env // TODO: integration service.Status.Conditions = falsifyResourceReadiness(service.Status.Conditions) s.Require().NoError(s.Client.Update(s.ctx, service)) } return true }, s.timeout, s.tick, "not all Services were created (enabled)") // StorageBucket and roles sb := &storageAPI.StorageBucket{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: generatedProjID, Namespace: bannerGUID, }, sb) return err == nil }, s.timeout, s.tick, "expected StorageBucket was never found") s.Require().True(isOwnedByBanner(sb, bannerGUID), "expected StorageBucket to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, sb.Annotations[meta.DeletionPolicyAnnotation]) // Integration? platform secrets and metrics scope from real gcp if integration.IsIntegrationTest() { // TODO: Assert real values from integration test secretmanager. The expected value // should come from test project values instead of hardcoded mocks for _, secretID := range edgeconstants.PlatformSecretIDs { v, err := s.sMgr.clients[bannerGUID].GetLatestSecretValue(s.ctx, secretID) s.NoError(err) s.Equal(DefaultSecretValue, string(v)) } } else { // Platform secrets s.Require().Eventually(func() bool { c := s.sMgr.clients[generatedProjID] if c == nil { return false } for _, secretID := range edgeconstants.PlatformSecretIDs { v, err := c.GetLatestSecretValue(s.ctx, secretID) if err != nil { return false } if !s.Equal(DefaultSecretValue, string(v)) { return false } } return true }, s.timeout, s.tick, "never got a generated GCP Project ID") } // Remote access ComputeAddress cAddr := &computeAPI.ComputeAddress{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-%s", bannerconstants.RemoteAccessIPName, banner.Name), Namespace: bannerGUID, }, } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: cAddr.Name, Namespace: cAddr.Namespace, }, cAddr) return err == nil }, s.timeout, s.tick, fmt.Sprintf("expected ComputeAddress %s/%s was never found", cAddr.Namespace, cAddr.Name)) dnsRecordCfgs := cAddr.Annotations[dennis.RecordConfigsAnnotation] s.Require().NotEmpty(dnsRecordCfgs, fmt.Sprintf("ComputeAddress was missing required annotation %s", dennis.RecordConfigsAnnotation)) cfgs := &[]dennis.RecordConfig{} err := json.Unmarshal([]byte(dnsRecordCfgs), cfgs) s.Require().NoError(err, fmt.Sprintf("ComputeAddress had invalid %s annotation", dennis.RecordConfigsAnnotation)) s.Require().Equal(meta.DeletionPolicyAbandon, cAddr.Annotations[meta.DeletionPolicyAnnotation]) // source: https://www.socketloop.com/tutorials/golang-use-regular-expression-to-validate-domain-name with modifications // for trailing '.' per RFC 1034 https://datatracker.ietf.org/doc/html/rfc1034 re := regexp.MustCompile(`((([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.){2}([a-zA-Z]{2,6}|([a-zA-Z0-9-]){2,30}\.[a-zA-Z]{2,3})\.$`) for _, cfg := range *cfgs { s.Require().True(re.MatchString(cfg.Name), fmt.Sprintf("remote access dns name '%s' was not valid", cfg.Name)) } // LoggingLogSink lls := &loggingAPI.LoggingLogSink{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: logSinkName, Namespace: bannerGUID, }, lls) return err == nil }, s.timeout, s.tick, "expected LoggingLogSink was never found") s.Require().True(isOwnedByBanner(lls, bannerGUID), "expected LoggingLogSink to be owned by Banner") s.Require().Equal(fmt.Sprintf("storage.googleapis.com/%s-siem", s.projectID), lls.Spec.Destination.StorageBucketRef.External) s.Require().Equal(banner.Spec.GCP.ProjectID, lls.Spec.ProjectRef.External) s.Require().True(*lls.Spec.UniqueWriterIdentity, "expected LoggingLogSink UniqueWriterIdentity to be true") // Metrics scope is a NOP. Just dont error here // Banner eventually becomes ready s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && kmeta.IsStatusConditionTrue(banner.Status.Conditions, status.ReadyCondition) }, s.timeout, s.tick, "Banner never became Ready") namespaceSO := &syncedobjectApi.SyncedObject{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: edgeconstants.BannerWideNamespace, Namespace: banner.Name, }, namespaceSO) return err == nil }, s.timeout, s.tick, "expected namespace synced obj was never found") s.Equal(namespaceSO.Spec.Cluster, "", "namespace synced obj does not have expected empty cluster") s.Equal(namespaceSO.Spec.Banner, banner.Spec.GCP.ProjectID, "namespace synced obj does not have expected project") // Foreman0 synced objects expectedForemanSO := []*syncedobjectApi.SyncedObject{ // 1. Emissary Mapping (banner ingress proxy) { ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("banner-proxy-%s", banner.Name), Namespace: banner.Name, }, }, } // Validate all foreman0 SO's get created with correct foreman0 name and project id for _, so := range expectedForemanSO { s.pollForSyncedObject(so) s.Require().Equal(s.projectID, so.Spec.Banner, fmt.Sprintf("SyncedObject %s/%s does not have banner set", so.Name, so.Namespace)) s.Require().Equal("foreman0", so.Spec.Cluster, fmt.Sprintf("synced object %s/%s does not have cluster set", so.Name, so.Namespace)) } // Checking GCP infra for KCC last since it depends on the cluster-infra cluster's EdgeID var hash string var kccResourceName string infraIAMSA := &v1beta1.IAMServiceAccount{} s.Require().Eventually(func() bool { if banner.Status.ClusterInfraClusterEdgeID != "" { hash = uuid.FromUUID(banner.Status.ClusterInfraClusterEdgeID).Hash() kccResourceName = fmt.Sprintf("kcc-%s", hash) } err := s.Client.Get(s.ctx, types.NamespacedName{ Name: kccResourceName, Namespace: bannerGUID, }, infraIAMSA) return err == nil }, s.timeout, s.tick, "expected kcc IAMServiceAccount was never found") s.Require().True(isOwnedByBanner(infraIAMSA, bannerGUID), "expected IAMServiceAccount to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, infraIAMSA.Annotations[meta.DeletionPolicyAnnotation]) s.Require().Equal(fmt.Sprintf("k8s cfg connector for project %s", banner.Spec.DisplayName), *infraIAMSA.Spec.DisplayName) projectOwner := &v1beta1.IAMPolicyMember{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: fmt.Sprintf("%s-owner", kccResourceName), Namespace: bannerGUID, }, projectOwner) return err == nil }, s.timeout, s.tick, "expected kcc project owner IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(projectOwner, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, projectOwner.Annotations[meta.DeletionPolicyAnnotation]) s.Require().Equal(fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", kccResourceName, generatedProjID), *projectOwner.Spec.Member) s.Require().Equal(resourceAPI.SchemeGroupVersion.String(), projectOwner.Spec.ResourceRef.APIVersion) s.Require().Equal(resourceAPI.ProjectGVK.Kind, projectOwner.Spec.ResourceRef.Kind) s.Require().Equal(generatedProjID, projectOwner.Spec.ResourceRef.External) s.Require().Equal(roles.Owner, projectOwner.Spec.Role) logWriter := &v1beta1.IAMPolicyMember{} policyName := fmt.Sprintf("%s-logging-logwriter", kccResourceName) s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: policyName, Namespace: bannerGUID, }, logWriter) return err == nil }, s.timeout, s.tick, "expected kcc project owner IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(logWriter, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, logWriter.Annotations[meta.DeletionPolicyAnnotation]) s.Require().Equal(fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", kccResourceName, generatedProjID), *logWriter.Spec.Member) s.Require().Equal(resourceAPI.SchemeGroupVersion.String(), logWriter.Spec.ResourceRef.APIVersion) s.Require().Equal(resourceAPI.ProjectGVK.Kind, logWriter.Spec.ResourceRef.Kind) s.Require().Equal(s.projectID, logWriter.Spec.ResourceRef.External) s.Require().Equal(roles.ProjectAdmin, logWriter.Spec.Role) s.Require().Equal("kcc_delegated_foreman_roles", logWriter.Spec.Condition.Title) s.Require().Equal("KCC delegated roles on foreman", *logWriter.Spec.Condition.Description) s.Require().Equal(KccDelegatedRolesExpression, logWriter.Spec.Condition.Expression) pubSubAdmin := &v1beta1.IAMPolicyMember{} policyName = fmt.Sprintf("%s-pubsub-admin", kccResourceName) s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: policyName, Namespace: bannerGUID, }, pubSubAdmin) return err == nil }, s.timeout, s.tick, "expected kcc project owner IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(pubSubAdmin, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, pubSubAdmin.Annotations[meta.DeletionPolicyAnnotation]) s.Require().Equal(fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", kccResourceName, generatedProjID), *pubSubAdmin.Spec.Member) s.Require().Equal(resourceAPI.SchemeGroupVersion.String(), pubSubAdmin.Spec.ResourceRef.APIVersion) s.Require().Equal(resourceAPI.ProjectGVK.Kind, pubSubAdmin.Spec.ResourceRef.Kind) s.Require().Equal(s.projectID, pubSubAdmin.Spec.ResourceRef.External) s.Require().Equal(roles.PubsubAdmin, pubSubAdmin.Spec.Role) s.Require().Nil(pubSubAdmin.Spec.Condition) wiMember := &v1beta1.IAMPolicyMember{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: fmt.Sprintf("%s-workload-id", kccResourceName), Namespace: bannerGUID, }, wiMember) return err == nil }, s.timeout, s.tick, "expected kcc workload identity IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(wiMember, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, wiMember.Annotations[meta.DeletionPolicyAnnotation]) s.Require().Equal(fmt.Sprintf("serviceAccount:%s.svc.id.goog[cnrm-system/cnrm-controller-manager]", generatedProjID), *wiMember.Spec.Member) s.Require().Equal(v1beta1.SchemeGroupVersion.String(), wiMember.Spec.ResourceRef.APIVersion) s.Require().Equal(v1beta1.IAMServiceAccountGVK.Kind, wiMember.Spec.ResourceRef.Kind) s.Require().Equal(kccResourceName, wiMember.Spec.ResourceRef.Name) s.Require().Equal(roles.WorkloadIdentityUser, wiMember.Spec.Role) s.validateEdgeLabels(banner) s.checkInfraStatusDatabaseValue(banner, edgedb.InfraStatusReady) } func (s *Suite) TestBannerCreation_UsingWarehouse() { bannerGUID := uuid.New().UUID generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix)))) banner := &bannerAPI.Banner{ ObjectMeta: metav1.ObjectMeta{ Name: bannerGUID, }, Spec: bannerAPI.BannerSpec{ DisplayName: "dev1-banner-use-warehouse", GCP: bannerAPI.GCPConfig{ ProjectID: generatedProjID, }, BSL: bannerAPI.BSLConfig{ EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{ ID: uuid.New().UUID, }, Organization: bannerAPI.BSLOrganization{ Name: "test-org-dev1", }, }, }, } s.createBanner(banner) defer s.deleteBanner(banner) // 0.1 GCP Project KCC resource is created and added to Banner status, becomes ready project := &resourceAPI.Project{ ObjectMeta: metav1.ObjectMeta{ Name: projectName, Namespace: bannerGUID, }, } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: project.Name, Namespace: project.Namespace, }, project) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Require().True(isOwnedByBanner(project, bannerGUID), "expected (namespaced) Project to be owned by (cluster scoped) Banner") // there is no Project controller to do this for us. simulate a ready resource project.Status.Conditions = falsifyResourceReadiness(project.Status.Conditions) project.Status.Number = &TestProjectNumber s.Require().NoError(s.Client.Update(s.ctx, project)) // 1. Warehouse Service dependency s.Require().Eventually(func() bool { service := &serviceAPI.Service{} err := s.Client.Get(s.ctx, types.NamespacedName{ Name: "artifactregistry.googleapis.com", Namespace: bannerGUID, }, service) if errors.IsNotFound(err) { return false } s.Require().True(isOwnedByBanner(service, bannerGUID), "expected Service to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, service.Annotations[meta.DeletionPolicyAnnotation]) // if the service was created, force it to be ready, it wont ever be ready in the test env service.Status.Conditions = falsifyResourceReadiness(service.Status.Conditions) s.Require().NoError(s.Client.Update(s.ctx, service)) return true }, s.timeout, s.tick, "not all Services were created (enabled)") // 2. Warehouse Registry Repository garRepo := ®istryAPI.ArtifactRegistryRepository{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: "warehouse", Namespace: bannerGUID, }, garRepo) return err == nil }, s.timeout, s.tick, "expected ArtifactRegistryRepository was never found") s.Require().Equal(meta.DeletionPolicyAbandon, garRepo.Annotations[meta.DeletionPolicyAnnotation]) } func (s *Suite) pollForSyncedObject(so *syncedobjectApi.SyncedObject) { s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: so.Name, Namespace: so.Namespace, }, so) return err == nil }, s.timeout, s.tick, fmt.Sprintf("expected SyncedObject %s/%s was never found", so.Namespace, so.Name)) } func (s *Suite) TestBannerCreation_WithInventoryPruning() { bannerGUID := uuid.New().UUID generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix)))) banner := &bannerAPI.Banner{ ObjectMeta: metav1.ObjectMeta{ Name: bannerGUID, }, Spec: bannerAPI.BannerSpec{ DisplayName: "test-banner-prune", GCP: bannerAPI.GCPConfig{ ProjectID: generatedProjID, }, BSL: bannerAPI.BSLConfig{ EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{ ID: uuid.New().UUID, }, Organization: bannerAPI.BSLOrganization{ Name: "test-org-dev-2", }, }, }, } s.createBanner(banner) defer s.deleteBanner(banner) // Secondary resources that should be created for a Banner: // GCP Project KCC resource is created and added to Banner status project := &resourceAPI.Project{ ObjectMeta: metav1.ObjectMeta{ Name: projectName, Namespace: bannerGUID, }, } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: project.Name, Namespace: project.Namespace, }, project) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Require().True(isOwnedByBanner(project, bannerGUID), "expected (namespaced) Project to be owned by (cluster scoped) Banner") // there is no Project controller to do this for us. simulate a ready resource project.Status.Conditions = falsifyResourceReadiness(project.Status.Conditions) project.Status.Number = &TestProjectNumber s.Require().NoError(s.Client.Update(s.ctx, project)) s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && banner.Status.ProjectRef != "" }, s.timeout, s.tick, "expected Banner Status was never set") // Add 3 mock objects to Banner Inventory banner.Status.Inventory = &inventory.ResourceInventory{} banner.Status.Inventory.Entries = append(banner.Status.Inventory.Entries, generateMockObjects()...) s.Require().NoError(s.Client.Update(s.ctx, banner)) // Verify GCP Project was created and matches Banner status projectViaStatusRef := &resourceAPI.Project{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: strings.Split(banner.Status.ProjectRef, "/")[1], Namespace: strings.Split(banner.Status.ProjectRef, "/")[0], }, projectViaStatusRef) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Equal(projectViaStatusRef, project, "could not find Project obj via Banner.Status.ProjectRef") // K8s Namespace with correct project annotation namespace := &corev1.Namespace{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: bannerGUID, }, namespace) if err != nil { return false } nsAnnos := namespace.Annotations if nsAnnos != nil { return s.Equal(generatedProjID, namespace.Annotations[meta.ProjectAnnotation]) } return false }, s.timeout, s.tick, "expected Namespace was never found or didnt have correct annotation") //cluster := &edgeCluster.Cluster{} //s.Require().Eventually(func() bool { // err := s.Client.Get(s.ctx, types.NamespacedName{ // Name: bannerconstants.CreateBannerClusterInfraName(banner.Spec.DisplayName), // }, cluster) // return err == nil //}, s.timeout, s.tick, "expected edge cluster was never found") //s.Require().True(isOwnedByBanner(cluster, bannerGUID), "expected edge cluster to be owned by Banner") // APIs (Service object) s.Require().Eventually(func() bool { for _, api := range gcpinfra.TenantAPIs { service := &serviceAPI.Service{} err := s.Client.Get(s.ctx, types.NamespacedName{ Name: api, Namespace: bannerGUID, }, service) if errors.IsNotFound(err) { return false } s.Require().True(isOwnedByBanner(service, bannerGUID), "expected Service to be owned by Banner") // if the service was created, force it to be ready, it wont ever be ready in the test env // TODO: integration service.Status.Conditions = falsifyResourceReadiness(service.Status.Conditions) s.Require().NoError(s.Client.Update(s.ctx, service)) } return true }, s.timeout, s.tick, "not all Services were created (enabled)") // StorageBucket and roles sb := &storageAPI.StorageBucket{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: generatedProjID, Namespace: bannerGUID, }, sb) return err == nil }, s.timeout, s.tick, "expected StorageBucket was never found") s.Require().True(isOwnedByBanner(sb, bannerGUID), "expected StorageBucket to be owned by Banner") // Integration? platform secrets and metrics scope from real gcp if integration.IsIntegrationTest() { // TODO: Assert real values from integration test secretmanager. The expected value // should come from test project values instead of hardcoded mocks for _, secretID := range edgeconstants.PlatformSecretIDs { v, err := s.sMgr.clients[bannerGUID].GetLatestSecretValue(s.ctx, secretID) s.NoError(err) s.Equal(("mocked secret value"), string(v)) } } else { // Platform secrets s.Require().Eventually(func() bool { c := s.sMgr.clients[generatedProjID] if c == nil { return false } for _, secretID := range edgeconstants.PlatformSecretIDs { v, err := c.GetLatestSecretValue(s.ctx, secretID) if err != nil { return false } if !s.Equal(DefaultSecretValue, string(v)) { return false } } return true }, s.timeout, s.tick, "never got a generated GCP Project ID") } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && kmeta.IsStatusConditionTrue(banner.Status.Conditions, status.ReadyCondition) }, s.timeout, s.tick, "Banner never became Ready") // Checking GCP infra for KCC last since it depends on the cluster-infra cluster's EdgeID // var hash string // var kccResourceName string hash := uuid.FromUUID(banner.Status.ClusterInfraClusterEdgeID).Hash() kccResourceName := fmt.Sprintf("kcc-%s", hash) infraIAMSA := &v1beta1.IAMServiceAccount{} s.Require().Eventually(func() bool { // if banner.Status.ClusterInfraClusterEdgeID != "" { // hash = uuid.FromUUID(banner.Status.ClusterInfraClusterEdgeID).Hash() // kccResourceName = fmt.Sprintf("kcc-%s", hash) // } err := s.Client.Get(s.ctx, types.NamespacedName{ Name: kccResourceName, Namespace: bannerGUID, }, infraIAMSA) return err == nil }, s.timeout, s.tick, "expected kcc IAMServiceAccount was never found") s.Require().True(isOwnedByBanner(infraIAMSA, bannerGUID), "expected IAMServiceAccount to be owned by Banner") s.Require().Equal(fmt.Sprintf("k8s cfg connector for project %s", banner.Spec.DisplayName), *infraIAMSA.Spec.DisplayName) projectOwner := &v1beta1.IAMPolicyMember{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: fmt.Sprintf("%s-owner", kccResourceName), Namespace: bannerGUID, }, projectOwner) return err == nil }, s.timeout, s.tick, "expected kcc project owner IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(projectOwner, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", kccResourceName, generatedProjID), *projectOwner.Spec.Member) s.Require().Equal(resourceAPI.SchemeGroupVersion.String(), projectOwner.Spec.ResourceRef.APIVersion) s.Require().Equal(resourceAPI.ProjectGVK.Kind, projectOwner.Spec.ResourceRef.Kind) s.Require().Equal(generatedProjID, projectOwner.Spec.ResourceRef.External) s.Require().Equal(roles.Owner, projectOwner.Spec.Role) wiMember := &v1beta1.IAMPolicyMember{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: fmt.Sprintf("%s-workload-id", kccResourceName), Namespace: bannerGUID, }, wiMember) return err == nil }, s.timeout, s.tick, "expected kcc workload identity IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(wiMember, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(fmt.Sprintf("serviceAccount:%s.svc.id.goog[cnrm-system/cnrm-controller-manager]", generatedProjID), *wiMember.Spec.Member) s.Require().Equal(v1beta1.SchemeGroupVersion.String(), wiMember.Spec.ResourceRef.APIVersion) s.Require().Equal(v1beta1.IAMServiceAccountGVK.Kind, wiMember.Spec.ResourceRef.Kind) s.Require().Equal(kccResourceName, wiMember.Spec.ResourceRef.Name) s.Require().Equal(roles.WorkloadIdentityUser, wiMember.Spec.Role) storageMember := &v1beta1.IAMPolicyMember{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: fmt.Sprintf("%s-storage-siemwriter", kccResourceName), Namespace: bannerGUID, }, storageMember) return err == nil }, s.timeout, s.tick, "expected kcc StorageBucket IAMPolicyMember was never found") s.Require().True(isOwnedByBanner(storageMember, bannerGUID), "expected IAMPolicyMember to be owned by Banner") s.Require().Equal(fmt.Sprintf("serviceAccount:service-%s@gcp-sa-logging.iam.gserviceaccount.com", TestProjectNumber), *storageMember.Spec.Member) s.Require().Equal(storageAPI.SchemeGroupVersion.String(), storageMember.Spec.ResourceRef.APIVersion) s.Require().Equal(storageAPI.StorageBucketGVK.Kind, storageMember.Spec.ResourceRef.Kind) s.Require().Equal(fmt.Sprintf("%s-siem", s.projectID), storageMember.Spec.ResourceRef.External) s.Require().Equal(roles.StorageObjectCreator, storageMember.Spec.Role) // check inventory pruning s.Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && len(banner.Status.Inventory.Entries) == 36 }, s.timeout, s.tick, fmt.Sprintf("expected inventory not created %d", len(banner.Status.Inventory.Entries))) s.checkInfraStatusDatabaseValue(banner, edgedb.InfraStatusReady) } // generateMockObjects generate 3 mock inventory objects // used to test that pruning works correctly. func generateMockObjects() []inventory.ResourceRef { return []inventory.ResourceRef{ { ID: "abc-123-456", Version: "v1", }, { ID: "abc-456-789", Version: "v1", }, { ID: "abc-789-123", Version: "v1", }, } } func (s *Suite) TestBannerDeletion_ResourceLifecycle() { integration.SkipIfNot(s.Framework) // TODO. Deletion lifecycle (doesnt work in limited envtest setup, integration only) bannerGUID := "guid-abc-123" banner := &bannerAPI.Banner{ ObjectMeta: metav1.ObjectMeta{ Name: bannerGUID, }, Spec: bannerAPI.BannerSpec{ DisplayName: "Testy McBanner", BSL: bannerAPI.BSLConfig{ Organization: bannerAPI.BSLOrganization{ Name: "test-org", }, }, }, } project := &resourceAPI.Project{ ObjectMeta: metav1.ObjectMeta{ Name: projectName, Namespace: bannerGUID, }, } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: project.Name, Namespace: project.Namespace, }, project) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Require().NoError(s.Client.Create(s.ctx, banner)) s.Require().NoError(s.Client.Delete(s.ctx, banner)) s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return errors.IsNotFound(err) }, s.timeout, s.tick, "test Banner was never deleted") // Project owned by Banner should be garbage collected s.Require().Eventually(func() bool { p := &resourceAPI.Project{} err := s.Client.Get(s.ctx, types.NamespacedName{ Name: project.Name, Namespace: project.Namespace, }, p) return errors.IsNotFound(err) }, s.timeout, s.tick, "banner Project was never deleted") // TODO: All children of project (gcp resources) should be cleaned up } func (s *Suite) TestBannerCreation_CouchdbConfig() { bannerGUID := uuid.New().UUID generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix)))) banner := &bannerAPI.Banner{ ObjectMeta: metav1.ObjectMeta{ Name: bannerGUID, }, Spec: bannerAPI.BannerSpec{ DisplayName: "couchdb-enabled", GCP: bannerAPI.GCPConfig{ ProjectID: generatedProjID, }, Enablements: []string{ "couchdb", }, BSL: bannerAPI.BSLConfig{ EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{ ID: uuid.New().UUID, }, Organization: bannerAPI.BSLOrganization{ Name: "test-org-dev-4", }, }, }, } s.createBanner(banner) defer s.deleteBanner(banner) // Secondary resources that should be created for a Couchdb Enabled Banner: // 1. GCP Project KCC resource is created and added to Banner status project := &resourceAPI.Project{ ObjectMeta: metav1.ObjectMeta{ Name: projectName, Namespace: bannerGUID, }, } s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: project.Name, Namespace: project.Namespace, }, project) return err == nil }, s.timeout, s.tick, "expected Project was never found") s.Require().True(isOwnedByBanner(project, bannerGUID), "expected (namespaced) Project to be owned by (cluster scoped) Banner") project.Status.Conditions = falsifyResourceReadiness(project.Status.Conditions) project.Status.Number = &TestProjectNumber s.Require().NoError(s.Client.Update(s.ctx, project)) s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && banner.Status.ProjectRef != "" }, s.timeout, s.tick, "expected Banner Status was never set") // 2. APIs (Service object) s.Require().Eventually(func() bool { for _, api := range gcpinfra.TenantAPIs { service := &serviceAPI.Service{} err := s.Client.Get(s.ctx, types.NamespacedName{ Name: api, Namespace: bannerGUID, }, service) if errors.IsNotFound(err) { return false } s.Require().True(isOwnedByBanner(service, bannerGUID), "expected Service to be owned by Banner") s.Require().Equal(meta.DeletionPolicyAbandon, service.Annotations[meta.DeletionPolicyAnnotation]) // if the service was created, force it to be ready, it wont ever be ready in the test env // TODO: integration service.Status.Conditions = falsifyResourceReadiness(service.Status.Conditions) s.Require().NoError(s.Client.Update(s.ctx, service)) } return true }, s.timeout, s.tick, "not all Services were created (enabled)") // Banner eventually becomes ready s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: banner.Name, Namespace: banner.Namespace, }, banner) return err == nil && kmeta.IsStatusConditionTrue(banner.Status.Conditions, status.ReadyCondition) }, s.timeout, s.tick, "Banner never became Ready") s.checkInfraStatusDatabaseValue(banner, edgedb.InfraStatusReady) } func (s *Suite) TestBannerControllerSetsInfraStatusError() { bannerGUID := uuid.New().UUID testBSLID := uuid.New().UUID testOrg := uuid.New().UUID // Set an invalid project id for the banner to get an error status banner := &bannerAPI.Banner{ ObjectMeta: metav1.ObjectMeta{ Name: bannerGUID, }, Spec: bannerAPI.BannerSpec{ DisplayName: "test-infra-status-error", GCP: bannerAPI.GCPConfig{ ProjectID: "invalid projectID", }, BSL: bannerAPI.BSLConfig{ EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{ ID: testBSLID, }, Organization: bannerAPI.BSLOrganization{ Name: testOrg, }, }, }, } s.createBanner(banner) defer s.deleteBanner(banner) // Ensure the banner reconciler properly set the infra_status to ERROR s.checkInfraStatusDatabaseValue(banner, edgedb.InfraStatusError) } func (s *Suite) createBanner(banner *bannerAPI.Banner) { bannerType := string(bannerconstants.EU) tenantID := uuid.New().UUID s.Require().NoError(banner.IsValid()) _, err := s.db.Exec("INSERT INTO tenants (tenant_edge_id, org_id, org_name) VALUES ($1, $2, $3);", tenantID, banner.Spec.BSL.EnterpriseUnit.ID, banner.Spec.BSL.Organization.Name) s.NoError(err) const stmt = "INSERT INTO banners (banner_bsl_id, banner_name, banner_type, project_id, tenant_edge_id, banner_edge_id, description) VALUES ($1, $2, $3, $4, $5, $6, $7)" _, err = s.db.Exec(stmt, banner.Spec.BSL.EnterpriseUnit.ID, banner.Spec.DisplayName, bannerType, banner.Spec.GCP.ProjectID, tenantID, banner.Name, nil) s.Require().NoError(err) s.Require().NoError(s.Client.Create(s.ctx, banner)) } func (s *Suite) checkInfraStatusDatabaseValue(banner *bannerAPI.Banner, infraStatus edgedb.InfraStatus) { const stmt = "SELECT infra_status FROM banners WHERE banner_edge_id=$1" s.Require().Eventually(func() bool { var v string err := s.db.QueryRow(stmt, banner.Name).Scan(&v) if err != nil { return false } return v == string(infraStatus) }, s.timeout, s.tick, "expected infra_status value %q not set in database", infraStatus) } func (s *Suite) validateEdgeLabels(banner *bannerAPI.Banner) { const stmt = "SELECT label_key, visible, editable, color, description, label_type FROM labels WHERE banner_edge_id=$1 AND label_type=$2 AND label_key=$3" expectedEdgeLabels := capabilities.EdgeAutomatedCapabilityLabels expectedEdgeLabels = append(expectedEdgeLabels, fetchEdgeCapabilityLabels()...) for _, label := range expectedEdgeLabels { var actualLabel = &model.LabelInput{} s.Require().Eventually(func() bool { row := s.db.QueryRowContext(s.ctx, stmt, banner.Name, label.Type, label.Key) if err := row.Scan(&actualLabel.Key, &actualLabel.Visible, &actualLabel.Editable, &actualLabel.Color, &actualLabel.Description, &actualLabel.Type); err != nil { return false } actualLabel.BannerEdgeID = banner.GetName() label.BannerEdgeID = banner.GetName() return cmp.Equal(label, actualLabel) }, s.timeout, s.tick, "expected label", label, "actual label", actualLabel) } } func (s *Suite) deleteBanner(banner *bannerAPI.Banner) { s.NoError(s.Client.Delete(s.ctx, banner)) s.Eventually(func() bool { return errors.IsNotFound(s.Client.Get(s.ctx, client.ObjectKeyFromObject(banner), banner)) }, s.timeout, s.tick, "banner resource was never deleted %v", banner) }