diff --git a/pkg/crdutil/crdutil.go b/pkg/crdutil/crdutil.go index 9fa4f9e..ccedb8f 100644 --- a/pkg/crdutil/crdutil.go +++ b/pkg/crdutil/crdutil.go @@ -26,6 +26,7 @@ import ( "path/filepath" "strings" + "github.com/spf13/pflag" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" v1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" @@ -49,20 +50,23 @@ func (s *StringList) Set(value string) error { } var ( - crdsDir StringList + files []string + recursive bool ) func initFlags() { - flag.Var(&crdsDir, "crds-dir", "Path to the directory containing the CRD manifests") - flag.Parse() + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.StringSliceVarP(&files, "filename", "f", files, "The files that contain the configurations to apply.") + pflag.BoolVarP(&recursive, "recursive", "R", false, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.") + pflag.Parse() - if len(crdsDir) == 0 { - log.Fatalf("CRDs directory is required") + if len(files) == 0 { + log.Fatalf("CRDs directory or single CRDs are required") } - for _, crdDir := range crdsDir { + for _, crdDir := range files { if _, err := os.Stat(crdDir); os.IsNotExist(err) { - log.Fatalf("CRDs directory %s does not exist", crdsDir) + log.Fatalf("CRDs directory %s does not exist", files) } } } @@ -84,38 +88,47 @@ func EnsureCRDsCmd() { log.Fatalf("Failed to create API extensions client: %v", err) } - if err := walkCrdsDir(ctx, client.ApiextensionsV1().CustomResourceDefinitions()); err != nil { + if err := walkCRDs(ctx, client.ApiextensionsV1().CustomResourceDefinitions()); err != nil { log.Fatalf("Failed to apply CRDs: %v", err) } } -// walkCrdsDir walks the CRDs directory and applies each YAML file. -func walkCrdsDir(ctx context.Context, crdClient v1.CustomResourceDefinitionInterface) error { - for _, crdDir := range crdsDir { +// walkCRDs walks the CRDs directory and applies each YAML file. +func walkCRDs(ctx context.Context, crdClient v1.CustomResourceDefinitionInterface) error { + for _, crdDir := range files { + // We can skip the errors as it has been checked in initFlags. + parentDir, _ := os.Stat(crdDir) // Walk the directory recursively and apply each YAML file. err := filepath.Walk(crdDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - if info.IsDir() || filepath.Ext(path) != ".yaml" { + if info.IsDir() { + return nil + } + if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" { + return nil + } + // If not recursive we want to only apply the CRDs in the top-level directory. + if !recursive && parentDir.IsDir() && filepath.Dir(path) != strings.TrimRight(crdDir, "/") { return nil } log.Printf("Apply CRDs from file: %s", path) - if err := applyCRDsFromFile(ctx, crdClient, path); err != nil { + if err := applyCRDs(ctx, crdClient, path); err != nil { return fmt.Errorf("apply CRD %s: %w", path, err) } return nil }) if err != nil { - return fmt.Errorf("walk the path %s: %w", crdsDir, err) + return fmt.Errorf("walk the path %s: %w", files, err) } } return nil } -// applyCRDsFromFile reads a YAML file, splits it into documents, and applies each CRD to the cluster. -func applyCRDsFromFile(ctx context.Context, crdClient v1.CustomResourceDefinitionInterface, filePath string) error { +// applyCRDs reads a YAML file, splits it into documents, and applies each CRD to the cluster. +func applyCRDs(ctx context.Context, crdClient v1.CustomResourceDefinitionInterface, filePath string) error { file, err := os.Open(filePath) if err != nil { return fmt.Errorf("open file %q: %w", filePath, err) @@ -171,6 +184,9 @@ func applyCRD( return fmt.Errorf("create CRD %s: %w", crd.Name, err) } } else { + if err != nil { + return fmt.Errorf("get CRD %s: %w", crd.Name, err) + } log.Printf("Update CRD %s", crd.Name) // Set resource version to update an existing CRD. crd.SetResourceVersion(curCRD.GetResourceVersion()) diff --git a/pkg/crdutil/crdutil_test.go b/pkg/crdutil/crdutil_test.go index 9691570..3de6671 100644 --- a/pkg/crdutil/crdutil_test.go +++ b/pkg/crdutil/crdutil_test.go @@ -38,13 +38,13 @@ var _ = Describe("CRD Application", func() { Expect(testCRDClient.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})).NotTo(HaveOccurred()) }) - Describe("applyCRDsFromFile", func() { + Describe("applyCRDs", func() { It("should apply CRDs multiple times from a valid YAML file", func() { By("applying CRDs") - Expect(applyCRDsFromFile(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) - Expect(applyCRDsFromFile(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) - Expect(applyCRDsFromFile(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) - Expect(applyCRDsFromFile(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) + Expect(applyCRDs(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) + Expect(applyCRDs(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) + Expect(applyCRDs(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) + Expect(applyCRDs(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) By("verifying CRDs are applied") crds, err := testCRDClient.List(ctx, metav1.ListOptions{}) @@ -54,7 +54,7 @@ var _ = Describe("CRD Application", func() { It("should update CRDs", func() { By("applying CRDs") - Expect(applyCRDsFromFile(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) + Expect(applyCRDs(ctx, testCRDClient, "test-files/test-crds.yaml")).To(Succeed()) By("verifying CRDs do not have spec.foobar") for _, crdName := range []string{"bars.example.com", "foos.example.com"} { @@ -66,7 +66,7 @@ var _ = Describe("CRD Application", func() { } By("updating CRDs") - Expect(applyCRDsFromFile(ctx, testCRDClient, "test-files/updated-test-crds.yaml")).To(Succeed()) + Expect(applyCRDs(ctx, testCRDClient, "test-files/updated-test-crds.yaml")).To(Succeed()) By("verifying CRDs are updated") for _, crdName := range []string{"bars.example.com", "foos.example.com"} {