Skip to content

crdutil: Add feature to also apply single files #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ linters-settings:
- github.com/go-logr/logr
- k8s.io
- sigs.k8s.io
- github.com/spf13/pflag
dupl:
threshold: 100
funlen:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/go-logr/logr v1.4.2
github.com/onsi/ginkgo/v2 v2.22.2
github.com/onsi/gomega v1.36.2
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.10.0
k8s.io/api v0.32.0
k8s.io/apiextensions-apiserver v0.32.0
Expand Down Expand Up @@ -69,7 +70,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
Expand Down
114 changes: 72 additions & 42 deletions pkg/crdutil/crdutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -37,38 +38,29 @@ 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
crdsDir []string
)

func initFlags() {
flag.Var(&crdsDir, "crds-dir", "Path to the directory containing the CRD manifests")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we concerned about this as a breaking change since the command line arguments are changed?

Copy link
Member Author

@tobiasgiese tobiasgiese Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also thinking about this but this pkg is pretty new and we are only using it for the network-operator currently. Do you think we should deprecate it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the network-operator is not released with this change, better to just update as it will be first use of this package.

Copy link
Member Author

@tobiasgiese tobiasgiese Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The network-operator 24.10.0 has been released 2 days ago with the change in it, see https://github.com/Mellanox/network-operator/blob/v24.10.0/deployment/network-operator/templates/upgrade-crd-hook.yaml#L84.

But IMO it is totally fine to change it, because it is a Helm pre-install hook. If it fails, it fails the installation/upgrade and the user has to use the latest Helm chart. There is no dependency to the runtime which can cause the operator to fail.

WDYT @e0ne @rollandf

tl;dr: this PR changes the flag --crds-dir to --filename/-f

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a concern, we could support both -- marking crds-dir as deprecated -- for a release. Happy with the change since this is effectively an internal API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also revert it to --crds-dir. Both crds-dir and filename does not contain both files and dirs, so it is not 100% explicit anyway :)
Request came through #58 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shivamerla WDYT? Should we remove the flag rename and recursive flag (see #58 (comment)) out of this PR and change it in a follow-up? I'd say this will reduce the size of this PR.

flag.Parse()
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.StringSliceVarP(&crdsDir, "crds-dir", "f", crdsDir, "The files or directories that contain the CRDs to apply.")
pflag.Parse()

if len(crdsDir) == 0 {
log.Fatalf("CRDs directory is required")
log.Fatalf("CRDs directory or single CRDs are required")
}

for _, crdDir := range crdsDir {
if _, err := os.Stat(crdDir); os.IsNotExist(err) {
log.Fatalf("CRDs directory %s does not exist", crdsDir)
for _, path := range crdsDir {
if _, err := os.Stat(path); os.IsNotExist(err) {
log.Fatalf("path does not exist: %s", path)
}
}
}

// 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()

Expand All @@ -84,38 +76,71 @@ 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)
pathsToApply, err := collectYamlPaths(crdsDir)
if err != nil {
log.Fatalf("Failed to walk through CRDs: %v", err)
}

for _, path := range pathsToApply {
log.Printf("Apply CRDs from file: %s", path)
if err := applyCRDs(ctx, client.ApiextensionsV1().CustomResourceDefinitions(), path); 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 {
// collectYamlPaths processes a list of paths and returns all YAML files.
func collectYamlPaths(crdDirs []string) ([]string, error) {
paths := map[string]struct{}{}
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, err := os.Stat(crdDir)
if err != nil {
return []string{}, fmt.Errorf("stat the path %s: %w", crdDir, err)
}
// Walk the directory recursively and apply each YAML file.
err := filepath.Walk(crdDir, func(path string, info os.FileInfo, err error) error {
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 we apply a dir we only want to apply the CRDs in the directory (i.e., not recursively).
// filepath.Dir() does not add a trailing slash, thus we need to trim it in the crdDir.
if parentDir.IsDir() && filepath.Dir(path) != strings.TrimRight(crdDir, "/") {
return nil
}

paths[path] = struct{}{}
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 mapToSlice(paths), 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 {
// mapToSlice converts a map to a slice.
// The map is used to deduplicate the paths.
func mapToSlice(m map[string]struct{}) []string {
s := []string{}
for k := range m {
s = append(s, k)
}
return s
}

// 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)
Expand Down Expand Up @@ -168,16 +193,21 @@ func applyCRD(
log.Printf("Create CRD %s", crd.Name)
_, err = crdClient.Create(ctx, crd, metav1.CreateOptions{})
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 fmt.Errorf("create CRD: %w", 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
}
49 changes: 42 additions & 7 deletions pkg/crdutil/crdutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,48 @@ var _ = Describe("CRD Application", func() {
Expect(testCRDClient.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})).NotTo(HaveOccurred())
})

Describe("applyCRDsFromFile", func() {
Describe("collectYamlPaths", func() {
It("should collect all YAML files in a directory", func() {
By("collecting YAML paths")
paths, err := collectYamlPaths([]string{"test-files"})
Expect(err).NotTo(HaveOccurred())
Expect(paths).To(ConsistOf(
"test-files/test-crds.yaml",
"test-files/updated-test-crds.yaml",
))
})

It("should collect a single YAML file", func() {
By("collecting YAML paths")
paths, err := collectYamlPaths([]string{"test-files/test-crds.yaml"})
Expect(err).NotTo(HaveOccurred())
Expect(paths).To(ConsistOf("test-files/test-crds.yaml"))
})

It("should deduplicate YAML file", func() {
By("collecting YAML paths")
paths, err := collectYamlPaths([]string{"test-files/test-crds.yaml", "test-files"})
Expect(err).NotTo(HaveOccurred())
Expect(paths).To(ConsistOf(
"test-files/test-crds.yaml",
"test-files/updated-test-crds.yaml",
))
})

It("should fail to collect non-existent YAML files", func() {
By("collecting YAML paths")
_, err := collectYamlPaths([]string{"test-files/non-existent.yaml"})
Expect(err).To(HaveOccurred())
})
})

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{})
Expand All @@ -54,7 +89,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"} {
Expand All @@ -66,7 +101,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"} {
Expand Down
22 changes: 22 additions & 0 deletions pkg/crdutil/test-files/nested/do-not-apply.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: foos.example.com
spec:
group: example.com
names:
kind: Foo
listKind: FooList
singular: foo
plural: foos
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
Loading