From 9b671c6e5fde1db597ac219d1f920c69b6870ea8 Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Mon, 13 Jul 2026 13:31:30 +0300 Subject: [PATCH 1/7] added ca8 with utils helper function, its unit testing and adjustments on ressources.go Signed-off-by: Ran Wurmbrand --- e2e-tests/framework/resources.go | 7 + .../tier0/ca8_label_scoped_export_test.go | 110 +++++++++++++++ e2e-tests/utils/utils.go | 41 ++++++ e2e-tests/utils/utils_test.go | 128 ++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 e2e-tests/tests/tier0/ca8_label_scoped_export_test.go diff --git a/e2e-tests/framework/resources.go b/e2e-tests/framework/resources.go index fb8d3e37..669fe1f7 100644 --- a/e2e-tests/framework/resources.go +++ b/e2e-tests/framework/resources.go @@ -124,6 +124,7 @@ func (crb ClusterRoleBinding) AddSubject(k KubectlRunner, sa ServiceAccount) err type ServiceAccount struct { Name string Namespace string + Label string } func (sa ServiceAccount) Create(k KubectlRunner) error { @@ -132,6 +133,12 @@ func (sa ServiceAccount) Create(k KubectlRunner) error { return fmt.Errorf("failed to create ServiceAccount %s in %s: %w", sa.Name, sa.Namespace, err) } log.Printf("created ServiceAccount %s in %s", sa.Name, sa.Namespace) + if sa.Label != "" { + _, err = k.Run("label", "serviceaccount", sa.Name, "-n", sa.Namespace, sa.Label) + if err != nil { + return fmt.Errorf("failed to label ServiceAccount %s: %w", sa.Name, err) + } + } return nil } diff --git a/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go b/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go new file mode 100644 index 00000000..76de2c62 --- /dev/null +++ b/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go @@ -0,0 +1,110 @@ +package e2e + +import ( + "log" + "path/filepath" + + "github.com/konveyor/crane/e2e-tests/config" + . "github.com/konveyor/crane/e2e-tests/framework" + "github.com/konveyor/crane/e2e-tests/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Cluster-level export filtering", func() { + It("[CA-8] Should export only labeled workload and its RBAC with --label-selector", Label("tier0"), func() { + appName := "simple-nginx-nopv" + namespace := "simple-nginx-nopv" + serviceName := "my-" + appName + + scenario := NewMigrationScenario( + appName, + namespace, + config.K8sDeployBin, + config.CraneBin, + config.SourceContext, + config.TargetContext, + ) + srcApp := scenario.SrcApp + tgtApp := scenario.TgtApp + kubectlSrc := scenario.KubectlSrc + kubectlTgt := scenario.KubectlTgt + runner := scenario.Crane + paths, err := NewScenarioPaths("crane-ca8-*") + Expect(err).NotTo(HaveOccurred()) + + exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir, + LabelSelector: "app=" + appName} + transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} + applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + OutputDir: paths.OutputDir} + + inScopeSA := ServiceAccount{Name: "nginx-sa", Namespace: namespace, Label: "app=simple-nginx-nopv"} + outOfScopeSA := ServiceAccount{Name: "out-of-scope-sa", Namespace: namespace, Label: "app=outScopedApp"} + + inScopeCR := ClusterRole{Name: "in-scope-cr", Verb: "get,list,watch", Resource: "pods", Label: "app=" + appName} + outOfScopeCR := ClusterRole{Name: "out-scope-cr", Verb: "get,list,watch,create,update,delete", Resource: "pods", Label: "app=outScopedApp"} + + inScopesubject := "--serviceaccount=" + namespace + ":" + inScopeSA.Name + outScopeubject := "--serviceaccount=" + namespace + ":" + outOfScopeSA.Name + + inScopeBinding := ClusterRoleBinding{Name: "in-scope-crb", ClusterRoleName: inScopeCR.Name, Subject: inScopesubject, Label: "app=" + appName} + outOfScopeBinding := ClusterRoleBinding{Name: "out-scope-crb", ClusterRoleName: outOfScopeCR.Name, Subject: outScopeubject, Label: "app=outScopedApp"} + + outOfScopeResources := []utils.ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: outOfScopeBinding.Name}, + {Kind: "ClusterRole", Name: outOfScopeCR.Name}, + } + inScopeResources := []utils.ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: inScopeBinding.Name}, + {Kind: "ClusterRole", Name: inScopeCR.Name}, + } + DeferCleanup(func() { + if err := ResourceCleanup([]KubectlRunner{kubectlSrc, kubectlTgt}, []Resource{ + inScopeBinding, outOfScopeBinding, inScopeCR, outOfScopeCR, inScopeSA, outOfScopeSA}); err != nil { + log.Printf("Resources cleanup: %v", err) + } + if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil { + log.Printf("Scenario cleanup: %v", err) + } + }) + + By("Deploying app on source cluster") + Expect(PrepareSourceApp(srcApp, kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating in-scope ServiceAccount with matching label") + Expect(inScopeSA.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating out-of-scope ServiceAccount with different label") + Expect(outOfScopeSA.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating in-scope ClusterRole with matching label") + Expect(inScopeCR.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating out-of-scope ClusterRole with different label") + Expect(outOfScopeCR.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating in-scope ClusterRoleBinding with matching label") + Expect(inScopeBinding.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating out-of-scope ClusterRoleBinding with different label") + Expect(outOfScopeBinding.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Waiting for source pods and endpoints to drain") + WaitForSourceQuiesce(kubectlSrc, namespace, "app="+appName, serviceName) + + By("Running crane export with label-selector, transform, apply") + Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) + + By("Verifying out-of-scope resources are not in export _cluster directory") + exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") + found, err := utils.AssertClusterResourcesExist(exportClusterPath, outOfScopeResources) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeFalse()) + + By("Verifying in-scope ClusterRole and ClusterRoleBinding exist in export, transform, and output") + found, err = utils.AssertClusterResourcesExist(exportClusterPath, inScopeResources) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + }) +}) diff --git a/e2e-tests/utils/utils.go b/e2e-tests/utils/utils.go index 299cab6d..bf8fc591 100644 --- a/e2e-tests/utils/utils.go +++ b/e2e-tests/utils/utils.go @@ -1361,3 +1361,44 @@ func ParseValidationReport(validateDir string, outputFormat string, report inter return nil } + +type ClusterResourceMatch struct { + Kind string + Name string + Version string // optional, empty means wildcard + Group string // optional, empty means wildcard +} + +func AssertClusterResourcesExist(dir string, resources []ClusterResourceMatch) (bool, error) { + existingFiles, err := ListFilesRecursivelyAsList(dir) + fmt.Println("=================existing files==============================") + fmt.Println(existingFiles) + fmt.Println("=============================================================") + if err != nil || len(existingFiles) == 0 { + return false, err + } + + for _, r := range resources { + prefix := r.Kind + if len(r.Group) > 0 { + prefix = prefix + "_" + r.Group + } + if len(r.Version) > 0 { + prefix = prefix + "_" + r.Version + } + suffix := "_" + r.Name + ".yaml" + found := false + for _, file := range existingFiles { + // under score is for avoiding missmatch such as : my-crb.yaml could match other-my-crb.yaml. + if strings.HasPrefix(file, prefix) && strings.HasSuffix(file, suffix) { + found = true + break + } + } + if !found { + return false, nil + } + } + + return true, nil +} diff --git a/e2e-tests/utils/utils_test.go b/e2e-tests/utils/utils_test.go index b4e19c0d..38e0d682 100644 --- a/e2e-tests/utils/utils_test.go +++ b/e2e-tests/utils/utils_test.go @@ -1667,3 +1667,131 @@ func TestCompareDirectoryYAMLSemanticsUnordered(t *testing.T) { }) } } + +func TestAssertClusterResourcesExist(t *testing.T) { + // Helper to create dummy cluster resource files in a temp directory + createClusterResourceFiles := func(t *testing.T, dir string, files []string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + for _, f := range files { + path := filepath.Join(dir, f) + if err := os.WriteFile(path, []byte("dummy"), 0o644); err != nil { + t.Fatal(err) + } + } + } + + cases := []struct { + name string + files []string + resources []ClusterResourceMatch + wantFound bool + wantErr bool + }{ + { + name: "finds_single_cluster_role_binding", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_my-crb.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantFound: true, + }, + { + name: "finds_cluster_role_with_group", + files: []string{ + "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_my-role.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRole", Name: "my-role", Group: "rbac.authorization.k8s.io"}, + }, + wantFound: true, + }, + { + name: "finds_cluster_role_with_group_and_version", + files: []string{ + "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_my-role.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRole", Name: "my-role", Group: "rbac.authorization.k8s.io", Version: "v1"}, + }, + wantFound: true, + }, + { + name: "finds_multiple_resources", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_crb-one.yaml", + "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_role-one.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "crb-one"}, + {Kind: "ClusterRole", Name: "role-one"}, + }, + wantFound: true, + }, + { + name: "returns_false_when_resource_not_found", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_other-crb.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantFound: false, + }, + { + name: "returns_false_when_one_of_multiple_not_found", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_crb-one.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "crb-one"}, + {Kind: "ClusterRole", Name: "role-missing"}, + }, + wantFound: false, + }, + { + name: "returns_false_for_empty_directory", + files: []string{}, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantFound: false, + }, + { + name: "does_not_match_partial_name", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_other-my-crb.yaml", + }, + resources: []ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantFound: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + createClusterResourceFiles(t, dir, tc.files) + + found, err := AssertClusterResourcesExist(dir, tc.resources) + if tc.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("AssertClusterResourcesExist: %v", err) + } + if found != tc.wantFound { + t.Fatalf("AssertClusterResourcesExist = %v, want %v", found, tc.wantFound) + } + }) + } +} From 450eea2120471866ad409f0f35cce4a831088796 Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Mon, 13 Jul 2026 14:18:50 +0300 Subject: [PATCH 2/7] added ca7 test Signed-off-by: Ran Wurmbrand --- .../tests/tier1/ca7_unrelated_crb_test.go | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 e2e-tests/tests/tier1/ca7_unrelated_crb_test.go diff --git a/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go b/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go new file mode 100644 index 00000000..31b8f8f2 --- /dev/null +++ b/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go @@ -0,0 +1,101 @@ +package e2e + +import ( + "log" + "path/filepath" + + "github.com/konveyor/crane/e2e-tests/config" + . "github.com/konveyor/crane/e2e-tests/framework" + "github.com/konveyor/crane/e2e-tests/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Cluster-level export filtering", func() { + It("[CA-7] Should not export CRB with subject from another namespace", Label("cluster-admin"), func() { + appName := "nginx-with-serviceaccount" + namespace := "simple-nginx-nopv" + serviceName := "my-" + appName + scenario := NewMigrationScenario( + appName, + namespace, + config.K8sDeployBin, + config.CraneBin, + config.SourceContext, + config.TargetContext, + ) + srcApp := scenario.SrcApp + tgtApp := scenario.TgtApp + kubectlSrc := scenario.KubectlSrc + kubectlTgt := scenario.KubectlTgt + runner := scenario.Crane + paths, err := NewScenarioPaths("crane-ca7-*") + Expect(err).NotTo(HaveOccurred()) + + exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir} + transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} + applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + OutputDir: paths.OutputDir} + + cr := ClusterRole{Name: "crane-cr", Verb: "get,list,watch", Resource: "pods", Label: "app=" + appName} + forigenNamespace := Namespace{Name: "forigen-name-space"} + + foreignSA := ServiceAccount{Name: "forigen-nginx-sa", Namespace: forigenNamespace.Name} + forigenSubject := "--serviceaccount=" + forigenNamespace.Name + ":" + foreignSA.Name + foreignCRB := ClusterRoleBinding{Name: "forigen-crb", ClusterRoleName: cr.Name, Subject: forigenSubject} + + relatedSa := ServiceAccount{Name: "nginx-sa", Namespace: namespace} + testSubject := "--serviceaccount=" + namespace + ":" + relatedSa.Name + testCRB := ClusterRoleBinding{Name: "test-crb", ClusterRoleName: cr.Name, Subject: testSubject} + + DeferCleanup(func() { + if err := ResourceCleanup([]KubectlRunner{kubectlSrc, kubectlTgt}, []Resource{ + cr, foreignSA, foreignCRB, relatedSa, testCRB, forigenNamespace}); err != nil { + log.Printf("Resources cleanup: %v", err) + } + if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil { + log.Printf("Scenario cleanup: %v", err) + } + }) + + By("Deploying app with ServiceAccount on source cluster") + Expect(PrepareSourceApp(srcApp, kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating ClusterRole on source") + Expect(cr.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating foreign namespace on source") + Expect(forigenNamespace.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating ServiceAccount in foreign namespace") + Expect(foreignSA.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating ClusterRoleBinding referencing app's ServiceAccount") + Expect(testCRB.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating ClusterRoleBinding referencing foreign namespace ServiceAccount") + Expect(foreignCRB.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Waiting for source pods and endpoints to drain") + WaitForSourceQuiesce(kubectlSrc, namespace, "app="+appName, serviceName) + + By("Running crane export, transform, apply") + Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) + + By("Verifying out-of-scope resources are not in export _cluster directory") + exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") + found, err := utils.AssertClusterResourcesExist(exportClusterPath, []utils.ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: foreignCRB.Name}, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeFalse()) + + By("Verifying linked ClusterRole and ClusterRoleBinding exist in export, transform, and output") + found, err = utils.AssertClusterResourcesExist(exportClusterPath, []utils.ClusterResourceMatch{ + {Kind: "ClusterRoleBinding", Name: testCRB.Name}, + {Kind: "ClusterRole", Name: cr.Name}, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + }) +}) From a70ee4d652cf90b1f6088769526271ccc90984d2 Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Mon, 13 Jul 2026 17:02:20 +0300 Subject: [PATCH 3/7] added test with the utils, and test data needed Signed-off-by: Ran Wurmbrand --- e2e-tests/testdata/gadget_cr.yaml | 7 + e2e-tests/testdata/gadget_crd.yaml | 27 +++ e2e-tests/tests/tier0/ca10_crd_flags_test.go | 217 ++++++++++++++++++ .../tier0/ca8_label_scoped_export_test.go | 16 +- .../tests/tier1/ca7_unrelated_crb_test.go | 35 +-- e2e-tests/utils/utils.go | 30 ++- e2e-tests/utils/utils_test.go | 56 +++-- 7 files changed, 344 insertions(+), 44 deletions(-) create mode 100644 e2e-tests/testdata/gadget_cr.yaml create mode 100644 e2e-tests/testdata/gadget_crd.yaml create mode 100644 e2e-tests/tests/tier0/ca10_crd_flags_test.go diff --git a/e2e-tests/testdata/gadget_cr.yaml b/e2e-tests/testdata/gadget_cr.yaml new file mode 100644 index 00000000..dd677444 --- /dev/null +++ b/e2e-tests/testdata/gadget_cr.yaml @@ -0,0 +1,7 @@ +apiVersion: crane-e2e.openshift.io/v1 +kind: Gadget +metadata: + name: test-gadget +spec: + color: red + size: 3 diff --git a/e2e-tests/testdata/gadget_crd.yaml b/e2e-tests/testdata/gadget_crd.yaml new file mode 100644 index 00000000..ac9b4f2c --- /dev/null +++ b/e2e-tests/testdata/gadget_crd.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: gadgets.crane-e2e.openshift.io +spec: + group: crane-e2e.openshift.io + names: + kind: Gadget + listKind: GadgetList + plural: gadgets + singular: gadget + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + color: + type: string + size: + type: integer diff --git a/e2e-tests/tests/tier0/ca10_crd_flags_test.go b/e2e-tests/tests/tier0/ca10_crd_flags_test.go new file mode 100644 index 00000000..ed77b63b --- /dev/null +++ b/e2e-tests/tests/tier0/ca10_crd_flags_test.go @@ -0,0 +1,217 @@ +package e2e + +import ( + "log" + "path/filepath" + + "github.com/konveyor/crane/e2e-tests/config" + . "github.com/konveyor/crane/e2e-tests/framework" + "github.com/konveyor/crane/e2e-tests/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CRD group filtering during export", func() { + appName := "simple-nginx-nopv" + namespace := "simple-nginx-nopv" + serviceName := "my-" + appName + It("[CA-10a] Should skip CRD when --crd-skip-group matches", Label("tier0"), func() { + scenario := NewMigrationScenario( + appName, + namespace, + config.K8sDeployBin, + config.CraneBin, + config.SourceContext, + config.TargetContext, + ) + srcApp := scenario.SrcApp + tgtApp := scenario.TgtApp + kubectlSrc := scenario.KubectlSrc + kubectlTgt := scenario.KubectlTgt + runner := scenario.Crane + + paths, err := NewScenarioPaths("crane-ca10a-*") + Expect(err).NotTo(HaveOccurred()) + crdYAML, err := utils.ReadTestdataFile("widget_crd.yaml") + Expect(err).NotTo(HaveOccurred()) + crYAML, err := utils.ReadTestdataFile("widget_cr.yaml") + Expect(err).NotTo(HaveOccurred()) + + crd := CustomResourceDefinition{ + Name: "widgets.crane-e2e.example.com", + YAML: crdYAML, + } + + cr := CustomResource{ + Name: "test-widget", + Namespace: namespace, + Kind: "Widget", + YAML: crYAML, + Resource: "widgets", + } + excludedResource := []utils.ResourceMatch{ + {Kind: "CustomResourceDefinition", Name: crd.Name}, + } + includedResource := []utils.ResourceMatch{ + {Kind: cr.Kind, Name: cr.Name, Scope: namespace}, + } + exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir, + CRDSkipGroups: []string{"crane-e2e.example.com"}} + transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} + applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + OutputDir: paths.OutputDir} + + DeferCleanup(func() { + if err := ResourceCleanup([]KubectlRunner{kubectlSrc, kubectlTgt}, []Resource{cr, crd}); err != nil { + log.Printf("Resources cleanup: %v", err) + } + if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil { + log.Printf("Scenario cleanup: %v", err) + } + }) + + By("Deploying app on source cluster") + Expect(PrepareSourceApp(srcApp, kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating Widget CRD on source") + Expect(crd.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Waiting for CRD to be established") + Expect(crd.WaitForEstablished(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating Widget custom resource in namespace") + Expect(cr.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Waiting for source pods and endpoints to drain") + WaitForSourceQuiesce(kubectlSrc, namespace, "app="+appName, serviceName) + + By("Running crane export with --crd-skip-group, transform, apply") + Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) + + By("Verifying CRD is excluded from export") + found, err := utils.AssertResourcesExist(paths.ExportDir, excludedResource) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeFalse()) + + By("Verifying Widget CR exists in namespace export directory") + nameSpaceDir := filepath.Join(paths.ExportDir, "resources", namespace) + found, err = utils.AssertResourcesExist(nameSpaceDir, includedResource) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + }) + + It("[CA-10b] Should include CRD when --crd-include-group matches", Label("tier0"), func() { + scenario := NewMigrationScenario( + appName, + namespace, + config.K8sDeployBin, + config.CraneBin, + config.SourceContext, + config.TargetContext, + ) + srcApp := scenario.SrcApp + tgtApp := scenario.TgtApp + kubectlSrc := scenario.KubectlSrc + kubectlTgt := scenario.KubectlTgt + runner := scenario.Crane + + paths, err := NewScenarioPaths("crane-ca10b-*") + Expect(err).NotTo(HaveOccurred()) + crdYAML, err := utils.ReadTestdataFile("gadget_crd.yaml") + Expect(err).NotTo(HaveOccurred()) + crYAML, err := utils.ReadTestdataFile("gadget_cr.yaml") + Expect(err).NotTo(HaveOccurred()) + + crd := CustomResourceDefinition{ + Name: "gadgets.crane-e2e.openshift.io", + YAML: crdYAML, + } + cr := CustomResource{ + Name: "test-gadget", + Namespace: namespace, + Kind: "Gadget", + YAML: crYAML, + Resource: "gadgets", + } + tgtNameSpace := Namespace{Name: namespace} + + exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir, + CRDIncludeGroups: []string{"crane-e2e.openshift.io"}} + transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} + applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + OutputDir: paths.OutputDir} + + DeferCleanup(func() { + if err := ResourceCleanup([]KubectlRunner{kubectlSrc, kubectlTgt}, []Resource{cr, crd, tgtNameSpace}); err != nil { + log.Printf("Resources cleanup: %v", err) + } + if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil { + log.Printf("Scenario cleanup: %v", err) + } + }) + + By("Deploying app on source cluster") + Expect(PrepareSourceApp(srcApp, kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating Gadget CRD on source") + Expect(crd.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Waiting for CRD to be established") + Expect(crd.WaitForEstablished(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating Gadget custom resource in namespace") + Expect(cr.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Waiting for source pods and endpoints to drain") + WaitForSourceQuiesce(kubectlSrc, namespace, "app="+appName, serviceName) + + By("Running crane export with --crd-include-group, transform, apply") + Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) + + By("Verifying CRD exists in export _cluster directory") + exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") + found, err := utils.AssertResourcesExist(exportClusterPath, []utils.ResourceMatch{ + {Kind: "CustomResourceDefinition", Name: crd.Name}}) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + + By("Verifying Gadget CR exists in namespace export directory") + namespaceDir := filepath.Join(paths.ExportDir, "resources", namespace) + found, err = utils.AssertResourcesExist(namespaceDir, []utils.ResourceMatch{ + {Kind: cr.Kind, Name: cr.Name, Scope: namespace}}) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + + // By("Verifying CRD exists in output _cluster directory") + // outputClusterPath := filepath.Join(paths.OutputDir, "resources", "_cluster") + // Expect(ValidateDirResources(outputClusterPath, crdPatterns)).NotTo(HaveOccurred()) + + By("Creating namespace on target cluster") + Expect(tgtNameSpace.Create(kubectlTgt)).NotTo(HaveOccurred()) + + By("Applying cluster resources to target") + Expect(kubectlTgt.ApplyDir(filepath.Join(paths.OutputDir, "resources", "_cluster"))).NotTo(HaveOccurred()) + + By("Waiting for CRD to be established on target") + Expect(crd.WaitForEstablished(kubectlTgt)).NotTo(HaveOccurred()) + + By("Applying namespace resources to target") + Expect(kubectlTgt.ApplyDir(filepath.Join(paths.OutputDir, "resources", namespace))).NotTo(HaveOccurred()) + + By("Verifying Gadget CR exists on target") + _, err = kubectlTgt.Run("get", "gadget", cr.Name, "-n", namespace) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Gadget CR has correct spec values on target") + color, err := kubectlTgt.Run("get", "gadget", cr.Name, "-n", namespace, + "-o", "jsonpath={.spec.color}") + Expect(err).NotTo(HaveOccurred()) + Expect(color).To(Equal("red")) + + By("Scaling target deployment and validating app") + Expect(kubectlTgt.ScaleDeployment(namespace, appName, 1)).NotTo(HaveOccurred()) + Eventually(tgtApp.Validate, "5m", "10s").Should(Succeed()) + + }) + +}) diff --git a/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go b/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go index 76de2c62..55a37570 100644 --- a/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go +++ b/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go @@ -45,17 +45,17 @@ var _ = Describe("Cluster-level export filtering", func() { inScopeCR := ClusterRole{Name: "in-scope-cr", Verb: "get,list,watch", Resource: "pods", Label: "app=" + appName} outOfScopeCR := ClusterRole{Name: "out-scope-cr", Verb: "get,list,watch,create,update,delete", Resource: "pods", Label: "app=outScopedApp"} - inScopesubject := "--serviceaccount=" + namespace + ":" + inScopeSA.Name - outScopeubject := "--serviceaccount=" + namespace + ":" + outOfScopeSA.Name + inScopeSubject := "--serviceaccount=" + namespace + ":" + inScopeSA.Name + outScopeSubject := "--serviceaccount=" + namespace + ":" + outOfScopeSA.Name - inScopeBinding := ClusterRoleBinding{Name: "in-scope-crb", ClusterRoleName: inScopeCR.Name, Subject: inScopesubject, Label: "app=" + appName} - outOfScopeBinding := ClusterRoleBinding{Name: "out-scope-crb", ClusterRoleName: outOfScopeCR.Name, Subject: outScopeubject, Label: "app=outScopedApp"} + inScopeBinding := ClusterRoleBinding{Name: "in-scope-crb", ClusterRoleName: inScopeCR.Name, Subject: inScopeSubject, Label: "app=" + appName} + outOfScopeBinding := ClusterRoleBinding{Name: "out-scope-crb", ClusterRoleName: outOfScopeCR.Name, Subject: outScopeSubject, Label: "app=outScopedApp"} - outOfScopeResources := []utils.ClusterResourceMatch{ + outOfScopeResources := []utils.ResourceMatch{ {Kind: "ClusterRoleBinding", Name: outOfScopeBinding.Name}, {Kind: "ClusterRole", Name: outOfScopeCR.Name}, } - inScopeResources := []utils.ClusterResourceMatch{ + inScopeResources := []utils.ResourceMatch{ {Kind: "ClusterRoleBinding", Name: inScopeBinding.Name}, {Kind: "ClusterRole", Name: inScopeCR.Name}, } @@ -98,12 +98,12 @@ var _ = Describe("Cluster-level export filtering", func() { By("Verifying out-of-scope resources are not in export _cluster directory") exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") - found, err := utils.AssertClusterResourcesExist(exportClusterPath, outOfScopeResources) + found, err := utils.AssertResourcesExist(exportClusterPath, outOfScopeResources) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeFalse()) By("Verifying in-scope ClusterRole and ClusterRoleBinding exist in export, transform, and output") - found, err = utils.AssertClusterResourcesExist(exportClusterPath, inScopeResources) + found, err = utils.AssertResourcesExist(exportClusterPath, inScopeResources) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeTrue()) }) diff --git a/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go b/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go index 31b8f8f2..d7c274c2 100644 --- a/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go +++ b/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go @@ -13,7 +13,7 @@ import ( var _ = Describe("Cluster-level export filtering", func() { It("[CA-7] Should not export CRB with subject from another namespace", Label("cluster-admin"), func() { - appName := "nginx-with-serviceaccount" + appName := "simple-nginx-nopv" namespace := "simple-nginx-nopv" serviceName := "my-" + appName scenario := NewMigrationScenario( @@ -38,11 +38,11 @@ var _ = Describe("Cluster-level export filtering", func() { OutputDir: paths.OutputDir} cr := ClusterRole{Name: "crane-cr", Verb: "get,list,watch", Resource: "pods", Label: "app=" + appName} - forigenNamespace := Namespace{Name: "forigen-name-space"} + unrelatedNamespace := Namespace{Name: "unrelated-name-space"} - foreignSA := ServiceAccount{Name: "forigen-nginx-sa", Namespace: forigenNamespace.Name} - forigenSubject := "--serviceaccount=" + forigenNamespace.Name + ":" + foreignSA.Name - foreignCRB := ClusterRoleBinding{Name: "forigen-crb", ClusterRoleName: cr.Name, Subject: forigenSubject} + unrelatedSA := ServiceAccount{Name: "unrelated-nginx-sa", Namespace: unrelatedNamespace.Name} + unrelatedSubject := "--serviceaccount=" + unrelatedNamespace.Name + ":" + unrelatedSA.Name + unrelatedCRB := ClusterRoleBinding{Name: "unrelated-crb", ClusterRoleName: cr.Name, Subject: unrelatedSubject} relatedSa := ServiceAccount{Name: "nginx-sa", Namespace: namespace} testSubject := "--serviceaccount=" + namespace + ":" + relatedSa.Name @@ -50,7 +50,7 @@ var _ = Describe("Cluster-level export filtering", func() { DeferCleanup(func() { if err := ResourceCleanup([]KubectlRunner{kubectlSrc, kubectlTgt}, []Resource{ - cr, foreignSA, foreignCRB, relatedSa, testCRB, forigenNamespace}); err != nil { + cr, unrelatedSA, unrelatedCRB, relatedSa, testCRB, unrelatedNamespace}); err != nil { log.Printf("Resources cleanup: %v", err) } if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil { @@ -64,18 +64,21 @@ var _ = Describe("Cluster-level export filtering", func() { By("Creating ClusterRole on source") Expect(cr.Create(kubectlSrc)).NotTo(HaveOccurred()) - By("Creating foreign namespace on source") - Expect(forigenNamespace.Create(kubectlSrc)).NotTo(HaveOccurred()) + By("Creating unrelated namespace on source") + Expect(unrelatedNamespace.Create(kubectlSrc)).NotTo(HaveOccurred()) - By("Creating ServiceAccount in foreign namespace") - Expect(foreignSA.Create(kubectlSrc)).NotTo(HaveOccurred()) + By("Creating ServiceAccount in unrelated namespace") + Expect(unrelatedSA.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating ClusterRoleBinding referencing foreign namespace ServiceAccount") + Expect(unrelatedCRB.Create(kubectlSrc)).NotTo(HaveOccurred()) + + By("Creating related ServiceAccount in app namespace") + Expect(relatedSa.Create(kubectlSrc)).NotTo(HaveOccurred()) By("Creating ClusterRoleBinding referencing app's ServiceAccount") Expect(testCRB.Create(kubectlSrc)).NotTo(HaveOccurred()) - By("Creating ClusterRoleBinding referencing foreign namespace ServiceAccount") - Expect(foreignCRB.Create(kubectlSrc)).NotTo(HaveOccurred()) - By("Waiting for source pods and endpoints to drain") WaitForSourceQuiesce(kubectlSrc, namespace, "app="+appName, serviceName) @@ -84,14 +87,14 @@ var _ = Describe("Cluster-level export filtering", func() { By("Verifying out-of-scope resources are not in export _cluster directory") exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") - found, err := utils.AssertClusterResourcesExist(exportClusterPath, []utils.ClusterResourceMatch{ - {Kind: "ClusterRoleBinding", Name: foreignCRB.Name}, + found, err := utils.AssertResourcesExist(exportClusterPath, []utils.ResourceMatch{ + {Kind: "ClusterRoleBinding", Name: unrelatedCRB.Name}, }) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeFalse()) By("Verifying linked ClusterRole and ClusterRoleBinding exist in export, transform, and output") - found, err = utils.AssertClusterResourcesExist(exportClusterPath, []utils.ClusterResourceMatch{ + found, err = utils.AssertResourcesExist(exportClusterPath, []utils.ResourceMatch{ {Kind: "ClusterRoleBinding", Name: testCRB.Name}, {Kind: "ClusterRole", Name: cr.Name}, }) diff --git a/e2e-tests/utils/utils.go b/e2e-tests/utils/utils.go index bf8fc591..a047f144 100644 --- a/e2e-tests/utils/utils.go +++ b/e2e-tests/utils/utils.go @@ -1362,18 +1362,28 @@ func ParseValidationReport(validateDir string, outputFormat string, report inter return nil } -type ClusterResourceMatch struct { +// ResourceMatch defines criteria for matching an exported resource file. +// Crane export filenames follow the pattern: +// +// Cluster-scoped: ___clusterscoped_.yaml +// Namespace-scoped: ____.yaml +// +// Only Kind and Name are required. Group and Version narrow the match +// but must be specified together in order (Group before Version). +type ResourceMatch struct { Kind string Name string + Scope string // optional, empty means clusterscoped Version string // optional, empty means wildcard Group string // optional, empty means wildcard } -func AssertClusterResourcesExist(dir string, resources []ClusterResourceMatch) (bool, error) { +// AssertResourcesExist checks if all specified resources exist in the directory. +// Pass the directory containing the YAML files directly (e.g., the _cluster dir +// for cluster-scoped, or the namespace dir for namespace-scoped resources). +// Returns (true, nil) if all match, (false, nil) if any missing, or (false, err) on error. +func AssertResourcesExist(dir string, resources []ResourceMatch) (bool, error) { existingFiles, err := ListFilesRecursivelyAsList(dir) - fmt.Println("=================existing files==============================") - fmt.Println(existingFiles) - fmt.Println("=============================================================") if err != nil || len(existingFiles) == 0 { return false, err } @@ -1386,10 +1396,16 @@ func AssertClusterResourcesExist(dir string, resources []ClusterResourceMatch) ( if len(r.Version) > 0 { prefix = prefix + "_" + r.Version } - suffix := "_" + r.Name + ".yaml" + + scope := "clusterscoped" + if r.Scope != "" { + scope = r.Scope + } + // under score is for avoiding missmatch such as: + // ns1_my-crb.yaml could match other-ns_my-crb.yaml. + suffix := "_" + scope + "_" + r.Name + ".yaml" found := false for _, file := range existingFiles { - // under score is for avoiding missmatch such as : my-crb.yaml could match other-my-crb.yaml. if strings.HasPrefix(file, prefix) && strings.HasSuffix(file, suffix) { found = true break diff --git a/e2e-tests/utils/utils_test.go b/e2e-tests/utils/utils_test.go index 38e0d682..4364847f 100644 --- a/e2e-tests/utils/utils_test.go +++ b/e2e-tests/utils/utils_test.go @@ -1668,7 +1668,7 @@ func TestCompareDirectoryYAMLSemanticsUnordered(t *testing.T) { } } -func TestAssertClusterResourcesExist(t *testing.T) { +func TestAssertResourcesExist(t *testing.T) { // Helper to create dummy cluster resource files in a temp directory createClusterResourceFiles := func(t *testing.T, dir string, files []string) { t.Helper() @@ -1686,7 +1686,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { cases := []struct { name string files []string - resources []ClusterResourceMatch + resources []ResourceMatch wantFound bool wantErr bool }{ @@ -1695,7 +1695,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { files: []string{ "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_my-crb.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRoleBinding", Name: "my-crb"}, }, wantFound: true, @@ -1705,7 +1705,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { files: []string{ "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_my-role.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRole", Name: "my-role", Group: "rbac.authorization.k8s.io"}, }, wantFound: true, @@ -1715,7 +1715,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { files: []string{ "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_my-role.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRole", Name: "my-role", Group: "rbac.authorization.k8s.io", Version: "v1"}, }, wantFound: true, @@ -1726,7 +1726,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_crb-one.yaml", "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_role-one.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRoleBinding", Name: "crb-one"}, {Kind: "ClusterRole", Name: "role-one"}, }, @@ -1737,7 +1737,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { files: []string{ "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_other-crb.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRoleBinding", Name: "my-crb"}, }, wantFound: false, @@ -1747,7 +1747,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { files: []string{ "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_crb-one.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRoleBinding", Name: "crb-one"}, {Kind: "ClusterRole", Name: "role-missing"}, }, @@ -1756,7 +1756,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { { name: "returns_false_for_empty_directory", files: []string{}, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRoleBinding", Name: "my-crb"}, }, wantFound: false, @@ -1766,11 +1766,41 @@ func TestAssertClusterResourcesExist(t *testing.T) { files: []string{ "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_other-my-crb.yaml", }, - resources: []ClusterResourceMatch{ + resources: []ResourceMatch{ {Kind: "ClusterRoleBinding", Name: "my-crb"}, }, wantFound: false, }, + { + name: "finds_namespace_scoped_resource", + files: []string{ + "Widget_crane-e2e.example.com_v1_myns_test-widget.yaml", + }, + resources: []ResourceMatch{ + {Kind: "Widget", Name: "test-widget", Scope: "myns"}, + }, + wantFound: true, + }, + { + name: "finds_namespace_scoped_with_group_and_version", + files: []string{ + "Deployment_apps_v1_default_my-deploy.yaml", + }, + resources: []ResourceMatch{ + {Kind: "Deployment", Name: "my-deploy", Scope: "default", Group: "apps", Version: "v1"}, + }, + wantFound: true, + }, + { + name: "does_not_match_wrong_namespace", + files: []string{ + "Widget_crane-e2e.example.com_v1_other-ns_test-widget.yaml", + }, + resources: []ResourceMatch{ + {Kind: "Widget", Name: "test-widget", Scope: "myns"}, + }, + wantFound: false, + }, } for _, tc := range cases { @@ -1779,7 +1809,7 @@ func TestAssertClusterResourcesExist(t *testing.T) { dir := t.TempDir() createClusterResourceFiles(t, dir, tc.files) - found, err := AssertClusterResourcesExist(dir, tc.resources) + found, err := AssertResourcesExist(dir, tc.resources) if tc.wantErr { if err == nil { t.Fatal("expected error, got nil") @@ -1787,10 +1817,10 @@ func TestAssertClusterResourcesExist(t *testing.T) { return } if err != nil { - t.Fatalf("AssertClusterResourcesExist: %v", err) + t.Fatalf("AssertResourcesExist: %v", err) } if found != tc.wantFound { - t.Fatalf("AssertClusterResourcesExist = %v, want %v", found, tc.wantFound) + t.Fatalf("AssertResourcesExist = %v, want %v", found, tc.wantFound) } }) } From a8a60600b6302b7a803bd95f802a2435ce5e04fb Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Tue, 14 Jul 2026 14:27:50 +0300 Subject: [PATCH 4/7] addressed coderabbit comments and added function for assertions that no file exist Signed-off-by: Ran Wurmbrand --- e2e-tests/framework/resources.go | 3 +- .../tier0/ca8_label_scoped_export_test.go | 8 +- e2e-tests/utils/utils.go | 59 +++-- e2e-tests/utils/utils_test.go | 220 ++++++++++++++++++ 4 files changed, 269 insertions(+), 21 deletions(-) diff --git a/e2e-tests/framework/resources.go b/e2e-tests/framework/resources.go index 669fe1f7..0737cd3a 100644 --- a/e2e-tests/framework/resources.go +++ b/e2e-tests/framework/resources.go @@ -136,7 +136,8 @@ func (sa ServiceAccount) Create(k KubectlRunner) error { if sa.Label != "" { _, err = k.Run("label", "serviceaccount", sa.Name, "-n", sa.Namespace, sa.Label) if err != nil { - return fmt.Errorf("failed to label ServiceAccount %s: %w", sa.Name, err) + return fmt.Errorf("failed to label ServiceAccount %q in namespace %q with label %q: %w", + sa.Name, sa.Namespace, sa.Label, err) } } return nil diff --git a/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go b/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go index 55a37570..a546b03b 100644 --- a/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go +++ b/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go @@ -98,13 +98,13 @@ var _ = Describe("Cluster-level export filtering", func() { By("Verifying out-of-scope resources are not in export _cluster directory") exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") - found, err := utils.AssertResourcesExist(exportClusterPath, outOfScopeResources) + allExcluded, err := utils.AssertResourcesDontExist(exportClusterPath, outOfScopeResources) Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse()) + Expect(allExcluded).To(BeTrue()) By("Verifying in-scope ClusterRole and ClusterRoleBinding exist in export, transform, and output") - found, err = utils.AssertResourcesExist(exportClusterPath, inScopeResources) + allFound, err := utils.AssertResourcesExist(exportClusterPath, inScopeResources) Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue()) + Expect(allFound).To(BeTrue()) }) }) diff --git a/e2e-tests/utils/utils.go b/e2e-tests/utils/utils.go index a047f144..b31a90c9 100644 --- a/e2e-tests/utils/utils.go +++ b/e2e-tests/utils/utils.go @@ -1378,6 +1378,29 @@ type ResourceMatch struct { Group string // optional, empty means wildcard } +func getPrefixAndSuffix(r ResourceMatch) (string, string) { + prefix := r.Kind + "_" + if len(r.Group) > 0 { + prefix = prefix + r.Group + "_" + } + + scope := "clusterscoped" + if r.Scope != "" { + scope = r.Scope + } + // under score is for avoiding missmatch such as: + // ns1_my-crb.yaml could match other-ns_my-crb.yaml. + suffix := "_" + scope + "_" + r.Name + ".yaml" + if len(r.Version) > 0 { + suffix = r.Version + suffix + } + return prefix, suffix +} + +func fileHasPrefixAndSuffix(file, prefix, suffix string) bool { + return strings.HasPrefix(file, prefix) && strings.HasSuffix(file, suffix) +} + // AssertResourcesExist checks if all specified resources exist in the directory. // Pass the directory containing the YAML files directly (e.g., the _cluster dir // for cluster-scoped, or the namespace dir for namespace-scoped resources). @@ -1389,24 +1412,10 @@ func AssertResourcesExist(dir string, resources []ResourceMatch) (bool, error) { } for _, r := range resources { - prefix := r.Kind - if len(r.Group) > 0 { - prefix = prefix + "_" + r.Group - } - if len(r.Version) > 0 { - prefix = prefix + "_" + r.Version - } - - scope := "clusterscoped" - if r.Scope != "" { - scope = r.Scope - } - // under score is for avoiding missmatch such as: - // ns1_my-crb.yaml could match other-ns_my-crb.yaml. - suffix := "_" + scope + "_" + r.Name + ".yaml" + prefix, suffix := getPrefixAndSuffix(r) found := false for _, file := range existingFiles { - if strings.HasPrefix(file, prefix) && strings.HasSuffix(file, suffix) { + if fileHasPrefixAndSuffix(file, prefix, suffix) { found = true break } @@ -1415,6 +1424,24 @@ func AssertResourcesExist(dir string, resources []ResourceMatch) (bool, error) { return false, nil } } + return true, nil +} +func AssertResourcesDontExist(dir string, resources []ResourceMatch) (bool, error) { + existingFiles, err := ListFilesRecursivelyAsList(dir) + if err != nil { + return false, err + } + if len(existingFiles) == 0 { + return true, nil + } + for _, r := range resources { + prefix, suffix := getPrefixAndSuffix(r) + for _, file := range existingFiles { + if fileHasPrefixAndSuffix(file, prefix, suffix) { + return false, nil + } + } + } return true, nil } diff --git a/e2e-tests/utils/utils_test.go b/e2e-tests/utils/utils_test.go index 4364847f..9dc1cde2 100644 --- a/e2e-tests/utils/utils_test.go +++ b/e2e-tests/utils/utils_test.go @@ -1825,3 +1825,223 @@ func TestAssertResourcesExist(t *testing.T) { }) } } + +func TestAssertResourcesDontExist(t *testing.T) { + createResourceFiles := func(t *testing.T, dir string, files []string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + for _, f := range files { + path := filepath.Join(dir, f) + if err := os.WriteFile(path, []byte("dummy"), 0o644); err != nil { + t.Fatal(err) + } + } + } + + cases := []struct { + name string + files []string + resources []ResourceMatch + wantExcluded bool + wantErr bool + }{ + { + name: "returns_true_for_empty_directory", + files: []string{}, + resources: []ResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantExcluded: true, + }, + { + name: "returns_true_when_resource_not_found", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_other-crb.yaml", + }, + resources: []ResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantExcluded: true, + }, + { + name: "returns_false_when_resource_exists", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_my-crb.yaml", + }, + resources: []ResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + }, + wantExcluded: false, + }, + { + name: "returns_false_when_any_resource_exists", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_crb-one.yaml", + }, + resources: []ResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "crb-one"}, + {Kind: "ClusterRole", Name: "role-missing"}, + }, + wantExcluded: false, + }, + { + name: "returns_true_when_none_of_multiple_exist", + files: []string{ + "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_other-crb.yaml", + "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_other-role.yaml", + }, + resources: []ResourceMatch{ + {Kind: "ClusterRoleBinding", Name: "my-crb"}, + {Kind: "ClusterRole", Name: "my-role"}, + }, + wantExcluded: true, + }, + { + name: "returns_true_for_namespace_scoped_not_found", + files: []string{ + "Widget_crane-e2e.example.com_v1_other-ns_test-widget.yaml", + }, + resources: []ResourceMatch{ + {Kind: "Widget", Name: "test-widget", Scope: "myns"}, + }, + wantExcluded: true, + }, + { + name: "returns_false_for_namespace_scoped_found", + files: []string{ + "Widget_crane-e2e.example.com_v1_myns_test-widget.yaml", + }, + resources: []ResourceMatch{ + {Kind: "Widget", Name: "test-widget", Scope: "myns"}, + }, + wantExcluded: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + createResourceFiles(t, dir, tc.files) + + excluded, err := AssertResourcesDontExist(dir, tc.resources) + if tc.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("AssertResourcesDontExist: %v", err) + } + if excluded != tc.wantExcluded { + t.Fatalf("AssertResourcesDontExist = %v, want %v", excluded, tc.wantExcluded) + } + }) + } +} + +func TestGetPrefixAndSuffix(t *testing.T) { + cases := []struct { + name string + resource ResourceMatch + wantPrefix string + wantSuffix string + }{ + { + name: "kind_only_defaults_to_clusterscoped", + resource: ResourceMatch{Kind: "ClusterRole", Name: "my-role"}, + wantPrefix: "ClusterRole_", + wantSuffix: "_clusterscoped_my-role.yaml", + }, + { + name: "with_group", + resource: ResourceMatch{Kind: "ClusterRole", Name: "my-role", Group: "rbac.authorization.k8s.io"}, + wantPrefix: "ClusterRole_rbac.authorization.k8s.io_", + wantSuffix: "_clusterscoped_my-role.yaml", + }, + { + name: "with_group_and_version", + resource: ResourceMatch{Kind: "ClusterRole", Name: "my-role", Group: "rbac.authorization.k8s.io", Version: "v1"}, + wantPrefix: "ClusterRole_rbac.authorization.k8s.io_", + wantSuffix: "v1_clusterscoped_my-role.yaml", + }, + { + name: "with_namespace_scope", + resource: ResourceMatch{Kind: "Widget", Name: "test-widget", Scope: "myns"}, + wantPrefix: "Widget_", + wantSuffix: "_myns_test-widget.yaml", + }, + { + name: "full_specification", + resource: ResourceMatch{Kind: "Deployment", Name: "my-app", Group: "apps", Version: "v1", Scope: "default"}, + wantPrefix: "Deployment_apps_", + wantSuffix: "v1_default_my-app.yaml", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + prefix, suffix := getPrefixAndSuffix(tc.resource) + if prefix != tc.wantPrefix { + t.Fatalf("getPrefixAndSuffix prefix = %q, want %q", prefix, tc.wantPrefix) + } + if suffix != tc.wantSuffix { + t.Fatalf("getPrefixAndSuffix suffix = %q, want %q", suffix, tc.wantSuffix) + } + }) + } +} + +func TestFileHasPrefixAndSuffix(t *testing.T) { + cases := []struct { + name string + file string + prefix string + suffix string + want bool + }{ + { + name: "matches_both", + file: "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_my-role.yaml", + prefix: "ClusterRole_", + suffix: "v1_clusterscoped_my-role.yaml", + want: true, + }, + { + name: "prefix_mismatch", + file: "ClusterRoleBinding_rbac.authorization.k8s.io_v1_clusterscoped_my-role.yaml", + prefix: "ClusterRole_", + suffix: "v1_clusterscoped_my-role.yaml", + want: false, + }, + { + name: "suffix_mismatch", + file: "ClusterRole_rbac.authorization.k8s.io_v1_clusterscoped_other-role.yaml", + prefix: "ClusterRole_", + suffix: "v1_clusterscoped_my-role.yaml", + want: false, + }, + { + name: "both_mismatch", + file: "Widget_example.com_v1_ns_widget.yaml", + prefix: "ClusterRole_", + suffix: "_clusterscoped_my-role.yaml", + want: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := fileHasPrefixAndSuffix(tc.file, tc.prefix, tc.suffix) + if got != tc.want { + t.Fatalf("fileHasPrefixAndSuffix(%q, %q, %q) = %v, want %v", + tc.file, tc.prefix, tc.suffix, got, tc.want) + } + }) + } +} From cea6f078d41139d6abd724ac11d77391e1d52868 Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Tue, 14 Jul 2026 16:56:34 +0300 Subject: [PATCH 5/7] Rename ca7/ca8/ca10 tests to mta_868/869/870 Signed-off-by: Ran Wurmbrand --- ..._scoped_export_test.go => mta_868_label_scoped_export_test.go} | 0 .../tier0/{ca10_crd_flags_test.go => mta_869_crd_flags_test.go} | 0 .../{ca7_unrelated_crb_test.go => mta_870_unrelated_crb_test.go} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename e2e-tests/tests/tier0/{ca8_label_scoped_export_test.go => mta_868_label_scoped_export_test.go} (100%) rename e2e-tests/tests/tier0/{ca10_crd_flags_test.go => mta_869_crd_flags_test.go} (100%) rename e2e-tests/tests/tier1/{ca7_unrelated_crb_test.go => mta_870_unrelated_crb_test.go} (100%) diff --git a/e2e-tests/tests/tier0/ca8_label_scoped_export_test.go b/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go similarity index 100% rename from e2e-tests/tests/tier0/ca8_label_scoped_export_test.go rename to e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go diff --git a/e2e-tests/tests/tier0/ca10_crd_flags_test.go b/e2e-tests/tests/tier0/mta_869_crd_flags_test.go similarity index 100% rename from e2e-tests/tests/tier0/ca10_crd_flags_test.go rename to e2e-tests/tests/tier0/mta_869_crd_flags_test.go diff --git a/e2e-tests/tests/tier1/ca7_unrelated_crb_test.go b/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go similarity index 100% rename from e2e-tests/tests/tier1/ca7_unrelated_crb_test.go rename to e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go From e7b566c75d7a53d04863379534dc04628de93712 Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Mon, 20 Jul 2026 16:06:32 +0300 Subject: [PATCH 6/7] addressed comments Signed-off-by: Ran Wurmbrand --- .../tests/tier0/mta_868_label_scoped_export_test.go | 4 ++-- e2e-tests/tests/tier0/mta_869_crd_flags_test.go | 12 ++++-------- e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go | 8 ++++---- e2e-tests/utils/utils.go | 6 +++++- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go b/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go index a546b03b..d982cae5 100644 --- a/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go +++ b/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go @@ -12,7 +12,7 @@ import ( ) var _ = Describe("Cluster-level export filtering", func() { - It("[CA-8] Should export only labeled workload and its RBAC with --label-selector", Label("tier0"), func() { + It("[MTA-868] Should export only labeled workload and its RBAC with --label-selector", Label("tier0"), func() { appName := "simple-nginx-nopv" namespace := "simple-nginx-nopv" serviceName := "my-" + appName @@ -102,7 +102,7 @@ var _ = Describe("Cluster-level export filtering", func() { Expect(err).NotTo(HaveOccurred()) Expect(allExcluded).To(BeTrue()) - By("Verifying in-scope ClusterRole and ClusterRoleBinding exist in export, transform, and output") + By("Verifying in-scope ClusterRole and ClusterRoleBinding exist after export") allFound, err := utils.AssertResourcesExist(exportClusterPath, inScopeResources) Expect(err).NotTo(HaveOccurred()) Expect(allFound).To(BeTrue()) diff --git a/e2e-tests/tests/tier0/mta_869_crd_flags_test.go b/e2e-tests/tests/tier0/mta_869_crd_flags_test.go index ed77b63b..ed5968a8 100644 --- a/e2e-tests/tests/tier0/mta_869_crd_flags_test.go +++ b/e2e-tests/tests/tier0/mta_869_crd_flags_test.go @@ -15,7 +15,7 @@ var _ = Describe("CRD group filtering during export", func() { appName := "simple-nginx-nopv" namespace := "simple-nginx-nopv" serviceName := "my-" + appName - It("[CA-10a] Should skip CRD when --crd-skip-group matches", Label("tier0"), func() { + It("[MTA-869A] Should skip CRD when --crd-skip-group matches", Label("tier0"), func() { scenario := NewMigrationScenario( appName, namespace, @@ -89,9 +89,9 @@ var _ = Describe("CRD group filtering during export", func() { Expect(RunCranePipelineWithChecks(runner, exportOpts, transformOpts, applyOpts)).NotTo(HaveOccurred()) By("Verifying CRD is excluded from export") - found, err := utils.AssertResourcesExist(paths.ExportDir, excludedResource) + found, err := utils.AssertResourcesDontExist(filepath.Join(paths.ExportDir, "resources", namespace, "_cluster"), excludedResource) Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse()) + Expect(found).To(BeTrue()) By("Verifying Widget CR exists in namespace export directory") nameSpaceDir := filepath.Join(paths.ExportDir, "resources", namespace) @@ -100,7 +100,7 @@ var _ = Describe("CRD group filtering during export", func() { Expect(found).To(BeTrue()) }) - It("[CA-10b] Should include CRD when --crd-include-group matches", Label("tier0"), func() { + It("[MTA-869b] Should include CRD when --crd-include-group matches", Label("tier0"), func() { scenario := NewMigrationScenario( appName, namespace, @@ -182,10 +182,6 @@ var _ = Describe("CRD group filtering during export", func() { Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeTrue()) - // By("Verifying CRD exists in output _cluster directory") - // outputClusterPath := filepath.Join(paths.OutputDir, "resources", "_cluster") - // Expect(ValidateDirResources(outputClusterPath, crdPatterns)).NotTo(HaveOccurred()) - By("Creating namespace on target cluster") Expect(tgtNameSpace.Create(kubectlTgt)).NotTo(HaveOccurred()) diff --git a/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go b/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go index d7c274c2..2a44c0a7 100644 --- a/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go +++ b/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go @@ -12,7 +12,7 @@ import ( ) var _ = Describe("Cluster-level export filtering", func() { - It("[CA-7] Should not export CRB with subject from another namespace", Label("cluster-admin"), func() { + It("[MTA-870] Should not export CRB with subject from another namespace", Label("cluster-admin"), func() { appName := "simple-nginx-nopv" namespace := "simple-nginx-nopv" serviceName := "my-" + appName @@ -87,13 +87,13 @@ var _ = Describe("Cluster-level export filtering", func() { By("Verifying out-of-scope resources are not in export _cluster directory") exportClusterPath := filepath.Join(paths.ExportDir, "resources", namespace, "_cluster") - found, err := utils.AssertResourcesExist(exportClusterPath, []utils.ResourceMatch{ + found, err := utils.AssertResourcesDontExist(exportClusterPath, []utils.ResourceMatch{ {Kind: "ClusterRoleBinding", Name: unrelatedCRB.Name}, }) Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeFalse()) + Expect(found).To(BeTrue()) - By("Verifying linked ClusterRole and ClusterRoleBinding exist in export, transform, and output") + By("Verifying linked ClusterRole and ClusterRoleBinding exist after export") found, err = utils.AssertResourcesExist(exportClusterPath, []utils.ResourceMatch{ {Kind: "ClusterRoleBinding", Name: testCRB.Name}, {Kind: "ClusterRole", Name: cr.Name}, diff --git a/e2e-tests/utils/utils.go b/e2e-tests/utils/utils.go index b31a90c9..efbb3ce0 100644 --- a/e2e-tests/utils/utils.go +++ b/e2e-tests/utils/utils.go @@ -1407,7 +1407,7 @@ func fileHasPrefixAndSuffix(file, prefix, suffix string) bool { // Returns (true, nil) if all match, (false, nil) if any missing, or (false, err) on error. func AssertResourcesExist(dir string, resources []ResourceMatch) (bool, error) { existingFiles, err := ListFilesRecursivelyAsList(dir) - if err != nil || len(existingFiles) == 0 { + if err != nil { return false, err } @@ -1428,6 +1428,10 @@ func AssertResourcesExist(dir string, resources []ResourceMatch) (bool, error) { } func AssertResourcesDontExist(dir string, resources []ResourceMatch) (bool, error) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return true, nil + } + existingFiles, err := ListFilesRecursivelyAsList(dir) if err != nil { return false, err From 4bd7dd34f9fb709fd01109757cb208d985957f2e Mon Sep 17 00:00:00 2001 From: Ran Wurmbrand Date: Thu, 20 Aug 2026 13:00:15 +0300 Subject: [PATCH 7/7] rebased and removed export dir from applyOpts Signed-off-by: Ran Wurmbrand --- e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go | 2 +- e2e-tests/tests/tier0/mta_869_crd_flags_test.go | 4 ++-- e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go b/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go index d982cae5..73b6ca3a 100644 --- a/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go +++ b/e2e-tests/tests/tier0/mta_868_label_scoped_export_test.go @@ -36,7 +36,7 @@ var _ = Describe("Cluster-level export filtering", func() { exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir, LabelSelector: "app=" + appName} transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} - applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + applyOpts := ApplyOptions{TransformDir: paths.TransformDir, OutputDir: paths.OutputDir} inScopeSA := ServiceAccount{Name: "nginx-sa", Namespace: namespace, Label: "app=simple-nginx-nopv"} diff --git a/e2e-tests/tests/tier0/mta_869_crd_flags_test.go b/e2e-tests/tests/tier0/mta_869_crd_flags_test.go index ed5968a8..24c3772f 100644 --- a/e2e-tests/tests/tier0/mta_869_crd_flags_test.go +++ b/e2e-tests/tests/tier0/mta_869_crd_flags_test.go @@ -58,7 +58,7 @@ var _ = Describe("CRD group filtering during export", func() { exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir, CRDSkipGroups: []string{"crane-e2e.example.com"}} transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} - applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + applyOpts := ApplyOptions{TransformDir: paths.TransformDir, OutputDir: paths.OutputDir} DeferCleanup(func() { @@ -138,7 +138,7 @@ var _ = Describe("CRD group filtering during export", func() { exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir, CRDIncludeGroups: []string{"crane-e2e.openshift.io"}} transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} - applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + applyOpts := ApplyOptions{TransformDir: paths.TransformDir, OutputDir: paths.OutputDir} DeferCleanup(func() { diff --git a/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go b/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go index 2a44c0a7..44896a28 100644 --- a/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go +++ b/e2e-tests/tests/tier1/mta_870_unrelated_crb_test.go @@ -34,7 +34,7 @@ var _ = Describe("Cluster-level export filtering", func() { exportOpts := ExportOptions{Namespace: srcApp.Namespace, ExportDir: paths.ExportDir} transformOpts := TransformOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir} - applyOpts := ApplyOptions{ExportDir: paths.ExportDir, TransformDir: paths.TransformDir, + applyOpts := ApplyOptions{TransformDir: paths.TransformDir, OutputDir: paths.OutputDir} cr := ClusterRole{Name: "crane-cr", Verb: "get,list,watch", Resource: "pods", Label: "app=" + appName}