diff --git a/.golangci.yaml b/.golangci.yaml index 107c376..d725fc0 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -102,6 +102,7 @@ linters-settings: - github.com/go-logr/logr - k8s.io - sigs.k8s.io + - github.com/spf13/pflag dupl: threshold: 100 funlen: diff --git a/go.mod b/go.mod index e5b4476..6458179 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-logr/logr v1.4.2 github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.34.2 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 k8s.io/api v0.31.2 k8s.io/apiextensions-apiserver v0.31.0 @@ -70,7 +71,6 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/pkg/crdutil/crdutil.go b/pkg/crdutil/crdutil.go index 9fa4f9e..7455875 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" @@ -37,38 +38,31 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) -type StringList []string - -func (s *StringList) String() string { - return strings.Join(*s, ", ") -} - -func (s *StringList) Set(value string) error { - *s = append(*s, value) - return nil -} - var ( - crdsDir StringList + filenames []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(&filenames, "filename", "f", filenames, "The files that contain the configurations to apply.") + pflag.BoolVarP(&recursive, "recursive", "R", false, "Process the directory used in -f, --filename recursively.") + pflag.Parse() - if len(crdsDir) == 0 { - log.Fatalf("CRDs directory is required") + if len(filenames) == 0 { + log.Fatalf("CRDs directory or single CRDs are required") } - for _, crdDir := range crdsDir { + for _, crdDir := range filenames { 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", filenames) } } } // EnsureCRDsCmd reads each YAML file in the directory, splits it into documents, and applies each CRD to the cluster. // The parameter --crds-dir is required and should point to the directory containing the CRD manifests. +// TODO: add unit test for this command. func EnsureCRDsCmd() { ctx := context.Background() @@ -84,38 +78,59 @@ func EnsureCRDsCmd() { log.Fatalf("Failed to create API extensions client: %v", err) } - if err := walkCrdsDir(ctx, client.ApiextensionsV1().CustomResourceDefinitions()); err != nil { - log.Fatalf("Failed to apply CRDs: %v", err) + dirsToApply, err := walkCRDs(recursive, filenames) + if err != nil { + log.Fatalf("Failed to walk through CRDs: %v", err) + } + + for _, dir := range dirsToApply { + log.Printf("Apply CRDs from file: %s", dir) + if err := applyCRDs(ctx, client.ApiextensionsV1().CustomResourceDefinitions(), dir); 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. +// TODO: add unit test for this function. +func walkCRDs(recursive bool, crdDirs []string) ([]string, error) { + var dirs []string + for _, crdDir := range crdDirs { + // We need the parent directory to check if we are in the top-level directory. + // This is necessary for the recursive logic. + // 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 this is a directory, skip it. + // filepath.Walk() is also called for directories, but we only want to apply CRDs from files. + if info.IsDir() { return nil } - - log.Printf("Apply CRDs from file: %s", path) - if err := applyCRDsFromFile(ctx, crdClient, path); err != nil { - return fmt.Errorf("apply CRD %s: %w", path, err) + 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. + // filepath.Dir() does not add a trailing slash, thus we need to trim it in the crdDir. + if !recursive && parentDir.IsDir() && filepath.Dir(path) != strings.TrimRight(crdDir, "/") { + return nil + } + + dirs = append(dirs, path) return nil }) if err != nil { - return fmt.Errorf("walk the path %s: %w", crdsDir, err) + return []string{}, fmt.Errorf("walk the path %s: %w", crdDirs, err) } } - return nil + return dirs, 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) @@ -170,14 +185,19 @@ func applyCRD( if err != nil { return fmt.Errorf("create CRD %s: %w", crd.Name, err) } - } else { - log.Printf("Update CRD %s", crd.Name) - // Set resource version to update an existing CRD. - crd.SetResourceVersion(curCRD.GetResourceVersion()) - _, err = crdClient.Update(ctx, crd, metav1.UpdateOptions{}) - if err != nil { - return fmt.Errorf("update CRD %s: %w", crd.Name, err) - } + return nil } + 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()) + _, err = crdClient.Update(ctx, crd, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("update CRD %s: %w", crd.Name, err) + } + return nil } 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"} {