From 9b1135cb7bd983bb4e3b03a4c0b2a9c8b09d616f Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Thu, 5 Sep 2024 08:15:05 +0100 Subject: [PATCH] Add ability to include/exclude namespaces. --- Dockerfile | 2 +- Makefile | 8 ++-- README.md | 11 +++++ api/raczylo.com/v1/clusterimage_types.go | 15 ++++--- .../v1/clusterimageexport_types.go | 4 +- api/raczylo.com/v1/zz_generated.deepcopy.go | 10 +++++ chart/Chart.yaml | 4 +- chart/templates/clusterimage-crd.yaml | 2 + chart/templates/clusterimageexport-crd.yaml | 8 ++++ chart/values.yaml | 2 +- .../raczylo.com_clusterimageexports.yaml | 8 ++++ .../crd/bases/raczylo.com_clusterimages.yaml | 2 + .../clusterimageexport_controller.go | 23 +++++++--- internal/shared/definitions.go | 45 ++++++++++++++++--- internal/shared/k8s.go | 17 +++---- 15 files changed, 124 insertions(+), 37 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2465f77..935cdf0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ COPY internal/ internal/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags "-X shared.BACKUP_JOB_IMAGE=ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:v${IMAGE_VERSION_TAG}" -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags "-X github.com/lukaszraczylo/kubernetes-images-sync-operator/internal/shared.BACKUP_JOB_IMAGE=ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:${IMAGE_VERSION_TAG}" -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index ea39c71..b300152 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ ifeq ($(CURRENT_VERSION),) $(error Failed to extract version number) endif +IMAGE_VERSION_TAG ?= $(CURRENT_VERSION) + # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin @@ -87,11 +89,11 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes .PHONY: build build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/main.go + go build -ldflags "-X github.com/lukaszraczylo/kubernetes-images-sync-operator/internal/shared.BACKUP_JOB_IMAGE=ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:$(IMAGE_VERSION_TAG)" -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go + go run -ldflags "-X github.com/lukaszraczylo/kubernetes-images-sync-operator/internal/shared.BACKUP_JOB_IMAGE=ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:$(IMAGE_VERSION_TAG)" ./cmd/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. @@ -231,7 +233,7 @@ release-chart: cr package --config ../../chart-releaser.yaml; cd ../helm-charts/; git add -A charts/packages; git fix; git push; cd ../helm-charts/charts/${CHART_NAME}; cr upload --config ../../chart-releaser.yaml --skip-existing; - cd ../helm-charts/charts/${CHART_NAME}; rm -fr .cr-index; mkdir .cr-index; cr index --config ../../chart-releaser.yaml; cp .cr-index/index.yaml ../../index.yaml; || true + cd ../helm-charts/charts/${CHART_NAME}; rm -fr .cr-index; mkdir .cr-index; cr index --config ../../chart-releaser.yaml; cp .cr-index/index.yaml ../../index.yaml; git fix; git push # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist diff --git a/README.md b/README.md index 3031a0a..e787f63 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,20 @@ spec: # Excludes will remove all images with listed wording from the backup list # excludes: # - nginx + # Includes will add ONLY images with listed wording to the backup list includes: - busybox + + # Works only with images within specified namespaces + # namespaces: + # - default + # - longhorn + + # Works with all images EXCEPT of the ones within namespaces specified + # excludedNamespaces: + # - my-awesome-namespace + basePath: /images # base path in the target directory storage: target: S3 # file backup is not ready yet diff --git a/api/raczylo.com/v1/clusterimage_types.go b/api/raczylo.com/v1/clusterimage_types.go index c6ac46e..1d1d53e 100644 --- a/api/raczylo.com/v1/clusterimage_types.go +++ b/api/raczylo.com/v1/clusterimage_types.go @@ -28,13 +28,14 @@ import ( // +kubebuilder:printcolumn:name="Path",type="string",JSONPath=".spec.exportPath" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" type ClusterImageSpec struct { - Image string `json:"image,omitempty"` - Tag string `json:"tag,omitempty"` - Sha string `json:"sha,omitempty"` - FullName string `json:"fullName,omitempty"` // Because I'm lazy and it's easier to pull that way - Storage string `json:"storage,omitempty"` - ExportName string `json:"exportName"` - ExportPath string `json:"exportPath,omitempty"` + Image string `json:"image,omitempty"` + Tag string `json:"tag,omitempty"` + Sha string `json:"sha,omitempty"` + FullName string `json:"fullName,omitempty"` // Because I'm lazy and it's easier to pull that way + Storage string `json:"storage,omitempty"` + ExportName string `json:"exportName"` + ExportPath string `json:"exportPath,omitempty"` + ImageNamespace string `json:"imageNamespace,omitempty"` } // ClusterImageStatus defines the observed state of ClusterImage diff --git a/api/raczylo.com/v1/clusterimageexport_types.go b/api/raczylo.com/v1/clusterimageexport_types.go index c323a7b..86f9989 100644 --- a/api/raczylo.com/v1/clusterimageexport_types.go +++ b/api/raczylo.com/v1/clusterimageexport_types.go @@ -54,7 +54,9 @@ type ClusterImageExportSpec struct { // Exclude images which contain these strings Excludes []string `json:"excludes,omitempty"` // Include only images which contain these strings - Includes []string `json:"includes,omitempty"` + Includes []string `json:"includes,omitempty"` + Namespaces []string `json:"namespaces,omitempty"` + ExcludedNamespaces []string `json:"excludedNamespaces,omitempty"` // Base path for the export - both file and S3 // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=255 diff --git a/api/raczylo.com/v1/zz_generated.deepcopy.go b/api/raczylo.com/v1/zz_generated.deepcopy.go index 0b2521f..32b0772 100644 --- a/api/raczylo.com/v1/zz_generated.deepcopy.go +++ b/api/raczylo.com/v1/zz_generated.deepcopy.go @@ -124,6 +124,16 @@ func (in *ClusterImageExportSpec) DeepCopyInto(out *ClusterImageExportSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludedNamespaces != nil { + in, out := &in.ExcludedNamespaces, &out.ExcludedNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } out.Storage = in.Storage } diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 3568d5c..92162f7 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -10,9 +10,9 @@ description: | type: application -version: 0.1.5 +version: 0.1.7 -appVersion: "0.1.5" +appVersion: "0.1.7" home: https://github.com/lukaszraczylo/kubernetes-images-sync-operator diff --git a/chart/templates/clusterimage-crd.yaml b/chart/templates/clusterimage-crd.yaml index c97266d..f29dab0 100644 --- a/chart/templates/clusterimage-crd.yaml +++ b/chart/templates/clusterimage-crd.yaml @@ -76,6 +76,8 @@ spec: type: string image: type: string + imageNamespace: + type: string sha: type: string storage: diff --git a/chart/templates/clusterimageexport-crd.yaml b/chart/templates/clusterimageexport-crd.yaml index 94285ee..49b3f0e 100644 --- a/chart/templates/clusterimageexport-crd.yaml +++ b/chart/templates/clusterimageexport-crd.yaml @@ -61,6 +61,10 @@ spec: createdAt: format: date-time type: string + excludedNamespaces: + items: + type: string + type: array excludes: description: Exclude images which contain these strings items: @@ -75,6 +79,10 @@ spec: type: integer name: type: string + namespaces: + items: + type: string + type: array storage: description: ClusterImageStorageSpec defines the desired state of ClusterImageStorage properties: diff --git a/chart/values.yaml b/chart/values.yaml index 6f99b67..7d77cad 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -11,7 +11,7 @@ cmRaczyloCom: - ALL image: repository: ghcr.io/lukaszraczylo/kubernetes-images-sync-operator - tag: 0.1.5 + tag: 0.1.7 resources: limits: cpu: 500m diff --git a/config/crd/bases/raczylo.com_clusterimageexports.yaml b/config/crd/bases/raczylo.com_clusterimageexports.yaml index 2529e32..4bec849 100644 --- a/config/crd/bases/raczylo.com_clusterimageexports.yaml +++ b/config/crd/bases/raczylo.com_clusterimageexports.yaml @@ -61,6 +61,10 @@ spec: createdAt: format: date-time type: string + excludedNamespaces: + items: + type: string + type: array excludes: description: Exclude images which contain these strings items: @@ -75,6 +79,10 @@ spec: type: integer name: type: string + namespaces: + items: + type: string + type: array storage: description: ClusterImageStorageSpec defines the desired state of ClusterImageStorage diff --git a/config/crd/bases/raczylo.com_clusterimages.yaml b/config/crd/bases/raczylo.com_clusterimages.yaml index 8622579..fc974ef 100644 --- a/config/crd/bases/raczylo.com_clusterimages.yaml +++ b/config/crd/bases/raczylo.com_clusterimages.yaml @@ -75,6 +75,8 @@ spec: type: string image: type: string + imageNamespace: + type: string sha: type: string storage: diff --git a/internal/controller/raczylo.com/clusterimageexport_controller.go b/internal/controller/raczylo.com/clusterimageexport_controller.go index b26f453..bdaa16f 100644 --- a/internal/controller/raczylo.com/clusterimageexport_controller.go +++ b/internal/controller/raczylo.com/clusterimageexport_controller.go @@ -130,13 +130,14 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R }, }, Spec: raczylocomv1.ClusterImageSpec{ - Image: image.Image, - Tag: image.Tag, - Sha: image.Sha, - FullName: image.FullName, - Storage: clusterImageExport.Spec.Storage.StorageTarget, - ExportName: clusterImageExport.Name, - ExportPath: clusterImageExport.Spec.BasePath, + Image: image.Image, + Tag: image.Tag, + Sha: image.Sha, + FullName: image.FullName, + ImageNamespace: image.ImageNamespace, + Storage: clusterImageExport.Spec.Storage.StorageTarget, + ExportName: clusterImageExport.Name, + ExportPath: clusterImageExport.Spec.BasePath, }, } @@ -211,6 +212,14 @@ func (r *ClusterImageExportReconciler) listImagesInCluster(ctx context.Context, containersList = shared.RemoveExcludedImages(containersList, clusterImageExport.Spec.Excludes) } + if len(clusterImageExport.Spec.Namespaces) > 0 { + containersList = shared.FilterOnlyFromNamespaces(containersList, clusterImageExport.Spec.Namespaces) + } + + if len(clusterImageExport.Spec.ExcludedNamespaces) > 0 { + containersList = shared.FilterOutWholeNamespaces(containersList, clusterImageExport.Spec.ExcludedNamespaces) + } + containersList = shared.RemoveDuplicates(containersList) l.Info("List of containers in the cluster", "containers", containersList) diff --git a/internal/shared/definitions.go b/internal/shared/definitions.go index c424881..1d1dcdd 100644 --- a/internal/shared/definitions.go +++ b/internal/shared/definitions.go @@ -5,10 +5,9 @@ import ( "strings" ) -const ( - // JOB IMAGES - BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:1.0.2" +var BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:1.0.2" +const ( // AVAILABLE STATUSES STATUS_PENDING = "PENDING" STATUS_STARTING = "STARTING" @@ -24,10 +23,11 @@ const ( ) type Container struct { - Image string `json:"image"` - Tag string `json:"tag"` - Sha string `json:"sha"` - FullName string `json:"fullName"` + Image string `json:"image"` + Tag string `json:"tag"` + Sha string `json:"sha"` + FullName string `json:"fullName"` + ImageNamespace string `json:"imageNamespace"` } type ContainersList struct { @@ -96,3 +96,34 @@ func NormalizeImageName(name string) string { // Trim leading and trailing hyphens return strings.Trim(normalized, "-") } + +// filterOnlyFromNamespaces filters out containers from namespaces that are not in the list +func FilterOnlyFromNamespaces(containers ContainersList, namespaces []string) ContainersList { + result := ContainersList{} + for _, container := range containers.Containers { + for _, namespace := range namespaces { + if container.ImageNamespace == namespace { + result.Containers = append(result.Containers, container) + } + } + } + return result +} + +// filterOutWholeNamespaces filters out containers from namespaces that are in the list +func FilterOutWholeNamespaces(containers ContainersList, namespaces []string) ContainersList { + result := ContainersList{} + for _, container := range containers.Containers { + excluded := false + for _, namespace := range namespaces { + if container.ImageNamespace == namespace { + excluded = true + break + } + } + if !excluded { + result.Containers = append(result.Containers, container) + } + } + return result +} diff --git a/internal/shared/k8s.go b/internal/shared/k8s.go index 0ed5cbf..f2aa21e 100644 --- a/internal/shared/k8s.go +++ b/internal/shared/k8s.go @@ -64,7 +64,7 @@ func processContainerName(containerName string) (Container, error) { return cnt, nil } -func processContainers[T K8sResource](resource T, containersList *ContainersList) error { +func processContainers[T K8sResource](resource T, namespace string, containersList *ContainersList) error { podSpec := resource.GetPodSpec() if podSpec == nil { return fmt.Errorf("nil PodSpec") @@ -72,13 +72,13 @@ func processContainers[T K8sResource](resource T, containersList *ContainersList allContainers := append(podSpec.Containers, podSpec.InitContainers...) for _, container := range allContainers { - if err := processContainer(container.Image, containersList); err != nil { + if err := processContainer(container.Image, namespace, containersList); err != nil { return err } } for _, container := range podSpec.EphemeralContainers { - if err := processContainer(container.EphemeralContainerCommon.Image, containersList); err != nil { + if err := processContainer(container.EphemeralContainerCommon.Image, namespace, containersList); err != nil { return err } } @@ -87,11 +87,12 @@ func processContainers[T K8sResource](resource T, containersList *ContainersList } // processContainer handles the processing of a single container image -func processContainer(image string, containersList *ContainersList) error { +func processContainer(image string, containerNamespace string, containersList *ContainersList) error { cnt, err := processContainerName(image) if err != nil { return fmt.Errorf("failed to process container name: %s - %w", image, err) } + cnt.ImageNamespace = containerNamespace containersList.Containers = append(containersList.Containers, cnt) return nil } @@ -105,25 +106,25 @@ func ListAndProcessResources[T K8sResource, L client.ObjectList](ctx context.Con switch typedList := any(list).(type) { case *appsv1.DeploymentList: for i := range typedList.Items { - if err := processContainers((*DeploymentWrapper)(&typedList.Items[i]), containersList); err != nil { + if err := processContainers((*DeploymentWrapper)(&typedList.Items[i]), typedList.Items[i].Namespace, containersList); err != nil { return err } } case *batchv1.JobList: for i := range typedList.Items { - if err := processContainers((*JobWrapper)(&typedList.Items[i]), containersList); err != nil { + if err := processContainers((*JobWrapper)(&typedList.Items[i]), typedList.Items[i].Namespace, containersList); err != nil { return err } } case *appsv1.DaemonSetList: for i := range typedList.Items { - if err := processContainers((*DaemonSetWrapper)(&typedList.Items[i]), containersList); err != nil { + if err := processContainers((*DaemonSetWrapper)(&typedList.Items[i]), typedList.Items[i].Namespace, containersList); err != nil { return err } } case *batchv1.CronJobList: for i := range typedList.Items { - if err := processContainers((*CronJobWrapper)(&typedList.Items[i]), containersList); err != nil { + if err := processContainers((*CronJobWrapper)(&typedList.Items[i]), typedList.Items[i].Namespace, containersList); err != nil { return err } }