From 180dfd1687785aaa526659bf47be4e52beaa7091 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Wed, 4 Sep 2024 20:46:36 +0100 Subject: [PATCH] Initial commit for the operator --- .dockerignore | 3 + .gitignore | 29 + .golangci.yml | 47 + Dockerfile | 33 + Makefile | 221 +++++ PROJECT | 28 + README.md | 114 +++ api/raczylo.com/v1/clusterimage_types.go | 79 ++ .../v1/clusterimageexport_types.go | 100 ++ api/raczylo.com/v1/groupversion_info.go | 36 + api/raczylo.com/v1/zz_generated.deepcopy.go | 246 +++++ chart-defaults/Chart.yaml | 21 + chart/.helmignore | 23 + chart/Chart.yaml | 21 + chart/templates/_helpers.tpl | 62 ++ chart/templates/clusterimage-crd.yaml | 108 +++ chart/templates/clusterimageexport-crd.yaml | 141 +++ .../cm-raczylo-com-metrics-service.yaml | 14 + chart/templates/deployment.yaml | 50 + .../templates/metrics-auth-raczylo-rbac.yaml | 19 + .../metrics-auth-raczylobinding-rbac.yaml | 14 + chart/templates/metrics-raczylo-rbac.yaml | 11 + chart/templates/mr-raczylo-com-rbac.yaml | 63 ++ .../templates/mr-raczylo-combinding-rbac.yaml | 14 + chart/templates/raczylo-com-leader-rbac.yaml | 38 + .../raczylo-com-leaderbinding-rbac.yaml | 14 + .../raczylo.com-clusterimage-editor-rbac.yaml | 25 + .../raczylo.com-clusterimage-viewer-rbac.yaml | 21 + ...lo.com-clusterimageexport-editor-rbac.yaml | 25 + ...lo.com-clusterimageexport-viewer-rbac.yaml | 21 + chart/templates/serviceaccount.yaml | 8 + chart/values.yaml | 32 + cmd/main.go | 183 ++++ .../raczylo.com_clusterimageexports.yaml | 136 +++ .../crd/bases/raczylo.com_clusterimages.yaml | 101 ++ config/crd/kustomization.yaml | 24 + config/crd/kustomizeconfig.yaml | 19 + config/default/kustomization.yaml | 151 +++ config/default/manager_metrics_patch.yaml | 4 + config/default/metrics_service.yaml | 17 + config/manager/kustomization.yaml | 2 + config/manager/manager.yaml | 95 ++ .../network-policy/allow-metrics-traffic.yaml | 26 + config/network-policy/kustomization.yaml | 2 + config/prometheus/kustomization.yaml | 2 + config/prometheus/monitor.yaml | 30 + config/rbac/kustomization.yaml | 29 + config/rbac/leader_election_role.yaml | 40 + config/rbac/leader_election_role_binding.yaml | 15 + config/rbac/metrics_auth_role.yaml | 17 + config/rbac/metrics_auth_role_binding.yaml | 12 + config/rbac/metrics_reader_role.yaml | 9 + .../raczylo.com_clusterimage_editor_role.yaml | 27 + .../raczylo.com_clusterimage_viewer_role.yaml | 23 + ...lo.com_clusterimageexport_editor_role.yaml | 27 + ...lo.com_clusterimageexport_viewer_role.yaml | 23 + config/rbac/role.yaml | 62 ++ config/rbac/role_binding.yaml | 15 + config/rbac/service_account.yaml | 8 + docker-image-management/Dockerfile | 36 + docker-image-management/cleanup.py | 118 +++ docker-image-management/containers.conf | 899 ++++++++++++++++++ docker-image-management/export.py | 119 +++ docker-image-management/registries.conf | 1 + docker-image-management/requirements.txt | 3 + docker-image-management/storage.conf | 5 + go.mod | 99 ++ go.sum | 253 +++++ hack/boilerplate.go.txt | 15 + .../raczylo.com/clusterimage_controller.go | 413 ++++++++ .../clusterimage_controller_test.go | 84 ++ .../clusterimageexport_controller.go | 320 +++++++ .../clusterimageexport_controller_test.go | 84 ++ internal/controller/raczylo.com/suite_test.go | 96 ++ semver.yaml | 5 + shared/definitions.go | 98 ++ shared/jobs.go | 82 ++ shared/k8s.go | 142 +++ test/e2e/e2e_suite_test.go | 32 + test/e2e/e2e_test.go | 122 +++ test/utils/utils.go | 140 +++ update-version.sh | 8 + 82 files changed, 5954 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 PROJECT create mode 100644 README.md create mode 100644 api/raczylo.com/v1/clusterimage_types.go create mode 100644 api/raczylo.com/v1/clusterimageexport_types.go create mode 100644 api/raczylo.com/v1/groupversion_info.go create mode 100644 api/raczylo.com/v1/zz_generated.deepcopy.go create mode 100644 chart-defaults/Chart.yaml create mode 100644 chart/.helmignore create mode 100644 chart/Chart.yaml create mode 100644 chart/templates/_helpers.tpl create mode 100644 chart/templates/clusterimage-crd.yaml create mode 100644 chart/templates/clusterimageexport-crd.yaml create mode 100644 chart/templates/cm-raczylo-com-metrics-service.yaml create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/metrics-auth-raczylo-rbac.yaml create mode 100644 chart/templates/metrics-auth-raczylobinding-rbac.yaml create mode 100644 chart/templates/metrics-raczylo-rbac.yaml create mode 100644 chart/templates/mr-raczylo-com-rbac.yaml create mode 100644 chart/templates/mr-raczylo-combinding-rbac.yaml create mode 100644 chart/templates/raczylo-com-leader-rbac.yaml create mode 100644 chart/templates/raczylo-com-leaderbinding-rbac.yaml create mode 100644 chart/templates/raczylo.com-clusterimage-editor-rbac.yaml create mode 100644 chart/templates/raczylo.com-clusterimage-viewer-rbac.yaml create mode 100644 chart/templates/raczylo.com-clusterimageexport-editor-rbac.yaml create mode 100644 chart/templates/raczylo.com-clusterimageexport-viewer-rbac.yaml create mode 100644 chart/templates/serviceaccount.yaml create mode 100644 chart/values.yaml create mode 100644 cmd/main.go create mode 100644 config/crd/bases/raczylo.com_clusterimageexports.yaml create mode 100644 config/crd/bases/raczylo.com_clusterimages.yaml create mode 100644 config/crd/kustomization.yaml create mode 100644 config/crd/kustomizeconfig.yaml create mode 100644 config/default/kustomization.yaml create mode 100644 config/default/manager_metrics_patch.yaml create mode 100644 config/default/metrics_service.yaml create mode 100644 config/manager/kustomization.yaml create mode 100644 config/manager/manager.yaml create mode 100644 config/network-policy/allow-metrics-traffic.yaml create mode 100644 config/network-policy/kustomization.yaml create mode 100644 config/prometheus/kustomization.yaml create mode 100644 config/prometheus/monitor.yaml create mode 100644 config/rbac/kustomization.yaml create mode 100644 config/rbac/leader_election_role.yaml create mode 100644 config/rbac/leader_election_role_binding.yaml create mode 100644 config/rbac/metrics_auth_role.yaml create mode 100644 config/rbac/metrics_auth_role_binding.yaml create mode 100644 config/rbac/metrics_reader_role.yaml create mode 100644 config/rbac/raczylo.com_clusterimage_editor_role.yaml create mode 100644 config/rbac/raczylo.com_clusterimage_viewer_role.yaml create mode 100644 config/rbac/raczylo.com_clusterimageexport_editor_role.yaml create mode 100644 config/rbac/raczylo.com_clusterimageexport_viewer_role.yaml create mode 100644 config/rbac/role.yaml create mode 100644 config/rbac/role_binding.yaml create mode 100644 config/rbac/service_account.yaml create mode 100644 docker-image-management/Dockerfile create mode 100755 docker-image-management/cleanup.py create mode 100644 docker-image-management/containers.conf create mode 100755 docker-image-management/export.py create mode 100644 docker-image-management/registries.conf create mode 100644 docker-image-management/requirements.txt create mode 100644 docker-image-management/storage.conf create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100644 internal/controller/raczylo.com/clusterimage_controller.go create mode 100644 internal/controller/raczylo.com/clusterimage_controller_test.go create mode 100644 internal/controller/raczylo.com/clusterimageexport_controller.go create mode 100644 internal/controller/raczylo.com/clusterimageexport_controller_test.go create mode 100644 internal/controller/raczylo.com/suite_test.go create mode 100644 semver.yaml create mode 100644 shared/definitions.go create mode 100644 shared/jobs.go create mode 100644 shared/k8s.go create mode 100644 test/e2e/e2e_suite_test.go create mode 100644 test/e2e/e2e_test.go create mode 100644 test/utils/utils.go create mode 100755 update-version.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a3aab7a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a233e9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ +**/.DS_Store +config/samples \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..aac8a13 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,47 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - exportloopref + - ginkgolinter + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + revive: + rules: + - name: comment-spacings diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a48973e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Build the manager binary +FROM golang:1.22 AS builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/controller/ internal/controller/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# 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 -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 +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1737457 --- /dev/null +++ b/Makefile @@ -0,0 +1,221 @@ +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/lukaszraczylo/kubernetes-images-sync-operator +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.31.0 + +CURRENT_VERSION = $(shell semver-gen generate -l | awk '/^SEMVER/ {print $$NF}') +ifeq ($(CURRENT_VERSION),) +$(error Failed to extract version number) +endif + +# 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 +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=mr-raczylo-com crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + +# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. +.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. +test-e2e: + go test ./test/e2e/ -v -ginkgo.v + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./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. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG}:${CURRENT_VERSION} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG}:${CURRENT_VERSION} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64 +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name kubernetes-images-sync-operator-builder + $(CONTAINER_TOOL) buildx use kubernetes-images-sync-operator-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG}:${CURRENT_VERSION} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm kubernetes-images-sync-operator-builder + rm Dockerfile.cross + +.PHONY: build-installer +build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. + mkdir -p dist + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}:${CURRENT_VERSION} + $(KUSTOMIZE) build config/default > dist/install.yaml + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +HELMIFY ?= helmify + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.4.3 +CONTROLLER_TOOLS_VERSION ?= v0.16.1 +ENVTEST_VERSION ?= release-0.19 +GOLANGCI_LINT_VERSION ?= v1.59.1 + +.PHONY: print-version +print-version: + @echo "Current version: $(CURRENT_VERSION)" + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. +$(ENVTEST): $(LOCALBIN) + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +.PHONY: helmify +helmify: $(HELMIFY) ## Download helmify locally if necessary. +$(HELMIFY): $(LOCALBIN) + test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest + +.PHONY: helm +helm: manifests kustomize helmify + $(KUSTOMIZE) build config/default | helmify && \ + cp chart-defaults/Chart.yaml chart/Chart.yaml && \ + ./update-version.sh $(CURRENT_VERSION) $(IMG) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef diff --git a/PROJECT b/PROJECT new file mode 100644 index 0000000..79d91c7 --- /dev/null +++ b/PROJECT @@ -0,0 +1,28 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: raczylo.com +layout: + - go.kubebuilder.io/v4 +multigroup: true +projectName: kubernetes-images-sync-operator +repo: raczylo.com/kubernetes-images-sync-operator +resources: + - api: + crdVersion: v1 + namespaced: true + controller: true + group: raczylo.com + kind: ClusterImageExport + path: raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1 + version: v1 + - api: + crdVersion: v1 + namespaced: true + controller: true + group: raczylo.com + kind: ClusterImage + path: raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1 + version: v1 +version: "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ebcbfc --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# kubernetes-images-sync-operator +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started + +### Prerequisites +- go version v1.22.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. + +### To Deploy on the cluster +**Build and push your image to the location specified by `IMG`:** + +```sh +make docker-build docker-push IMG=/kubernetes-images-sync-operator:tag +``` + +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. + +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/kubernetes-images-sync-operator:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +>**NOTE**: Ensure that the samples has default values to test it out. + +### To Uninstall +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +## Project Distribution + +Following are the steps to build the installer and distribute this project to users. + +1. Build the installer for the image built and published in the registry: + +```sh +make build-installer IMG=/kubernetes-images-sync-operator:tag +``` + +NOTE: The makefile target mentioned above generates an 'install.yaml' +file in the dist directory. This file contains all the resources built +with Kustomize, which are necessary to install this project without +its dependencies. + +2. Using the installer + +Users can just run kubectl apply -f to install the project, i.e.: + +```sh +kubectl apply -f https://raw.githubusercontent.com//kubernetes-images-sync-operator//dist/install.yaml +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +**NOTE:** Run `make help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/api/raczylo.com/v1/clusterimage_types.go b/api/raczylo.com/v1/clusterimage_types.go new file mode 100644 index 0000000..c6ac46e --- /dev/null +++ b/api/raczylo.com/v1/clusterimage_types.go @@ -0,0 +1,79 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterImageSpec defines the desired state of ClusterImage +// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image" +// +kubebuilder:printcolumn:name="Tag",type="string",JSONPath=".spec.tag" +// +kubebuilder:printcolumn:name="SHA",type="string",JSONPath=".spec.sha" +// +kubebuilder:printcolumn:name="Storage",type="string",JSONPath=".spec.storage" +// +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"` +} + +// ClusterImageStatus defines the observed state of ClusterImage +type ClusterImageStatus struct { + Progress string `json:"progress,omitempty"` + // default value is 0 + // +kubebuilder:default:=0 + RetryCount int `json:"retryCount,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// ClusterImage is the Schema for the clusterimages API +// +kubebuilder:printcolumn:name="Ref",type="string",JSONPath=".spec.exportName" +// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image" +// +kubebuilder:printcolumn:name="Tag",type="string",JSONPath=".spec.tag" +// +kubebuilder:printcolumn:name="SHA",type="string",JSONPath=".spec.sha" +// +kubebuilder:printcolumn:name="Storage",type="string",JSONPath=".spec.storage" +// +kubebuilder:printcolumn:name="Path",type="string",JSONPath=".spec.exportPath" +// +kubebuilder:printcolumn:name="Progress",type="string",JSONPath=".status.progress" +// +kubebuilder:printcolumn:name="Retries",type="integer",JSONPath=".status.retryCount" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type ClusterImage struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterImageSpec `json:"spec,omitempty"` + Status ClusterImageStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterImageList contains a list of ClusterImage +type ClusterImageList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterImage `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterImage{}, &ClusterImageList{}) +} diff --git a/api/raczylo.com/v1/clusterimageexport_types.go b/api/raczylo.com/v1/clusterimageexport_types.go new file mode 100644 index 0000000..c323a7b --- /dev/null +++ b/api/raczylo.com/v1/clusterimageexport_types.go @@ -0,0 +1,100 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ClusterImageStorageS3 struct { + // Bucket name + Bucket string `json:"bucket"` + Region string `json:"region"` + // S3 bucket credentials + AccessKey string `json:"accessKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + UseRole bool `json:"useRole,omitempty"` + // RoleARN is the ARN of the role to be used for the deployment + RoleARN string `json:"roleARN,omitempty"` + // Defines the endpoint for the S3 storage + // If none specified - default AWS endpoint will be used + Endpoint string `json:"endpoint,omitempty"` + // Defines the secret name for credentials + SecretName string `json:"secretName,omitempty"` +} + +// ClusterImageStorageSpec defines the desired state of ClusterImageStorage +type ClusterImageStorageSpec struct { + // +kubebuilder:validation:Enum=file;S3 + StorageTarget string `json:"target"` + S3 ClusterImageStorageS3 `json:"s3,omitempty"` +} + +// ClusterImageExportSpec defines the desired state of ClusterImageExport +// +kubebuilder:printcolumn:name="BasePath",type="string",JSONPath=".spec.basePath" +// +kubebuilder:printcolumn:name="Storage",type="string",JSONPath=".spec.storage.target" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type ClusterImageExportSpec struct { + Name string `json:"name"` + CreatedAt metav1.Time `json:"createdAt,omitempty"` + // Exclude images which contain these strings + Excludes []string `json:"excludes,omitempty"` + // Include only images which contain these strings + Includes []string `json:"includes,omitempty"` + // Base path for the export - both file and S3 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + BasePath string `json:"basePath"` + Storage ClusterImageStorageSpec `json:"storage"` + // +kubebuilder:validation.Minimum=1 + // +kubebuilder:validation.Maximum=100 + MaxConcurrentJobs int `json:"maxConcurrentJobs"` +} + +// ClusterImageExportStatus defines the observed state of ClusterImageExport +type ClusterImageExportStatus struct { + Progress string `json:"progress,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ClusterImageExport is the Schema for the clusterimageexports API +// +kubebuilder:printcolumn:name="BasePath",type="string",JSONPath=".spec.basePath" +// +kubebuilder:printcolumn:name="Storage",type="string",JSONPath=".spec.storage.target" +// +kubebuilder:printcolumn:name="Progress",type="string",JSONPath=".status.progress" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type ClusterImageExport struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterImageExportSpec `json:"spec,omitempty"` + Status ClusterImageExportStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterImageExportList contains a list of ClusterImageExport +type ClusterImageExportList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterImageExport `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterImageExport{}, &ClusterImageExportList{}) +} diff --git a/api/raczylo.com/v1/groupversion_info.go b/api/raczylo.com/v1/groupversion_info.go new file mode 100644 index 0000000..d689874 --- /dev/null +++ b/api/raczylo.com/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the raczylo.com v1 API group +// +kubebuilder:object:generate=true +// +groupName=raczylo.com +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "raczylo.com", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/raczylo.com/v1/zz_generated.deepcopy.go b/api/raczylo.com/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..0b2521f --- /dev/null +++ b/api/raczylo.com/v1/zz_generated.deepcopy.go @@ -0,0 +1,246 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImage) DeepCopyInto(out *ClusterImage) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImage. +func (in *ClusterImage) DeepCopy() *ClusterImage { + if in == nil { + return nil + } + out := new(ClusterImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterImage) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageExport) DeepCopyInto(out *ClusterImageExport) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageExport. +func (in *ClusterImageExport) DeepCopy() *ClusterImageExport { + if in == nil { + return nil + } + out := new(ClusterImageExport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterImageExport) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageExportList) DeepCopyInto(out *ClusterImageExportList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterImageExport, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageExportList. +func (in *ClusterImageExportList) DeepCopy() *ClusterImageExportList { + if in == nil { + return nil + } + out := new(ClusterImageExportList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterImageExportList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageExportSpec) DeepCopyInto(out *ClusterImageExportSpec) { + *out = *in + in.CreatedAt.DeepCopyInto(&out.CreatedAt) + if in.Excludes != nil { + in, out := &in.Excludes, &out.Excludes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Includes != nil { + in, out := &in.Includes, &out.Includes + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Storage = in.Storage +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageExportSpec. +func (in *ClusterImageExportSpec) DeepCopy() *ClusterImageExportSpec { + if in == nil { + return nil + } + out := new(ClusterImageExportSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageExportStatus) DeepCopyInto(out *ClusterImageExportStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageExportStatus. +func (in *ClusterImageExportStatus) DeepCopy() *ClusterImageExportStatus { + if in == nil { + return nil + } + out := new(ClusterImageExportStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageList) DeepCopyInto(out *ClusterImageList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageList. +func (in *ClusterImageList) DeepCopy() *ClusterImageList { + if in == nil { + return nil + } + out := new(ClusterImageList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterImageList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageSpec) DeepCopyInto(out *ClusterImageSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageSpec. +func (in *ClusterImageSpec) DeepCopy() *ClusterImageSpec { + if in == nil { + return nil + } + out := new(ClusterImageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageStatus) DeepCopyInto(out *ClusterImageStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageStatus. +func (in *ClusterImageStatus) DeepCopy() *ClusterImageStatus { + if in == nil { + return nil + } + out := new(ClusterImageStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageStorageS3) DeepCopyInto(out *ClusterImageStorageS3) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageStorageS3. +func (in *ClusterImageStorageS3) DeepCopy() *ClusterImageStorageS3 { + if in == nil { + return nil + } + out := new(ClusterImageStorageS3) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterImageStorageSpec) DeepCopyInto(out *ClusterImageStorageSpec) { + *out = *in + out.S3 = in.S3 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterImageStorageSpec. +func (in *ClusterImageStorageSpec) DeepCopy() *ClusterImageStorageSpec { + if in == nil { + return nil + } + out := new(ClusterImageStorageSpec) + in.DeepCopyInto(out) + return out +} diff --git a/chart-defaults/Chart.yaml b/chart-defaults/Chart.yaml new file mode 100644 index 0000000..29c56f4 --- /dev/null +++ b/chart-defaults/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: kube-images-sync +description: | + A Helm chart for Kubernetes Images Sync Operator. + Kubernetes Images Sync Operator is responsible for backing up and restoring images from a Kubernetes cluster. + It's ultimate goal is to provide synchonization of images between multiple environments, quite often air-gapped. + It compiles the list of images currently present in the cluster and uploads them to the specified storage. + Whenever new CRD is created - it will try to figure out which images were already uploaded and which are new and + upload only the new ones to avoid repetition. + +type: application + +version: 0.0.0 + +appVersion: "0.0.0" + +home: https://github.com/lukaszraczylo/kubernetes-images-sync-operator + +maintainers: + - name: lukaszraczylo + email: github-enquiries@raczylo.com diff --git a/chart/.helmignore b/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..b5e855a --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: kube-images-sync +description: | + A Helm chart for Kubernetes Images Sync Operator. + Kubernetes Images Sync Operator is responsible for backing up and restoring images from a Kubernetes cluster. + It's ultimate goal is to provide synchonization of images between multiple environments, quite often air-gapped. + It compiles the list of images currently present in the cluster and uploads them to the specified storage. + Whenever new CRD is created - it will try to figure out which images were already uploaded and which are new and + upload only the new ones to avoid repetition. + +type: application + +version: 0.0.26 + +appVersion: "0.0.26" + +home: https://github.com/lukaszraczylo/kubernetes-images-sync-operator + +maintainers: + - name: lukaszraczylo + email: github-enquiries@raczylo.com diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..7ba5edc --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "chart.labels" -}} +helm.sh/chart: {{ include "chart.chart" . }} +{{ include "chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/chart/templates/clusterimage-crd.yaml b/chart/templates/clusterimage-crd.yaml new file mode 100644 index 0000000..c97266d --- /dev/null +++ b/chart/templates/clusterimage-crd.yaml @@ -0,0 +1,108 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterimages.raczylo.com + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + group: raczylo.com + names: + kind: ClusterImage + listKind: ClusterImageList + plural: clusterimages + singular: clusterimage + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.exportName + name: Ref + type: string + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .spec.tag + name: Tag + type: string + - jsonPath: .spec.sha + name: SHA + type: string + - jsonPath: .spec.storage + name: Storage + type: string + - jsonPath: .spec.exportPath + name: Path + type: string + - jsonPath: .status.progress + name: Progress + type: string + - jsonPath: .status.retryCount + name: Retries + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterImage is the Schema for the clusterimages API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClusterImageSpec defines the desired state of ClusterImage + properties: + exportName: + type: string + exportPath: + type: string + fullName: + type: string + image: + type: string + sha: + type: string + storage: + type: string + tag: + type: string + required: + - exportName + type: object + status: + description: ClusterImageStatus defines the observed state of ClusterImage + properties: + progress: + type: string + retryCount: + default: 0 + description: default value is 0 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/chart/templates/clusterimageexport-crd.yaml b/chart/templates/clusterimageexport-crd.yaml new file mode 100644 index 0000000..94285ee --- /dev/null +++ b/chart/templates/clusterimageexport-crd.yaml @@ -0,0 +1,141 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterimageexports.raczylo.com + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + group: raczylo.com + names: + kind: ClusterImageExport + listKind: ClusterImageExportList + plural: clusterimageexports + singular: clusterimageexport + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.basePath + name: BasePath + type: string + - jsonPath: .spec.storage.target + name: Storage + type: string + - jsonPath: .status.progress + name: Progress + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterImageExport is the Schema for the clusterimageexports API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClusterImageExportSpec defines the desired state of ClusterImageExport + properties: + basePath: + description: Base path for the export - both file and S3 + maxLength: 255 + minLength: 1 + type: string + createdAt: + format: date-time + type: string + excludes: + description: Exclude images which contain these strings + items: + type: string + type: array + includes: + description: Include only images which contain these strings + items: + type: string + type: array + maxConcurrentJobs: + type: integer + name: + type: string + storage: + description: ClusterImageStorageSpec defines the desired state of ClusterImageStorage + properties: + s3: + properties: + accessKey: + description: S3 bucket credentials + type: string + bucket: + description: Bucket name + type: string + endpoint: + description: |- + Defines the endpoint for the S3 storage + If none specified - default AWS endpoint will be used + type: string + region: + type: string + roleARN: + description: RoleARN is the ARN of the role to be used for the + deployment + type: string + secretKey: + type: string + secretName: + description: Defines the secret name for credentials + type: string + useRole: + type: boolean + required: + - bucket + - region + type: object + target: + enum: + - file + - S3 + type: string + required: + - target + type: object + required: + - basePath + - maxConcurrentJobs + - name + - storage + type: object + status: + description: ClusterImageExportStatus defines the observed state of ClusterImageExport + properties: + progress: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/chart/templates/cm-raczylo-com-metrics-service.yaml b/chart/templates/cm-raczylo-com-metrics-service.yaml new file mode 100644 index 0000000..e7eeb11 --- /dev/null +++ b/chart/templates/cm-raczylo-com-metrics-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "chart.fullname" . }}-cm-raczylo-com-metrics-service + labels: + control-plane: cm-raczylo-com + {{- include "chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.cmRaczyloComMetricsService.type }} + selector: + control-plane: cm-raczylo-com + {{- include "chart.selectorLabels" . | nindent 4 }} + ports: + {{- .Values.cmRaczyloComMetricsService.ports | toYaml | nindent 2 }} \ No newline at end of file diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..c71d54e --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "chart.fullname" . }}-cm-raczylo-com + labels: + control-plane: cm-raczylo-com + {{- include "chart.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.cmRaczyloCom.replicas }} + selector: + matchLabels: + control-plane: cm-raczylo-com + {{- include "chart.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + control-plane: cm-raczylo-com + {{- include "chart.selectorLabels" . | nindent 8 }} + annotations: + kubectl.kubernetes.io/default-container: manager + spec: + containers: + - args: {{- toYaml .Values.cmRaczyloCom.manager.args | nindent 8 }} + command: + - /manager + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.cmRaczyloCom.manager.image.repository }}:{{ .Values.cmRaczyloCom.manager.image.tag + | default .Chart.AppVersion }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {{- toYaml .Values.cmRaczyloCom.manager.resources | nindent 10 }} + securityContext: {{- toYaml .Values.cmRaczyloCom.manager.containerSecurityContext + | nindent 10 }} + securityContext: + runAsNonRoot: true + serviceAccountName: {{ include "chart.fullname" . }}-cm-raczylo-com + terminationGracePeriodSeconds: 10 \ No newline at end of file diff --git a/chart/templates/metrics-auth-raczylo-rbac.yaml b/chart/templates/metrics-auth-raczylo-rbac.yaml new file mode 100644 index 0000000..aff731d --- /dev/null +++ b/chart/templates/metrics-auth-raczylo-rbac.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-metrics-auth-raczylo + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create \ No newline at end of file diff --git a/chart/templates/metrics-auth-raczylobinding-rbac.yaml b/chart/templates/metrics-auth-raczylobinding-rbac.yaml new file mode 100644 index 0000000..4cc45ef --- /dev/null +++ b/chart/templates/metrics-auth-raczylobinding-rbac.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "chart.fullname" . }}-metrics-auth-raczylobinding + labels: + {{- include "chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "chart.fullname" . }}-metrics-auth-raczylo' +subjects: +- kind: ServiceAccount + name: '{{ include "chart.fullname" . }}-cm-raczylo-com' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/chart/templates/metrics-raczylo-rbac.yaml b/chart/templates/metrics-raczylo-rbac.yaml new file mode 100644 index 0000000..8de2344 --- /dev/null +++ b/chart/templates/metrics-raczylo-rbac.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-metrics-raczylo + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get \ No newline at end of file diff --git a/chart/templates/mr-raczylo-com-rbac.yaml b/chart/templates/mr-raczylo-com-rbac.yaml new file mode 100644 index 0000000..ec1388a --- /dev/null +++ b/chart/templates/mr-raczylo-com-rbac.yaml @@ -0,0 +1,63 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-mr-raczylo-com + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - apps + resources: + - daemonsets + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - get + - list + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimageexports + - clusterimages + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimageexports/finalizers + verbs: + - update +- apiGroups: + - raczylo.com + resources: + - clusterimageexports/status + verbs: + - get + - patch + - update \ No newline at end of file diff --git a/chart/templates/mr-raczylo-combinding-rbac.yaml b/chart/templates/mr-raczylo-combinding-rbac.yaml new file mode 100644 index 0000000..5d39c43 --- /dev/null +++ b/chart/templates/mr-raczylo-combinding-rbac.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "chart.fullname" . }}-mr-raczylo-combinding + labels: + {{- include "chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "chart.fullname" . }}-mr-raczylo-com' +subjects: +- kind: ServiceAccount + name: '{{ include "chart.fullname" . }}-cm-raczylo-com' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/chart/templates/raczylo-com-leader-rbac.yaml b/chart/templates/raczylo-com-leader-rbac.yaml new file mode 100644 index 0000000..9283e34 --- /dev/null +++ b/chart/templates/raczylo-com-leader-rbac.yaml @@ -0,0 +1,38 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "chart.fullname" . }}-raczylo-com-leader + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch \ No newline at end of file diff --git a/chart/templates/raczylo-com-leaderbinding-rbac.yaml b/chart/templates/raczylo-com-leaderbinding-rbac.yaml new file mode 100644 index 0000000..dc3e27d --- /dev/null +++ b/chart/templates/raczylo-com-leaderbinding-rbac.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "chart.fullname" . }}-raczylo-com-leaderbinding + labels: + {{- include "chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: '{{ include "chart.fullname" . }}-raczylo-com-leader' +subjects: +- kind: ServiceAccount + name: '{{ include "chart.fullname" . }}-cm-raczylo-com' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/chart/templates/raczylo.com-clusterimage-editor-rbac.yaml b/chart/templates/raczylo.com-clusterimage-editor-rbac.yaml new file mode 100644 index 0000000..9261080 --- /dev/null +++ b/chart/templates/raczylo.com-clusterimage-editor-rbac.yaml @@ -0,0 +1,25 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-raczylo.com-clusterimage-editor-role + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - raczylo.com + resources: + - clusterimages + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimages/status + verbs: + - get \ No newline at end of file diff --git a/chart/templates/raczylo.com-clusterimage-viewer-rbac.yaml b/chart/templates/raczylo.com-clusterimage-viewer-rbac.yaml new file mode 100644 index 0000000..0fddff6 --- /dev/null +++ b/chart/templates/raczylo.com-clusterimage-viewer-rbac.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-raczylo.com-clusterimage-viewer-role + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - raczylo.com + resources: + - clusterimages + verbs: + - get + - list + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimages/status + verbs: + - get \ No newline at end of file diff --git a/chart/templates/raczylo.com-clusterimageexport-editor-rbac.yaml b/chart/templates/raczylo.com-clusterimageexport-editor-rbac.yaml new file mode 100644 index 0000000..5bd4b77 --- /dev/null +++ b/chart/templates/raczylo.com-clusterimageexport-editor-rbac.yaml @@ -0,0 +1,25 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-raczylo.com-clusterimageexport-editor-role + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - raczylo.com + resources: + - clusterimageexports + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimageexports/status + verbs: + - get \ No newline at end of file diff --git a/chart/templates/raczylo.com-clusterimageexport-viewer-rbac.yaml b/chart/templates/raczylo.com-clusterimageexport-viewer-rbac.yaml new file mode 100644 index 0000000..99d203d --- /dev/null +++ b/chart/templates/raczylo.com-clusterimageexport-viewer-rbac.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-raczylo.com-clusterimageexport-viewer-role + labels: + {{- include "chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - raczylo.com + resources: + - clusterimageexports + verbs: + - get + - list + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimageexports/status + verbs: + - get \ No newline at end of file diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..f8bcd7e --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "chart.fullname" . }}-cm-raczylo-com + labels: + {{- include "chart.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.cmRaczyloCom.serviceAccount.annotations | nindent 4 }} \ No newline at end of file diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..2560416 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,32 @@ +cmRaczyloCom: + manager: + args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: + repository: controller + tag: latest + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + replicas: 1 + serviceAccount: + annotations: {} +cmRaczyloComMetricsService: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + type: ClusterIP +kubernetesClusterDomain: cluster.local diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..c5840d9 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,183 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "crypto/tls" + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" + raczylocomcontroller "raczylo.com/kubernetes-images-sync-operator/internal/controller/raczylo.com" + "raczylo.com/kubernetes-images-sync-operator/shared" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(raczylocomv1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: tlsOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are + // not provided, self-signed certificates will be generated by default. This option is not recommended for + // production environments as self-signed certificates do not offer the same level of trust and security + // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing + // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName + // to provide certificates, ensuring the server communicates using trusted and secure certificates. + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "97ba1beb.raczylo.com", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err := shared.SetupIndexers(mgr); err != nil { + setupLog.Error(err, "unable to setup indexers") + os.Exit(1) + } + + if err = (&raczylocomcontroller.ClusterImageExportReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterImageExport") + os.Exit(1) + } + if err = (&raczylocomcontroller.ClusterImageReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterImage") + os.Exit(1) + } + // +kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/config/crd/bases/raczylo.com_clusterimageexports.yaml b/config/crd/bases/raczylo.com_clusterimageexports.yaml new file mode 100644 index 0000000..2529e32 --- /dev/null +++ b/config/crd/bases/raczylo.com_clusterimageexports.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: clusterimageexports.raczylo.com +spec: + group: raczylo.com + names: + kind: ClusterImageExport + listKind: ClusterImageExportList + plural: clusterimageexports + singular: clusterimageexport + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.basePath + name: BasePath + type: string + - jsonPath: .spec.storage.target + name: Storage + type: string + - jsonPath: .status.progress + name: Progress + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterImageExport is the Schema for the clusterimageexports + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClusterImageExportSpec defines the desired state of ClusterImageExport + properties: + basePath: + description: Base path for the export - both file and S3 + maxLength: 255 + minLength: 1 + type: string + createdAt: + format: date-time + type: string + excludes: + description: Exclude images which contain these strings + items: + type: string + type: array + includes: + description: Include only images which contain these strings + items: + type: string + type: array + maxConcurrentJobs: + type: integer + name: + type: string + storage: + description: ClusterImageStorageSpec defines the desired state of + ClusterImageStorage + properties: + s3: + properties: + accessKey: + description: S3 bucket credentials + type: string + bucket: + description: Bucket name + type: string + endpoint: + description: |- + Defines the endpoint for the S3 storage + If none specified - default AWS endpoint will be used + type: string + region: + type: string + roleARN: + description: RoleARN is the ARN of the role to be used for + the deployment + type: string + secretKey: + type: string + secretName: + description: Defines the secret name for credentials + type: string + useRole: + type: boolean + required: + - bucket + - region + type: object + target: + enum: + - file + - S3 + type: string + required: + - target + type: object + required: + - basePath + - maxConcurrentJobs + - name + - storage + type: object + status: + description: ClusterImageExportStatus defines the observed state of ClusterImageExport + properties: + progress: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/raczylo.com_clusterimages.yaml b/config/crd/bases/raczylo.com_clusterimages.yaml new file mode 100644 index 0000000..8622579 --- /dev/null +++ b/config/crd/bases/raczylo.com_clusterimages.yaml @@ -0,0 +1,101 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: clusterimages.raczylo.com +spec: + group: raczylo.com + names: + kind: ClusterImage + listKind: ClusterImageList + plural: clusterimages + singular: clusterimage + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.exportName + name: Ref + type: string + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .spec.tag + name: Tag + type: string + - jsonPath: .spec.sha + name: SHA + type: string + - jsonPath: .spec.storage + name: Storage + type: string + - jsonPath: .spec.exportPath + name: Path + type: string + - jsonPath: .status.progress + name: Progress + type: string + - jsonPath: .status.retryCount + name: Retries + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterImage is the Schema for the clusterimages API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClusterImageSpec defines the desired state of ClusterImage + properties: + exportName: + type: string + exportPath: + type: string + fullName: + type: string + image: + type: string + sha: + type: string + storage: + type: string + tag: + type: string + required: + - exportName + type: object + status: + description: ClusterImageStatus defines the observed state of ClusterImage + properties: + progress: + type: string + retryCount: + default: 0 + description: default value is 0 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 0000000..3c123b9 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,24 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: + - bases/raczylo.com_clusterimageexports.yaml + - bases/raczylo.com_clusterimages.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_raczylo.com_clusterimageexports.yaml +#- path: patches/cainjection_in_raczylo.com_clusterimages.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +#configurations: +#- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..ec5c150 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 0000000..842b8ed --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,151 @@ +# Adds namespace to all resources. +namespace: kubernetes-images-sync-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: kubernetes-images-sync-operator- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus +# [METRICS] Expose the controller manager metrics service. +- metrics_service.yaml +# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. +# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. +# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will +# be able to communicate with the Webhook Server. +#- ../network-policy + +# Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager +patches: +# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. +# More info: https://book.kubebuilder.io/reference/metrics +- path: manager_metrics_patch.yaml + target: + kind: Deployment + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/config/default/manager_metrics_patch.yaml b/config/default/manager_metrics_patch.yaml new file mode 100644 index 0000000..2aaef65 --- /dev/null +++ b/config/default/manager_metrics_patch.yaml @@ -0,0 +1,4 @@ +# This patch adds the args to allow exposing the metrics endpoint using HTTPS +- op: add + path: /spec/template/spec/containers/0/args/0 + value: --metrics-bind-address=:8443 diff --git a/config/default/metrics_service.yaml b/config/default/metrics_service.yaml new file mode 100644 index 0000000..8a5d541 --- /dev/null +++ b/config/default/metrics_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: cm-raczylo-com + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: cm-raczylo-com-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: cm-raczylo-com diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 0000000..5c5f0b8 --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 0000000..c94fe7e --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,95 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: cm-raczylo-com + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cm-raczylo-com + namespace: system + labels: + control-plane: cm-raczylo-com + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: cm-raczylo-com + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: cm-raczylo-com + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: cm-raczylo-com + terminationGracePeriodSeconds: 10 diff --git a/config/network-policy/allow-metrics-traffic.yaml b/config/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 0000000..83ffacc --- /dev/null +++ b/config/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic +# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those +# namespaces are able to gathering data from the metrics endpoint. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: allow-metrics-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: cm-raczylo-com + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label metrics: enabled + - from: + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label + ports: + - port: 8443 + protocol: TCP diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml new file mode 100644 index 0000000..ec0fb5e --- /dev/null +++ b/config/network-policy/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- allow-metrics-traffic.yaml diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 0000000..ed13716 --- /dev/null +++ b/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 0000000..cb19312 --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -0,0 +1,30 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: cm-raczylo-com + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: cm-raczylo-com-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https # Ensure this is the name of the port that exposes HTTPS metrics + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables + # certificate verification. This poses a significant security risk by making the system vulnerable to + # man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between + # Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data, + # compromising the integrity and confidentiality of the information. + # Please use the following options for secure configurations: + # caFile: /etc/metrics-certs/ca.crt + # certFile: /etc/metrics-certs/tls.crt + # keyFile: /etc/metrics-certs/tls.key + insecureSkipVerify: true + selector: + matchLabels: + control-plane: cm-raczylo-com diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 0000000..ee5f16e --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,29 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# The following RBAC configurations are used to protect +# the metrics endpoint with authn/authz. These configurations +# ensure that only authorized users and service accounts +# can access the metrics endpoint. Comment the following +# permissions if you want to disable this protection. +# More info: https://book.kubebuilder.io/reference/metrics.html +- metrics_auth_role.yaml +- metrics_auth_role_binding.yaml +- metrics_reader_role.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- raczylo.com_clusterimage_editor_role.yaml +- raczylo.com_clusterimage_viewer_role.yaml +- raczylo.com_clusterimageexport_editor_role.yaml +- raczylo.com_clusterimageexport_viewer_role.yaml + diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..4300fc7 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,40 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: raczylo-com-leader +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..6e593ca --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: raczylo-com-leaderbinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: raczylo-com-leader +subjects: + - kind: ServiceAccount + name: cm-raczylo-com + namespace: system diff --git a/config/rbac/metrics_auth_role.yaml b/config/rbac/metrics_auth_role.yaml new file mode 100644 index 0000000..1711239 --- /dev/null +++ b/config/rbac/metrics_auth_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-auth-raczylo +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/rbac/metrics_auth_role_binding.yaml b/config/rbac/metrics_auth_role_binding.yaml new file mode 100644 index 0000000..375952e --- /dev/null +++ b/config/rbac/metrics_auth_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: metrics-auth-raczylobinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metrics-auth-raczylo +subjects: + - kind: ServiceAccount + name: cm-raczylo-com + namespace: system diff --git a/config/rbac/metrics_reader_role.yaml b/config/rbac/metrics_reader_role.yaml new file mode 100644 index 0000000..52c43ae --- /dev/null +++ b/config/rbac/metrics_reader_role.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-raczylo +rules: + - nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/config/rbac/raczylo.com_clusterimage_editor_role.yaml b/config/rbac/raczylo.com_clusterimage_editor_role.yaml new file mode 100644 index 0000000..ec3aa7b --- /dev/null +++ b/config/rbac/raczylo.com_clusterimage_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit clusterimages. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: raczylo.com-clusterimage-editor-role +rules: + - apiGroups: + - raczylo.com + resources: + - clusterimages + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - raczylo.com + resources: + - clusterimages/status + verbs: + - get diff --git a/config/rbac/raczylo.com_clusterimage_viewer_role.yaml b/config/rbac/raczylo.com_clusterimage_viewer_role.yaml new file mode 100644 index 0000000..2e9ef11 --- /dev/null +++ b/config/rbac/raczylo.com_clusterimage_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view clusterimages. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: raczylo.com-clusterimage-viewer-role +rules: + - apiGroups: + - raczylo.com + resources: + - clusterimages + verbs: + - get + - list + - watch + - apiGroups: + - raczylo.com + resources: + - clusterimages/status + verbs: + - get diff --git a/config/rbac/raczylo.com_clusterimageexport_editor_role.yaml b/config/rbac/raczylo.com_clusterimageexport_editor_role.yaml new file mode 100644 index 0000000..4c0214e --- /dev/null +++ b/config/rbac/raczylo.com_clusterimageexport_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit clusterimageexports. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: raczylo.com-clusterimageexport-editor-role +rules: + - apiGroups: + - raczylo.com + resources: + - clusterimageexports + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - raczylo.com + resources: + - clusterimageexports/status + verbs: + - get diff --git a/config/rbac/raczylo.com_clusterimageexport_viewer_role.yaml b/config/rbac/raczylo.com_clusterimageexport_viewer_role.yaml new file mode 100644 index 0000000..9f0585a --- /dev/null +++ b/config/rbac/raczylo.com_clusterimageexport_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view clusterimageexports. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: raczylo.com-clusterimageexport-viewer-role +rules: + - apiGroups: + - raczylo.com + resources: + - clusterimageexports + verbs: + - get + - list + - watch + - apiGroups: + - raczylo.com + resources: + - clusterimageexports/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..41781d8 --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,62 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mr-raczylo-com +rules: +- apiGroups: + - apps + resources: + - daemonsets + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - get + - list + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimageexports + - clusterimages + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - raczylo.com + resources: + - clusterimageexports/finalizers + verbs: + - update +- apiGroups: + - raczylo.com + resources: + - clusterimageexports/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 0000000..e682b69 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: mr-raczylo-combinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: mr-raczylo-com +subjects: + - kind: ServiceAccount + name: cm-raczylo-com + namespace: system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 0000000..1bdef0c --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: kubernetes-images-sync-operator + app.kubernetes.io/managed-by: kustomize + name: cm-raczylo-com + namespace: system diff --git a/docker-image-management/Dockerfile b/docker-image-management/Dockerfile new file mode 100644 index 0000000..aff3a62 --- /dev/null +++ b/docker-image-management/Dockerfile @@ -0,0 +1,36 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive +ARG TARGETPLATFORM +ARG TARGETARCH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + gnupg2 \ + python3-pip \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +RUN echo "deb [arch=${TARGETARCH}] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_22.04/ /" | tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list \ + && curl -fsSL "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_22.04/Release.key" | apt-key add - + +RUN apt-get update && apt-get install -y --no-install-recommends \ + uidmap \ + fuse-overlayfs \ + podman \ + netavark \ + && rm -rf /var/lib/apt/lists/* + +RUN adduser --disabled-password --gecos "" --uid 1001 runner \ + && groupadd docker --gid 123 \ + && usermod -aG sudo,docker runner \ + && echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers \ + && echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers + +WORKDIR /home/runner + +COPY storage.conf containers.conf registries.conf /home/runner/.config/containers/ +COPY requirements.txt export.py cleanup.py ./ +USER runner +RUN sudo chown -R runner:runner /home/runner/.config \ + && python3 -m pip install --no-cache-dir --only-binary=:all: -r requirements.txt \ No newline at end of file diff --git a/docker-image-management/cleanup.py b/docker-image-management/cleanup.py new file mode 100755 index 0000000..6fa44fc --- /dev/null +++ b/docker-image-management/cleanup.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +import os +import boto3 +import argparse +from botocore.exceptions import ClientError + +def get_s3_client(use_role=False, role_name=None, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None): + """ + Create and return an S3 client based on the provided authentication method, endpoint, and region. + """ + client_kwargs = {} + + if endpoint_url: + client_kwargs['endpoint_url'] = endpoint_url + elif region: + client_kwargs['region_name'] = region + + if use_role: + if role_name: + # Assume the specified role + sts_client = boto3.client('sts') + assumed_role_object = sts_client.assume_role( + RoleArn=f"arn:aws:iam::{boto3.client('sts').get_caller_identity()['Account']}:role/{role_name}", + RoleSessionName="AssumeRoleSession" + ) + credentials = assumed_role_object['Credentials'] + client_kwargs['aws_access_key_id'] = credentials['AccessKeyId'] + client_kwargs['aws_secret_access_key'] = credentials['SecretAccessKey'] + client_kwargs['aws_session_token'] = credentials['SessionToken'] + return boto3.client('s3', **client_kwargs) + elif aws_access_key_id and aws_secret_access_key: + client_kwargs['aws_access_key_id'] = aws_access_key_id + client_kwargs['aws_secret_access_key'] = aws_secret_access_key + return boto3.client('s3', **client_kwargs) + else: + raise ValueError("Either use_role must be True, or both aws_access_key_id and aws_secret_access_key must be provided") + +def remove_directory(destination, use_role=False, role_name=None, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None): + """ + Remove a directory recursively, either local or in an S3 bucket + """ + if destination.startswith('s3://'): + # Removing from S3 + s3_client = get_s3_client(use_role, role_name, aws_access_key_id, aws_secret_access_key, endpoint_url, region) + bucket, prefix = parse_s3_path(destination) + try: + paginator = s3_client.get_paginator('list_objects_v2') + for page in paginator.paginate(Bucket=bucket, Prefix=prefix): + if 'Contents' in page: + objects_to_delete = [{'Key': obj['Key']} for obj in page['Contents']] + s3_client.delete_objects(Bucket=bucket, Delete={'Objects': objects_to_delete}) + print(f"Directory {destination} removed successfully from S3") + except ClientError as e: + print(f"Error removing directory from S3: {str(e)}") + return False + else: + # Removing local directory + try: + import shutil + if os.path.exists(destination): + shutil.rmtree(destination) + print(f"Directory {destination} removed successfully") + else: + print(f"Directory {destination} does not exist") + except IOError as e: + print(f"Error removing directory: {str(e)}") + return False + return True + +def parse_s3_path(s3_path): + """ + Parse an S3 path into bucket and key + """ + parts = s3_path.replace('s3://', '').split('/', 1) + bucket = parts[0] + key = parts[1] if len(parts) > 1 else '' + return bucket, key + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Remove a directory recursively, either local or in an S3 bucket.") + parser.add_argument("destination", help="The directory path (local) or S3 path (e.g., 's3://bucket/prefix') to remove") + parser.add_argument("--use_role", action="store_true", help="Use IAM role for authentication") + parser.add_argument("--role_name", help="The name of the IAM role to assume") + parser.add_argument("--aws_access_key_id", help="AWS access key ID") + parser.add_argument("--aws_secret_access_key", help="AWS secret access key") + parser.add_argument("--endpoint_url", help="S3-compatible endpoint URL") + parser.add_argument("--region", help="AWS region (ignored if endpoint_url is specified)") + + args = parser.parse_args() + + if args.destination.startswith('s3://'): + if args.use_role and (args.aws_access_key_id or args.aws_secret_access_key or args.endpoint_url): + parser.error("When using IAM role (--use_role), access key, secret, and endpoint URL should not be specified.") + + if (args.aws_access_key_id or args.aws_secret_access_key) and not (args.aws_access_key_id and args.aws_secret_access_key): + parser.error("Both --aws_access_key_id and --aws_secret_access_key must be provided when using access key authentication.") + + if not args.use_role and not (args.aws_access_key_id and args.aws_secret_access_key): + parser.error("Either --use_role or both --aws_access_key_id and --aws_secret_access_key must be provided for S3 operations.") + + if args.use_role and args.role_name and (args.aws_access_key_id or args.aws_secret_access_key): + parser.error("When using a specific role (--role_name), access key and secret should not be specified.") + + success = remove_directory( + args.destination, + args.use_role, + args.role_name, + args.aws_access_key_id, + args.aws_secret_access_key, + args.endpoint_url, + args.region + ) + + if success: + print("Cleanup completed successfully.") + else: + print("Cleanup failed.") + exit(1) \ No newline at end of file diff --git a/docker-image-management/containers.conf b/docker-image-management/containers.conf new file mode 100644 index 0000000..19a2cdb --- /dev/null +++ b/docker-image-management/containers.conf @@ -0,0 +1,899 @@ +# The containers configuration file specifies all of the available configuration +# command-line options/flags for container engine tools like Podman & Buildah, +# but in a TOML format that can be easily modified and versioned. + +# Please refer to containers.conf(5) for details of all configuration options. +# Not all container engines implement all of the options. +# All of the options have hard coded defaults and these options will override +# the built in defaults. Users can then override these options via the command +# line. Container engines will read containers.conf files in up to three +# locations in the following order: +# 1. /usr/share/containers/containers.conf +# 2. /etc/containers/containers.conf +# 3. $XDG_CONFIG_HOME/containers/containers.conf or +# $HOME/.config/containers/containers.conf if $XDG_CONFIG_HOME is not set +# Items specified in the latter containers.conf, if they exist, override the +# previous containers.conf settings, or the default settings. + +[containers] + +# List of annotation. Specified as +# "key = value" +# If it is empty or commented out, no annotations will be added +# +#annotations = [] + +# Used to change the name of the default AppArmor profile of container engine. +# +#apparmor_profile = "container-default" + +# The hosts entries from the base hosts file are added to the containers hosts +# file. This must be either an absolute path or as special values "image" which +# uses the hosts file from the container image or "none" which means +# no base hosts file is used. The default is "" which will use /etc/hosts. +# +#base_hosts_file = "" + +# List of cgroup_conf entries specifying a list of cgroup files to write to and +# their values. For example `memory.high=1073741824` sets the +# memory.high limit to 1GB. +# cgroup_conf = [] + +# Default way to to create a cgroup namespace for the container +# Options are: +# `private` Create private Cgroup Namespace for the container. +# `host` Share host Cgroup Namespace with the container. +# +#cgroupns = "private" + +# Control container cgroup configuration +# Determines whether the container will create CGroups. +# Options are: +# `enabled` Enable cgroup support within container +# `disabled` Disable cgroup support, will inherit cgroups from parent +# `no-conmon` Do not create a cgroup dedicated to conmon. +# +#cgroups = "enabled" + +# List of default capabilities for containers. If it is empty or commented out, +# the default capabilities defined in the container engine will be added. +# +#default_capabilities = [ +# "CHOWN", +# "DAC_OVERRIDE", +# "FOWNER", +# "FSETID", +# "KILL", +# "NET_BIND_SERVICE", +# "SETFCAP", +# "SETGID", +# "SETPCAP", +# "SETUID", +# "SYS_CHROOT", +#] + +# A list of sysctls to be set in containers by default, +# specified as "name=value", +# for example:"net.ipv4.ping_group_range=0 0". +# +default_sysctls = [ + "net.ipv4.ping_group_range=0 0", +] + +# A list of ulimits to be set in containers by default, specified as +# "=:", for example: +# "nofile=1024:2048" +# See setrlimit(2) for a list of resource names. +# Any limit not specified here will be inherited from the process launching the +# container engine. +# Ulimits has limits for non privileged container engines. +# +#default_ulimits = [ +# "nofile=1280:2560", +#] + +# List of devices. Specified as +# "::", for example: +# "/dev/sdc:/dev/xvdc:rwm". +# If it is empty or commented out, only the default devices will be used +# +#devices = [] + +# List of default DNS options to be added to /etc/resolv.conf inside of the container. +# +#dns_options = [] + +# List of default DNS search domains to be added to /etc/resolv.conf inside of the container. +# +#dns_searches = [] + +# Set default DNS servers. +# This option can be used to override the DNS configuration passed to the +# container. The special value "none" can be specified to disable creation of +# /etc/resolv.conf in the container. +# The /etc/resolv.conf file in the image will be used without changes. +# +#dns_servers = [] + +# Environment variable list for the conmon process; used for passing necessary +# environment variables to conmon or the runtime. +# +#env = [ +# "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", +#] + +# Pass all host environment variables into the container. +# +#env_host = false + +# Set the ip for the host.containers.internal entry in the containers /etc/hosts +# file. This can be set to "none" to disable adding this entry. By default it +# will automatically choose the host ip. +# +# NOTE: When using podman machine this entry will never be added to the containers +# hosts file instead the gvproxy dns resolver will resolve this hostname. Therefore +# it is not possible to disable the entry in this case. +# +#host_containers_internal_ip = "" + +# Default proxy environment variables passed into the container. +# The environment variables passed in include: +# http_proxy, https_proxy, ftp_proxy, no_proxy, and the upper case versions of +# these. This option is needed when host system uses a proxy but container +# should not use proxy. Proxy environment variables specified for the container +# in any other way will override the values passed from the host. +# +#http_proxy = true + +# Run an init inside the container that forwards signals and reaps processes. +# +#init = false + +# Container init binary, if init=true, this is the init binary to be used for containers. +# If this option is not set catatonit is searched in the directories listed under +# the helper_binaries_dir option. It is recommended to just install catatonit +# there instead of configuring this option here. +# +#init_path = "/usr/libexec/podman/catatonit" + +# Default way to to create an IPC namespace (POSIX SysV IPC) for the container +# Options are: +# "host" Share host IPC Namespace with the container. +# "none" Create shareable IPC Namespace for the container without a private /dev/shm. +# "private" Create private IPC Namespace for the container, other containers are not allowed to share it. +# "shareable" Create shareable IPC Namespace for the container. +# +#ipcns = "shareable" + +# Default way to set an interface name inside container. Defaults to legacy +# pattern of ethX, where X is a integer, when left undefined. +# Options are: +# "device" Uses the network_interface name from the network config as interface name. +# Falls back to the ethX pattern if the network_interface is not set. +#interface_name = "" + +# keyring tells the container engine whether to create +# a kernel keyring for use within the container. +# +#keyring = true + +# label tells the container engine whether to use container separation using +# MAC(SELinux) labeling or not. +# The label flag is ignored on label disabled systems. +# +#label = true + +# label_users indicates whether to enforce confined users in containers on +# SELinux systems. This option causes containers to maintain the current user +# and role field of the calling process. By default SELinux containers run with +# the user system_u, and the role system_r. +#label_users = false + +# Logging driver for the container. Available options: k8s-file and journald. +# +#log_driver = "k8s-file" + +# Maximum size allowed for the container log file. Negative numbers indicate +# that no size limit is imposed. If positive, it must be >= 8192 to match or +# exceed conmon's read buffer. The file is truncated and re-opened so the +# limit is never exceeded. +# +#log_size_max = -1 + +# Specifies default format tag for container log messages. +# This is useful for creating a specific tag for container log messages. +# Containers logs default to truncated container ID as a tag. +# +#log_tag = "" + +# List of mounts. Specified as +# "type=TYPE,source=,destination=,", for example: +# "type=bind,source=/var/lib/foobar,destination=/var/lib/foobar,ro". +# If it is empty or commented out, no mounts will be added +# +#mounts = [] + +# Default way to to create a Network namespace for the container +# Options are: +# `private` Create private Network Namespace for the container. +# `host` Share host Network Namespace with the container. +# `none` Containers do not use the network +# +#netns = "private" + +# Create /etc/hosts for the container. By default, container engine manage +# /etc/hosts, automatically adding the container's own IP address. +# +#no_hosts = false + +# Tune the host's OOM preferences for containers +# (accepts values from -1000 to 1000). +#oom_score_adj = 0 + +# Default way to to create a PID namespace for the container +# Options are: +# `private` Create private PID Namespace for the container. +# `host` Share host PID Namespace with the container. +# +#pidns = "private" + +# Maximum number of processes allowed in a container. +# +#pids_limit = 2048 + +# Copy the content from the underlying image into the newly created volume +# when the container is created instead of when it is started. If false, +# the container engine will not copy the content until the container is started. +# Setting it to true may have negative performance implications. +# +#prepare_volume_on_create = false + +# Give extended privileges to all containers. A privileged container turns off +# the security features that isolate the container from the host. Dropped +# Capabilities, limited devices, read-only mount points, Apparmor/SELinux +# separation, and Seccomp filters are all disabled. Due to the disabled +# security features the privileged field should almost never be set as +# containers can easily break out of confinment. +# +# Containers running in a user namespace (e.g., rootless containers) cannot +# have more privileges than the user that launched them. +# +#privileged = false + +# Run all containers with root file system mounted read-only +# +# read_only = false + +# Path to the seccomp.json profile which is used as the default seccomp profile +# for the runtime. +# +#seccomp_profile = "/usr/share/containers/seccomp.json" + +# Size of /dev/shm. Specified as . +# Unit is optional, values: +# b (bytes), k (kilobytes), m (megabytes), or g (gigabytes). +# If the unit is omitted, the system uses bytes. +# +#shm_size = "65536k" + +# Set timezone in container. Takes IANA timezones as well as "local", +# which sets the timezone in the container to match the host machine. +# +#tz = "" + +# Set umask inside the container +# +#umask = "0022" + +# Default way to to create a User namespace for the container +# Options are: +# `auto` Create unique User Namespace for the container. +# `host` Share host User Namespace with the container. +# +#userns = "host" + +# Default way to to create a UTS namespace for the container +# Options are: +# `private` Create private UTS Namespace for the container. +# `host` Share host UTS Namespace with the container. +# +#utsns = "private" + +# List of volumes. Specified as +# "::", for example: +# "/db:/var/lib/db:ro". +# If it is empty or commented out, no volumes will be added +# +#volumes = [] + +#[engine.platform_to_oci_runtime] +#"wasi/wasm" = ["crun-wasm"] +#"wasi/wasm32" = ["crun-wasm"] +#"wasi/wasm64" = ["crun-wasm"] + +[secrets] +#driver = "file" + +[secrets.opts] +#root = "/example/directory" + +[network] + +# Network backend determines what network driver will be used to set up and tear down container networks. +# Valid values are "cni" and "netavark". +# The default value is empty which means that it will automatically choose CNI or netavark. If there are +# already containers/images or CNI networks preset it will choose CNI. +# +# Before changing this value all containers must be stopped otherwise it is likely that +# iptables rules and network interfaces might leak on the host. A reboot will fix this. +# +network_backend = "cni" + +# Path to directory where CNI plugin binaries are located. +# +#cni_plugin_dirs = [ +# "/usr/local/libexec/cni", +# "/usr/libexec/cni", +# "/usr/local/lib/cni", +# "/usr/lib/cni", +# "/opt/cni/bin", +#] + +# List of directories that will be searched for netavark plugins. +# +#netavark_plugin_dirs = [ +# "/usr/local/libexec/netavark", +# "/usr/libexec/netavark", +# "/usr/local/lib/netavark", +# "/usr/lib/netavark", +#] + +# The firewall driver to be used by netavark. +# The default is empty which means netavark will pick one accordingly. Current supported +# drivers are "iptables", "nftables", "none" (no firewall rules will be created) and "firewalld" (firewalld is +# experimental at the moment and not recommend outside of testing). +# +#firewall_driver = "" + + +# The network name of the default network to attach pods to. +# +#default_network = "podman" + +# The default subnet for the default network given in default_network. +# If a network with that name does not exist, a new network using that name and +# this subnet will be created. +# Must be a valid IPv4 CIDR prefix. +# +#default_subnet = "10.88.0.0/16" + +# DefaultSubnetPools is a list of subnets and size which are used to +# allocate subnets automatically for podman network create. +# It will iterate through the list and will pick the first free subnet +# with the given size. This is only used for ipv4 subnets, ipv6 subnets +# are always assigned randomly. +# +#default_subnet_pools = [ +# {"base" = "10.89.0.0/16", "size" = 24}, +# {"base" = "10.90.0.0/15", "size" = 24}, +# {"base" = "10.92.0.0/14", "size" = 24}, +# {"base" = "10.96.0.0/11", "size" = 24}, +# {"base" = "10.128.0.0/9", "size" = 24}, +#] + + + +# Configure which rootless network program to use by default. Valid options are +# `slirp4netns` and `pasta` (default). +# +#default_rootless_network_cmd = "pasta" + +# Path to the directory where network configuration files are located. +# For the CNI backend the default is "/etc/cni/net.d" as root +# and "$HOME/.config/cni/net.d" as rootless. +# For the netavark backend "/etc/containers/networks" is used as root +# and "$graphroot/networks" as rootless. +# +#network_config_dir = "/etc/cni/net.d/" + +# Port to use for dns forwarding daemon with netavark in rootful bridge +# mode and dns enabled. +# Using an alternate port might be useful if other dns services should +# run on the machine. +# +#dns_bind_port = 53 + +# A list of default pasta options that should be used running pasta. +# It accepts the pasta cli options, see pasta(1) for the full list of options. +# +#pasta_options = [] + +[engine] +# Index to the active service +# +#active_service = "production" + +#List of compression algorithms. If set makes sure that requested compression variant +#for each platform is added to the manifest list keeping original instance intact in +#the same manifest list on every `manifest push`. Supported values are (`gzip`, `zstd` and `zstd:chunked`). +# +#add_compression = ["gzip", "zstd", "zstd:chunked"] + +# Enforces using docker.io for completing short names in Podman's compatibility +# REST API. Note that this will ignore unqualified-search-registries and +# short-name aliases defined in containers-registries.conf(5). +#compat_api_enforce_docker_hub = true + +# Specify one or more external providers for the compose command. The first +# found provider is used for execution. Can be an absolute and relative path +# or a (file) name. +#compose_providers=[] + +# Emit logs on each invocation of the compose command indicating that an +# external compose provider is being executed. +#compose_warning_logs = true + +# The compression format to use when pushing an image. +# Valid options are: `gzip`, `zstd` and `zstd:chunked`. +# This field is ignored when pushing images to the docker-daemon and +# docker-archive formats. It is also ignored when the manifest format is set +# to v2s2. +# +#compression_format = "gzip" + +# The compression level to use when pushing an image. +# Valid options depend on the compression format used. +# For gzip, valid options are 1-9, with a default of 5. +# For zstd, valid options are 1-20, with a default of 3. +# +#compression_level = 5 + +# Cgroup management implementation used for the runtime. +# Valid options "systemd" or "cgroupfs" +# +#cgroup_manager = "systemd" + +# Environment variables to pass into conmon +# +#conmon_env_vars = [ +# "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +#] + +# Paths to look for the conmon container manager binary +# +#conmon_path = [ +# "/usr/libexec/podman/conmon", +# "/usr/local/libexec/podman/conmon", +# "/usr/local/lib/podman/conmon", +# "/usr/bin/conmon", +# "/usr/sbin/conmon", +# "/usr/local/bin/conmon", +# "/usr/local/sbin/conmon" +#] + +# Enforces using docker.io for completing short names in Podman's compatibility +# REST API. Note that this will ignore unqualified-search-registries and +# short-name aliases defined in containers-registries.conf(5). +#compat_api_enforce_docker_hub = true + +# The database backend of Podman. Supported values are "" (default), "boltdb" +# and "sqlite". An empty value means it will check whenever a boltdb already +# exists and use it when it does, otherwise it will use sqlite as default +# (e.g. new installs). This allows for backwards compatibility with older versions. +# Please run `podman-system-reset` prior to changing the database +# backend of an existing deployment, to make sure Podman can operate correctly. +# +#database_backend = "" + +# Specify the keys sequence used to detach a container. +# Format is a single character [a-Z] or a comma separated sequence of +# `ctrl-`, where `` is one of: +# `a-z`, `@`, `^`, `[`, `\`, `]`, `^` or `_` +# Specifying "" disables this feature. +#detach_keys = "ctrl-p,ctrl-q" + +# Determines whether engine will reserve ports on the host when they are +# forwarded to containers. When enabled, when ports are forwarded to containers, +# ports are held open by as long as the container is running, ensuring that +# they cannot be reused by other programs on the host. However, this can cause +# significant memory usage if a container has many ports forwarded to it. +# Disabling this can save memory. +# +#enable_port_reservation = true + +# Environment variables to be used when running the container engine (e.g., Podman, Buildah). +# For example "http_proxy=internal.proxy.company.com". +# Note these environment variables will not be used within the container. +# Set the env section under [containers] table, if you want to set environment variables for the container. +# +#env = [] + +# Define where event logs will be stored, when events_logger is "file". +#events_logfile_path="" + +# Sets the maximum size for events_logfile_path. +# The size can be b (bytes), k (kilobytes), m (megabytes), or g (gigabytes). +# The format for the size is ``, e.g., `1b` or `3g`. +# If no unit is included then the size will be read in bytes. +# When the limit is exceeded, the logfile will be rotated and the old one will be deleted. +# If the maximum size is set to 0, then no limit will be applied, +# and the logfile will not be rotated. +#events_logfile_max_size = "1m" + +# Selects which logging mechanism to use for container engine events. +# Valid values are `journald`, `file` and `none`. +# +#events_logger = "journald" + +# Creates a more verbose container-create event which includes a JSON payload +# with detailed information about the container. +#events_container_create_inspect_data = false + +# Whenever Podman should log healthcheck events. +# With many running healthcheck on short interval Podman will spam the event +# log a lot as it generates a event for each single healthcheck run. Because +# this event is optional and only useful to external consumers that may want +# to know when a healthcheck is run or failed allow users to turn it off by +# setting it to false. The default is true. +# +#healthcheck_events = true + +# A is a list of directories which are used to search for helper binaries. +# +#helper_binaries_dir = [ +# "/usr/local/libexec/podman", +# "/usr/local/lib/podman", +# "/usr/libexec/podman", +# "/usr/lib/podman", +#] + +# Path to OCI hooks directories for automatically executed hooks. +# +#hooks_dir = [ +# "/usr/share/containers/oci/hooks.d", +#] + +# Directories to scan for CDI Spec files. +# +#cdi_spec_dirs = [ +# "/etc/cdi", +#] + +# Manifest Type (oci, v2s2, or v2s1) to use when pulling, pushing, building +# container images. By default image pulled and pushed match the format of the +# source image. Building/committing defaults to OCI. +# +#image_default_format = "" + +# Default transport method for pulling and pushing for images +# +#image_default_transport = "docker://" + +# Maximum number of image layers to be copied (pulled/pushed) simultaneously. +# Not setting this field, or setting it to zero, will fall back to containers/image defaults. +# +#image_parallel_copies = 0 + +# Tells container engines how to handle the built-in image volumes. +# * anonymous: An anonymous named volume will be created and mounted +# into the container. +# * tmpfs: The volume is mounted onto the container as a tmpfs, +# which allows users to create content that disappears when +# the container is stopped. +# * ignore: All volumes are just ignored and no action is taken. +# +#image_volume_mode = "" + +# Default command to run the infra container +# +#infra_command = "/pause" + +# Infra (pause) container image name for pod infra containers. When running a +# pod, we start a `pause` process in a container to hold open the namespaces +# associated with the pod. This container does nothing other than sleep, +# reserving the pod's resources for the lifetime of the pod. By default container +# engines run a built-in container using the pause executable. If you want override +# specify an image to pull. +# +#infra_image = "" + +# Default Kubernetes kind/specification of the kubernetes yaml generated with the `podman kube generate` command. +# The possible options are `pod` and `deployment`. +#kube_generate_type = "pod" + +# Specify the locking mechanism to use; valid values are "shm" and "file". +# Change the default only if you are sure of what you are doing, in general +# "file" is useful only on platforms where cgo is not available for using the +# faster "shm" lock type. You may need to run "podman system renumber" after +# you change the lock type. +# +#lock_type = "shm" + +# MultiImageArchive - if true, the container engine allows for storing archives +# (e.g., of the docker-archive transport) with multiple images. By default, +# Podman creates single-image archives. +# +#multi_image_archive = false + +# Default engine namespace +# If engine is joined to a namespace, it will see only containers and pods +# that were created in the same namespace, and will create new containers and +# pods in that namespace. +# The default namespace is "", which corresponds to no namespace. When no +# namespace is set, all containers and pods are visible. +# +#namespace = "" + +# Path to the slirp4netns binary +# +#network_cmd_path = "" + +# Default options to pass to the slirp4netns binary. +# Valid options values are: +# +# - allow_host_loopback=true|false: Allow the slirp4netns to reach the host loopback IP (`10.0.2.2`). +# Default is false. +# - mtu=MTU: Specify the MTU to use for this network. (Default is `65520`). +# - cidr=CIDR: Specify ip range to use for this network. (Default is `10.0.2.0/24`). +# - enable_ipv6=true|false: Enable IPv6. Default is true. (Required for `outbound_addr6`). +# - outbound_addr=INTERFACE: Specify the outbound interface slirp should bind to (ipv4 traffic only). +# - outbound_addr=IPv4: Specify the outbound ipv4 address slirp should bind to. +# - outbound_addr6=INTERFACE: Specify the outbound interface slirp should bind to (ipv6 traffic only). +# - outbound_addr6=IPv6: Specify the outbound ipv6 address slirp should bind to. +# - port_handler=rootlesskit: Use rootlesskit for port forwarding. Default. +# Note: Rootlesskit changes the source IP address of incoming packets to a IP address in the container +# network namespace, usually `10.0.2.100`. If your application requires the real source IP address, +# e.g. web server logs, use the slirp4netns port handler. The rootlesskit port handler is also used for +# rootless containers when connected to user-defined networks. +# - port_handler=slirp4netns: Use the slirp4netns port forwarding, it is slower than rootlesskit but +# preserves the correct source IP address. This port handler cannot be used for user-defined networks. +# +#network_cmd_options = [] + +# Whether to use chroot instead of pivot_root in the runtime +# +#no_pivot_root = false + +# Number of locks available for containers, pods, and volumes. Each container, +# pod, and volume consumes 1 lock for as long as it exists. +# If this is changed, a lock renumber must be performed (e.g. with the +# 'podman system renumber' command). +# +#num_locks = 2048 + +# Set the exit policy of the pod when the last container exits. +#pod_exit_policy = "continue" + +# Whether to pull new image before running a container +# +#pull_policy = "missing" + +# Indicates whether the application should be running in remote mode. This flag modifies the +# --remote option on container engines. Setting the flag to true will default +# `podman --remote=true` for access to the remote Podman service. +# +#remote = false + +# Number of times to retry pulling/pushing images in case of failure +# +#retry = 3 + +# Delay between retries in case pulling/pushing image fails. +# If set, container engines will retry at the set interval, +# otherwise they delay 2 seconds and then exponentially back off. +# +#retry_delay = "2s" + +# Default OCI runtime +# +#runtime = "crun" + +# List of the OCI runtimes that support --format=json. When json is supported +# engine will use it for reporting nicer errors. +# +#runtime_supports_json = ["crun", "runc", "kata", "runsc", "youki", "krun"] + +# List of the OCI runtimes that supports running containers with KVM Separation. +# +#runtime_supports_kvm = ["kata", "krun"] + +# List of the OCI runtimes that supports running containers without cgroups. +# +#runtime_supports_nocgroups = ["crun", "krun"] + +# Default location for storing temporary container image content. Can be overridden with the TMPDIR environment +# variable. If you specify "storage", then the location of the +# container/storage tmp directory will be used. +# image_copy_tmp_dir="/var/tmp" + +# Number of seconds to wait without a connection +# before the `podman system service` times out and exits +# +#service_timeout = 5 + +# Directory for persistent engine files (database, etc) +# By default, this will be configured relative to where the containers/storage +# stores containers +# Uncomment to change location from this default +# +#static_dir = "/var/lib/containers/storage/libpod" + +# Number of seconds to wait for container to exit before sending kill signal. +# +#stop_timeout = 10 + +# Number of seconds to wait before exit command in API process is given to. +# This mimics Docker's exec cleanup behaviour, where the default is 5 minutes (value is in seconds). +# +#exit_command_delay = 300 + +# map of service destinations +# +# [engine.service_destinations] +# [engine.service_destinations.production] +# URI to access the Podman service +# Examples: +# rootless "unix:///run/user/$UID/podman/podman.sock" (Default) +# rootful "unix:///run/podman/podman.sock (Default) +# remote rootless ssh://engineering.lab.company.com/run/user/1000/podman/podman.sock +# remote rootful ssh://root@10.10.1.136:22/run/podman/podman.sock +# +# uri = "ssh://user@production.example.com/run/user/1001/podman/podman.sock" +# Path to file containing ssh identity key +# identity = "~/.ssh/id_rsa" + +# Directory for temporary files. Must be tmpfs (wiped after reboot) +# +#tmp_dir = "/run/libpod" + +# Directory for libpod named volumes. +# By default, this will be configured relative to where containers/storage +# stores containers. +# Uncomment to change location from this default. +# +#volume_path = "/var/lib/containers/storage/volumes" + +# Default timeout (in seconds) for volume plugin operations. +# Plugins are external programs accessed via a REST API; this sets a timeout +# for requests to that API. +# A value of 0 is treated as no timeout. +#volume_plugin_timeout = 5 + +# Paths to look for a valid OCI runtime (crun, runc, kata, runsc, krun, etc) +[engine.runtimes] +#crun = [ +# "/usr/bin/crun", +# "/usr/sbin/crun", +# "/usr/local/bin/crun", +# "/usr/local/sbin/crun", +# "/sbin/crun", +# "/bin/crun", +# "/run/current-system/sw/bin/crun", +#] + +#crun-vm = [ +# "/usr/bin/crun-vm", +# "/usr/local/bin/crun-vm", +# "/usr/local/sbin/crun-vm", +# "/sbin/crun-vm", +# "/bin/crun-vm", +# "/run/current-system/sw/bin/crun-vm", +#] + +#kata = [ +# "/usr/bin/kata-runtime", +# "/usr/sbin/kata-runtime", +# "/usr/local/bin/kata-runtime", +# "/usr/local/sbin/kata-runtime", +# "/sbin/kata-runtime", +# "/bin/kata-runtime", +# "/usr/bin/kata-qemu", +# "/usr/bin/kata-fc", +#] + +#runc = [ +# "/usr/bin/runc", +# "/usr/sbin/runc", +# "/usr/local/bin/runc", +# "/usr/local/sbin/runc", +# "/sbin/runc", +# "/bin/runc", +# "/usr/lib/cri-o-runc/sbin/runc", +#] + +#runsc = [ +# "/usr/bin/runsc", +# "/usr/sbin/runsc", +# "/usr/local/bin/runsc", +# "/usr/local/sbin/runsc", +# "/bin/runsc", +# "/sbin/runsc", +# "/run/current-system/sw/bin/runsc", +#] + +#youki = [ +# "/usr/local/bin/youki", +# "/usr/bin/youki", +# "/bin/youki", +# "/run/current-system/sw/bin/youki", +#] + +#krun = [ +# "/usr/bin/krun", +# "/usr/local/bin/krun", +#] + +[engine.volume_plugins] +#testplugin = "/run/podman/plugins/test.sock" + +[machine] +# Number of CPU's a machine is created with. +# +#cpus=1 + +# The size of the disk in GB created when init-ing a podman-machine VM. +# +#disk_size=10 + +# Default Image used when creating a new VM using `podman machine init`. +# Can be specified as registry with a bootable OCI artifact, download URL, or a local path. +# Registry target must be in the form of `docker://registry/repo/image:version`. +# Container engines translate URIs $OS and $ARCH to the native OS and ARCH. +# URI "https://example.com/$OS/$ARCH/foobar.ami" would become +# "https://example.com/linux/amd64/foobar.ami" on a Linux AMD machine. +# If unspecified, the default Podman machine image will be used. +# +#image = "" + +# Memory in MB a machine is created with. +# +#memory=2048 + +# The username to use and create on the podman machine OS for rootless +# container access. +# +#user = "core" + +# Host directories to be mounted as volumes into the VM by default. +# Environment variables like $HOME as well as complete paths are supported for +# the source and destination. An optional third field `:ro` can be used to +# tell the container engines to mount the volume readonly. +# +#volumes = [ +# "$HOME:$HOME", +#] + +# Virtualization provider used to run Podman machine. +# If it is empty or commented out, the default provider will be used. +# +#provider = "" + +# Rosetta supports running x86_64 Linux binaries on a Podman machine on Apple silicon. +# The default value is `true`. Supported on AppleHV(arm64) machines only. +# +#rosetta=true + +# The [machine] table MUST be the last entry in this file. +# (Unless another table is added) +# TOML does not provide a way to end a table other than a further table being +# defined, so every key hereafter will be part of [machine] and not the +# main config. + +[farms] +# +# the default farm to use when farming out builds +# default = "" +# +# map of existing farms +#[farms.list] + +[podmansh] +# Shell to spawn in container. Default: /bin/sh. +#shell = "/bin/sh" +# +# Name of the container the podmansh user should join. +#container = "podmansh" +# +# Default timeout in seconds for podmansh logins. +# Favored over the deprecated "podmansh_timeout" field. +#timeout = 30 \ No newline at end of file diff --git a/docker-image-management/export.py b/docker-image-management/export.py new file mode 100755 index 0000000..c9d67c9 --- /dev/null +++ b/docker-image-management/export.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +import os +import boto3 +import argparse +from botocore.exceptions import ClientError + +def get_s3_client(use_role=False, role_name=None, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None): + """ + Create and return an S3 client based on the provided authentication method, endpoint, and region. + """ + client_kwargs = {} + + if endpoint_url: + client_kwargs['endpoint_url'] = endpoint_url + elif region: + client_kwargs['region_name'] = region + + if use_role: + if role_name: + # Assume the specified role + sts_client = boto3.client('sts') + assumed_role_object = sts_client.assume_role( + RoleArn=f"arn:aws:iam::{boto3.client('sts').get_caller_identity()['Account']}:role/{role_name}", + RoleSessionName="AssumeRoleSession" + ) + credentials = assumed_role_object['Credentials'] + client_kwargs['aws_access_key_id'] = credentials['AccessKeyId'] + client_kwargs['aws_secret_access_key'] = credentials['SecretAccessKey'] + client_kwargs['aws_session_token'] = credentials['SessionToken'] + return boto3.client('s3', **client_kwargs) + elif aws_access_key_id and aws_secret_access_key: + client_kwargs['aws_access_key_id'] = aws_access_key_id + client_kwargs['aws_secret_access_key'] = aws_secret_access_key + return boto3.client('s3', **client_kwargs) + else: + raise ValueError("Either use_role must be True, or both aws_access_key_id and aws_secret_access_key must be provided") + +def transfer_file(source, destination, use_role=False, role_name=None, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None): + """ + Transfer a file from a local source to either a local destination or an S3 bucket + """ + if not os.path.isfile(source): + print(f"Error: Source file '{source}' does not exist or is not a file.") + return False + + if destination.startswith('s3://'): + # Uploading to S3 + s3_client = get_s3_client(use_role, role_name, aws_access_key_id, aws_secret_access_key, endpoint_url, region) + bucket, s3_key = parse_s3_path(destination) + try: + s3_client.upload_file(source, bucket, s3_key) + print(f"File {source} uploaded successfully to {destination}") + except ClientError as e: + print(f"Error uploading file: {str(e)}") + return False + else: + # Copying to local destination + try: + import shutil + # Create destination directory if it doesn't exist + os.makedirs(os.path.dirname(destination), exist_ok=True) + shutil.copy2(source, destination) + print(f"File {source} copied successfully to {destination}") + except IOError as e: + print(f"Error copying file: {str(e)}") + return False + return True + +def parse_s3_path(s3_path): + """ + Parse an S3 path into bucket and key + """ + parts = s3_path.replace('s3://', '').split('/', 1) + bucket = parts[0] + key = parts[1] if len(parts) > 1 else '' + return bucket, key + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Transfer a file from a local source to either a local destination or an S3 bucket.") + parser.add_argument("source", help="The local source file path") + parser.add_argument("destination", help="The destination file path (local) or S3 path (e.g., 's3://bucket/key')") + parser.add_argument("--use_role", action="store_true", help="Use IAM role for authentication") + parser.add_argument("--role_name", help="The name of the IAM role to assume") + parser.add_argument("--aws_access_key_id", help="AWS access key ID") + parser.add_argument("--aws_secret_access_key", help="AWS secret access key") + parser.add_argument("--endpoint_url", help="S3-compatible endpoint URL") + parser.add_argument("--region", help="AWS region (ignored if endpoint_url is specified)") + + args = parser.parse_args() + + if args.destination.startswith('s3://'): + if args.use_role and (args.aws_access_key_id or args.aws_secret_access_key or args.endpoint_url): + parser.error("When using IAM role (--use_role), access key, secret, and endpoint URL should not be specified.") + + if (args.aws_access_key_id or args.aws_secret_access_key) and not (args.aws_access_key_id and args.aws_secret_access_key): + parser.error("Both --aws_access_key_id and --aws_secret_access_key must be provided when using access key authentication.") + + if not args.use_role and not (args.aws_access_key_id and args.aws_secret_access_key): + parser.error("Either --use_role or both --aws_access_key_id and --aws_secret_access_key must be provided for S3 transfers.") + + if args.use_role and args.role_name and (args.aws_access_key_id or args.aws_secret_access_key): + parser.error("When using a specific role (--role_name), access key and secret should not be specified.") + + success = transfer_file( + args.source, + args.destination, + args.use_role, + args.role_name, + args.aws_access_key_id, + args.aws_secret_access_key, + args.endpoint_url, + args.region + ) + + if success: + print("Transfer completed successfully.") + else: + print("Transfer failed.") + exit(1) \ No newline at end of file diff --git a/docker-image-management/registries.conf b/docker-image-management/registries.conf new file mode 100644 index 0000000..1a76073 --- /dev/null +++ b/docker-image-management/registries.conf @@ -0,0 +1 @@ +unqualified-search-registries = ["docker.io"] \ No newline at end of file diff --git a/docker-image-management/requirements.txt b/docker-image-management/requirements.txt new file mode 100644 index 0000000..dd20b13 --- /dev/null +++ b/docker-image-management/requirements.txt @@ -0,0 +1,3 @@ +boto3 +botocore +jmespath \ No newline at end of file diff --git a/docker-image-management/storage.conf b/docker-image-management/storage.conf new file mode 100644 index 0000000..a4c9e93 --- /dev/null +++ b/docker-image-management/storage.conf @@ -0,0 +1,5 @@ +[storage] +driver = "overlay" + +[storage.options.overlay] +mount_program = "/usr/bin/fuse-overlayfs" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b514ab5 --- /dev/null +++ b/go.mod @@ -0,0 +1,99 @@ +module github.com/lukaszraczylo/kubernetes-images-sync-operator + +go 1.22.0 + +require ( + github.com/go-logr/logr v1.4.2 + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 + google.golang.org/grpc v1.65.0 + k8s.io/api v0.31.0 + k8s.io/apimachinery v0.31.0 + k8s.io/client-go v0.31.0 + k8s.io/cri-api v0.31.0 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + sigs.k8s.io/controller-runtime v0.19.0 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/cel-go v0.20.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect + k8s.io/apiserver v0.31.0 // indirect + k8s.io/component-base v0.31.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..254b861 --- /dev/null +++ b/go.sum @@ -0,0 +1,253 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY= +k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= +k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= +k8s.io/cri-api v0.31.0 h1:6o0XrhWlc1/zseGCh+aMScdXCg5nT6KCGdyx7HQkSKo= +k8s.io/cri-api v0.31.0/go.mod h1:Po3TMAYH/+KrZabi7QiwQI4a692oZcUOUThd/rqwxrI= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..ff72ff2 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/internal/controller/raczylo.com/clusterimage_controller.go b/internal/controller/raczylo.com/clusterimage_controller.go new file mode 100644 index 0000000..173c1e9 --- /dev/null +++ b/internal/controller/raczylo.com/clusterimage_controller.go @@ -0,0 +1,413 @@ +package raczylocom + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/go-logr/logr" + v1batch "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" + "raczylo.com/kubernetes-images-sync-operator/shared" +) + +type ClusterImageReconciler struct { + client.Client + Scheme *runtime.Scheme + MaxParallelJobs int + ActiveJobs int +} + +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimages,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimages/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimages/finalizers,verbs=update +// # additional RBAC rules - create and manage jobs +// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimageexports,verbs=get;list;watch;update;patch +func (r *ClusterImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + + clusterImage := &raczylocomv1.ClusterImage{} + if err := r.Get(ctx, req.NamespacedName, clusterImage); err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + l.Error(err, "unable to fetch ClusterImage") + return ctrl.Result{}, err + } + + clusterImageExport := &raczylocomv1.ClusterImageExport{} + if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Spec.ExportName, Namespace: clusterImage.Namespace}, clusterImageExport); err != nil { + l.Error(err, "unable to fetch ClusterImageExport") + return ctrl.Result{}, err + } + + r.MaxParallelJobs = clusterImageExport.Spec.MaxConcurrentJobs + + // If the ClusterImage is new, set its status to PENDING + if clusterImage.Status.Progress == "" { + clusterImage.Status.Progress = shared.STATUS_PENDING + if err := r.Status().Update(ctx, clusterImage); err != nil { + l.Error(err, "unable to update ClusterImage status to PENDING") + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + + // If we've reached the maximum number of parallel jobs, requeue + if r.ActiveJobs >= r.MaxParallelJobs && clusterImage.Status.Progress == shared.STATUS_PENDING { + return ctrl.Result{RequeueAfter: time.Second * 30}, nil + } + + // Process the ClusterImage based on its current status + switch clusterImage.Status.Progress { + case shared.STATUS_PENDING: + return r.handlePendingClusterImage(ctx, clusterImage, l) + case shared.STATUS_RUNNING, shared.STATUS_RETRYING: + return r.handleRunningClusterImage(ctx, clusterImage, l) + case shared.STATUS_SUCCESS, shared.STATUS_FAILED, shared.STATUS_PRESENT: + return ctrl.Result{}, nil // No further action needed + default: + l.Info("Unexpected ClusterImage status", "Status", clusterImage.Status.Progress) + return ctrl.Result{}, nil + } +} + +func (r *ClusterImageReconciler) handlePendingClusterImage(ctx context.Context, clusterImage *raczylocomv1.ClusterImage, l logr.Logger) (ctrl.Result, error) { + // Check if the image is present + exists, err := r.checkImageExists(ctx, clusterImage) + if err != nil { + l.Error(err, "unable to check if image exists") + return ctrl.Result{}, err + } + if exists { + clusterImage.Status.Progress = shared.STATUS_PRESENT + if err := r.Status().Update(ctx, clusterImage); err != nil { + l.Error(err, "unable to update ClusterImage status") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Fetch the associated ClusterImageExport + clusterImageExport := &raczylocomv1.ClusterImageExport{} + if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Spec.ExportName, Namespace: clusterImage.Namespace}, clusterImageExport); err != nil { + l.Error(err, "unable to fetch ClusterImageExport") + return ctrl.Result{}, err + } + + // Create the backup job + if err := r.createBackupJob(ctx, clusterImage, clusterImageExport, l); err != nil { + l.Error(err, "unable to create backup job") + return ctrl.Result{}, err + } + + // Update ClusterImage status to RUNNING + clusterImage.Status.Progress = shared.STATUS_RUNNING + if err := r.Status().Update(ctx, clusterImage); err != nil { + l.Error(err, "unable to update ClusterImage status to RUNNING") + return ctrl.Result{}, err + } + + // Increment the active jobs count + r.ActiveJobs++ + + return ctrl.Result{Requeue: true}, nil +} + +func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context, clusterImage *raczylocomv1.ClusterImage, l logr.Logger) (ctrl.Result, error) { + // Check for existing job for this ClusterImage + existingJob := &v1batch.Job{} + jobName := fmt.Sprintf("img-export-%s", clusterImage.Name) + err := r.Get(ctx, types.NamespacedName{Name: jobName, Namespace: clusterImage.Namespace}, existingJob) + + if err != nil { + if errors.IsNotFound(err) { + // Job doesn't exist, set status back to PENDING + clusterImage.Status.Progress = shared.STATUS_PENDING + if err := r.Status().Update(ctx, clusterImage); err != nil { + l.Error(err, "unable to update ClusterImage status back to PENDING") + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + l.Error(err, "unable to check for existing job") + return ctrl.Result{}, err + } + + // Check job status and update ClusterImage accordingly + if existingJob.Status.Succeeded > 0 { + clusterImage.Status.Progress = shared.STATUS_SUCCESS + r.ActiveJobs-- + } else if existingJob.Status.Failed > 0 { + if clusterImage.Status.RetryCount < 3 { + clusterImage.Status.Progress = shared.STATUS_RETRYING + clusterImage.Status.RetryCount++ + if err := r.Delete(ctx, existingJob); err != nil { + l.Error(err, "unable to delete failed job for retry") + return ctrl.Result{}, err + } + r.ActiveJobs-- + return ctrl.Result{Requeue: true}, nil + } else { + clusterImage.Status.Progress = shared.STATUS_FAILED + r.ActiveJobs-- + } + } + + if err := r.handleJobRestarts(ctx, existingJob, clusterImage); err != nil { + l.Error(err, "unable to handle job restarts") + return ctrl.Result{}, err + } + + // Update ClusterImage status + if err := r.Status().Update(ctx, clusterImage); err != nil { + l.Error(err, "unable to update ClusterImage status") + return ctrl.Result{}, err + } + + // Delete the completed job + if clusterImage.Status.Progress == shared.STATUS_SUCCESS || clusterImage.Status.Progress == shared.STATUS_FAILED { + if err := r.Delete(ctx, existingJob); err != nil && !errors.IsNotFound(err) { + l.Error(err, "unable to delete completed job") + return ctrl.Result{}, err + } + } + + l.Info("Reconciling ClusterImage completed", "Name", clusterImage.Name, "Status", clusterImage.Status.Progress) + + return r.updateClusterImageExportStatus(ctx, clusterImage) +} + +func (r *ClusterImageReconciler) cleanupJobPods(ctx context.Context, job *v1batch.Job) error { + podList := &v1.PodList{} + if err := r.List(ctx, podList, client.InNamespace(job.Namespace), client.MatchingLabels(job.Spec.Selector.MatchLabels)); err != nil { + return err + } + + for _, pod := range podList.Items { + if err := r.Delete(ctx, &pod); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} + +func (r *ClusterImageReconciler) createBackupJob(ctx context.Context, clusterImage *raczylocomv1.ClusterImage, clusterImageExport *raczylocomv1.ClusterImageExport, l logr.Logger) error { + normalisedImageName := shared.NormalizeImageName(clusterImage.Spec.FullName) + + defaultCommands := []string{ + "podman pull " + clusterImage.Spec.FullName, + "podman save --quiet -o /tmp/" + normalisedImageName + ".tar " + clusterImage.Spec.FullName, + } + + if clusterImage.Spec.Storage == shared.STORAGE_S3 { + s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3) + additionalCommands := []string{ + "./export.py " + strings.Join(s3Params, " ") + " '/tmp/" + normalisedImageName + ".tar' " + "'s3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImage.Spec.ExportPath + "/" + clusterImage.Spec.ExportName + "/" + normalisedImageName + ".tar'", + } + defaultCommands = append(defaultCommands, additionalCommands...) + } else if clusterImage.Spec.Storage == shared.STORAGE_FILE { + additionalCommands := []string{ + "./export.py /tmp/" + normalisedImageName + ".tar" + " " + clusterImage.Spec.ExportPath + "/" + clusterImage.Spec.ExportName + "/" + normalisedImageName + ".tar", + } + defaultCommands = append(defaultCommands, additionalCommands...) + } + defaultCommands = append(defaultCommands, "rm -f /tmp/"+normalisedImageName+".tar") + + jobParams := shared.JobParams{ + Name: fmt.Sprintf("img-export-%s", clusterImage.Name), + Namespace: clusterImage.Namespace, + Image: shared.BACKUP_JOB_IMAGE, + Commands: defaultCommands, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: clusterImage.APIVersion, + Kind: clusterImage.Kind, + Name: clusterImage.Name, + UID: clusterImage.UID, + BlockOwnerDeletion: pointer.Bool(true), + Controller: pointer.Bool(true), + }, + }, + } + + backupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil }) + + if err := r.Create(ctx, backupJob); err != nil { + return err + } + + clusterImage.Status.Progress = shared.STATUS_RUNNING + return r.Status().Update(ctx, clusterImage) +} + +func (r *ClusterImageReconciler) updateClusterImageExportStatus(ctx context.Context, clusterImage *raczylocomv1.ClusterImage) (ctrl.Result, error) { + l := log.FromContext(ctx) + + clusterImageExport := &raczylocomv1.ClusterImageExport{} + if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Spec.ExportName, Namespace: clusterImage.Namespace}, clusterImageExport); err != nil { + l.Error(err, "unable to fetch ClusterImageExport") + return ctrl.Result{}, err + } + + clusterImageList := &raczylocomv1.ClusterImageList{} + if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImage.Namespace), client.MatchingFields{"spec.exportName": clusterImage.Spec.ExportName}); err != nil { + l.Error(err, "unable to list ClusterImages") + return ctrl.Result{}, err + } + + allCompleted := true + anyFailed := false + anyRunning := false + + for _, ci := range clusterImageList.Items { + switch ci.Status.Progress { + case shared.STATUS_SUCCESS, shared.STATUS_PRESENT: + // These statuses are considered completed + case shared.STATUS_FAILED: + anyFailed = true + allCompleted = false + case shared.STATUS_RUNNING, shared.STATUS_RETRYING: + allCompleted = false + anyRunning = true + case shared.STATUS_PENDING: + allCompleted = false + } + } + + var newStatus string + if allCompleted { + newStatus = shared.STATUS_SUCCESS + } else if anyFailed { + newStatus = shared.STATUS_FAILED + } else if anyRunning { + newStatus = shared.STATUS_RUNNING + } else { + newStatus = shared.STATUS_PENDING + } + + if clusterImageExport.Status.Progress != newStatus { + clusterImageExport.Status.Progress = newStatus + if err := r.Status().Update(ctx, clusterImageExport); err != nil { + l.Error(err, "unable to update ClusterImageExport status") + return ctrl.Result{}, err + } + l.Info("Updated ClusterImageExport status", "ExportName", clusterImageExport.Name, "NewStatus", newStatus) + } + + // If there are still pending or running images, requeue + if !allCompleted { + return ctrl.Result{Requeue: true}, nil + } + + return ctrl.Result{}, nil +} + +func (r *ClusterImageReconciler) handleJobRestarts(ctx context.Context, job *v1batch.Job, clusterImage *raczylocomv1.ClusterImage) error { + podList := &v1.PodList{} + if err := r.List(ctx, podList, client.InNamespace(job.Namespace), client.MatchingLabels(job.Spec.Selector.MatchLabels)); err != nil { + return err + } + + for _, pod := range podList.Items { + for _, containerStatus := range pod.Status.ContainerStatuses { + if containerStatus.RestartCount > 0 { + clusterImage.Status.RetryCount += int(containerStatus.RestartCount) + if clusterImage.Status.RetryCount >= 3 { + clusterImage.Status.Progress = shared.STATUS_FAILED + if err := r.Status().Update(ctx, clusterImage); err != nil { + return err + } + return r.removeAllJobsAndContainers(ctx, clusterImage.Namespace) + } else { + clusterImage.Status.Progress = shared.STATUS_RETRYING + } + + if err := r.Status().Update(ctx, clusterImage); err != nil { + return err + } + return nil + } + } + } + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ClusterImageReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&raczylocomv1.ClusterImage{}). + Owns(&v1batch.Job{}). + Complete(r) +} + +func (r *ClusterImageReconciler) removeAllJobsAndContainers(ctx context.Context, namespace string) error { + jobList := &v1batch.JobList{} + if err := r.List(ctx, jobList, client.InNamespace(namespace), client.MatchingLabels{"app": "image-export"}); err != nil { + return err + } + + for _, job := range jobList.Items { + if err := r.Delete(ctx, &job, client.PropagationPolicy(metav1.DeletePropagationForeground)); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} + +func (r *ClusterImageReconciler) checkImageExists(ctx context.Context, clusterImage *raczylocomv1.ClusterImage) (bool, error) { + clusterImageList := &raczylocomv1.ClusterImageList{} + if err := r.List(ctx, clusterImageList); err != nil { + return false, err + } + + for _, ci := range clusterImageList.Items { + if ci.Spec.FullName == clusterImage.Spec.FullName && ci.Name != clusterImage.Name { + if ci.Status.Progress == shared.STATUS_SUCCESS || ci.Status.Progress == shared.STATUS_PRESENT || ci.Status.Progress == shared.STATUS_RUNNING { + return true, nil + } + } + } + + // Check if the image is already in the COMPLETED state + if clusterImage.Status.Progress == shared.STATUS_SUCCESS { + return true, nil + } + + return false, nil +} + +func (r *ClusterImageReconciler) isJobStarted(ctx context.Context, job *v1batch.Job) (bool, error) { + podList := &v1.PodList{} + if err := r.List(ctx, podList, client.InNamespace(job.Namespace), client.MatchingLabels(job.Spec.Selector.MatchLabels)); err != nil { + return false, err + } + + for _, pod := range podList.Items { + if pod.Status.Phase == v1.PodRunning { + return true, nil + } + } + + return false, nil +} + +func (r *ClusterImageReconciler) hasJobTimedOut(job *v1batch.Job) bool { + // Check if the job has been running for more than 5 minutes without starting + return time.Since(job.CreationTimestamp.Time) > 5*time.Minute +} diff --git a/internal/controller/raczylo.com/clusterimage_controller_test.go b/internal/controller/raczylo.com/clusterimage_controller_test.go new file mode 100644 index 0000000..03be91c --- /dev/null +++ b/internal/controller/raczylo.com/clusterimage_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package raczylocom + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" +) + +var _ = Describe("ClusterImage Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + clusterimage := &raczylocomv1.ClusterImage{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ClusterImage") + err := k8sClient.Get(ctx, typeNamespacedName, clusterimage) + if err != nil && errors.IsNotFound(err) { + resource := &raczylocomv1.ClusterImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &raczylocomv1.ClusterImage{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ClusterImage") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ClusterImageReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/raczylo.com/clusterimageexport_controller.go b/internal/controller/raczylo.com/clusterimageexport_controller.go new file mode 100644 index 0000000..52d5c4f --- /dev/null +++ b/internal/controller/raczylo.com/clusterimageexport_controller.go @@ -0,0 +1,320 @@ +package raczylocom + +import ( + "context" + "crypto/md5" + "fmt" + "strings" + "time" + + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + v1batch "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" + shared "raczylo.com/kubernetes-images-sync-operator/shared" +) + +// ClusterImageExportReconciler reconciles a ClusterImageExport object +type ClusterImageExportReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimageexports,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimageexports/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimageexports/finalizers,verbs=update +// additional RBAC rules +// +kubebuilder:rbac:groups=raczylo.com,resources=clusterimages,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch +// +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch +// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch + +const clusterImageExportFinalizer = "finalizer.clusterimageexport.raczylo.com" + +func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + l.Info("Reconciling ClusterImageExport") + + // Fetch the ClusterImageExport instance + clusterImageExport := &raczylocomv1.ClusterImageExport{} + if err := r.Get(ctx, req.NamespacedName, clusterImageExport); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !clusterImageExport.ObjectMeta.DeletionTimestamp.IsZero() { + return r.handleDeletion(ctx, clusterImageExport) + } + + // Add finalizer if it doesn't exist + if !controllerutil.ContainsFinalizer(clusterImageExport, clusterImageExportFinalizer) { + controllerutil.AddFinalizer(clusterImageExport, clusterImageExportFinalizer) + if err := r.Update(ctx, clusterImageExport); err != nil { + return ctrl.Result{}, err + } + } + + // Early return if the ClusterImageExport is already in a completed state + if clusterImageExport.Status.Progress == shared.STATUS_SUCCESS || clusterImageExport.Status.Progress == shared.STATUS_FAILED { + l.Info("ClusterImageExport is already in a completed state", "Status", clusterImageExport.Status.Progress) + return ctrl.Result{}, nil + } + + // If the status is empty, set it to PENDING + if clusterImageExport.Status.Progress == "" { + clusterImageExport.Status.Progress = shared.STATUS_PENDING + if err := r.Status().Update(ctx, clusterImageExport); err != nil { + l.Error(err, "unable to update ClusterImageExport status") + return ctrl.Result{}, err + } + } + + // Proceed with the rest of the reconciliation logic + fullImagesList, err := r.listImagesInCluster(ctx, l, clusterImageExport) + if err != nil { + l.Error(err, "unable to list images in the cluster") + return ctrl.Result{}, err + } + + clusterImageExport.Status.Progress = shared.STATUS_RUNNING + if err := r.Status().Update(ctx, clusterImageExport); err != nil { + l.Error(err, "unable to update ClusterImageExport status to RUNNING") + return ctrl.Result{}, err + } + + for _, image := range fullImagesList.Containers { + nameHash := fmt.Sprintf("%x", md5.Sum([]byte(clusterImageExport.Name+image.Image+image.Tag+image.Sha)))[:14] + + // Check if the ClusterImage already exists + clusterImage := &raczylocomv1.ClusterImage{} + err := r.Get(ctx, client.ObjectKey{Namespace: clusterImageExport.Namespace, Name: nameHash}, clusterImage) + if err == nil { + // ClusterImage exists, check its status + if clusterImage.Status.Progress == shared.STATUS_FAILED { + clusterImageExport.Status.Progress = shared.STATUS_FAILED + if err := r.Status().Update(ctx, clusterImageExport); err != nil { + l.Error(err, "unable to update ClusterImageExport status to FAILED") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + continue + } else if !errors.IsNotFound(err) { + l.Error(err, "unable to get ClusterImage") + return ctrl.Result{}, err + } + + // Create a new ClusterImage + newClusterImage := &raczylocomv1.ClusterImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: nameHash, + Namespace: clusterImageExport.Namespace, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: clusterImageExport.APIVersion, + Kind: clusterImageExport.Kind, + Name: clusterImageExport.Name, + UID: clusterImageExport.UID, + Controller: pointer.Bool(true), + }, + }, + }, + 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, + }, + } + + if err := r.Create(ctx, newClusterImage); err != nil { + l.Error(err, "unable to create ClusterImage", "image", image) + return ctrl.Result{}, err + } + } + + // Check if all ClusterImages are completed + allCompleted, err := r.checkAllClusterImagesCompleted(ctx, clusterImageExport) + if err != nil { + l.Error(err, "unable to check ClusterImages status") + return ctrl.Result{}, err + } + + if allCompleted { + clusterImageExport.Status.Progress = shared.STATUS_SUCCESS + if err := r.Status().Update(ctx, clusterImageExport); err != nil { + l.Error(err, "unable to update ClusterImageExport status to SUCCESS") + return ctrl.Result{}, err + } + } + + return ctrl.Result{Requeue: !allCompleted}, nil +} + +func (r *ClusterImageExportReconciler) checkAllClusterImagesCompleted(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) (bool, error) { + clusterImageList := &raczylocomv1.ClusterImageList{} + if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace), client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil { + return false, err + } + + for _, ci := range clusterImageList.Items { + if ci.Status.Progress != shared.STATUS_SUCCESS && ci.Status.Progress != shared.STATUS_PRESENT { + return false, nil + } + } + + return true, nil +} + +// SetupWithManager sets up the controller with the Manager. + +func (r *ClusterImageExportReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&raczylocomv1.ClusterImageExport{}). + Owns(&raczylocomv1.ClusterImage{}). + Complete(r) +} + +func (r *ClusterImageExportReconciler) listImagesInCluster(ctx context.Context, l logr.Logger, clusterImageExport *raczylocomv1.ClusterImageExport) (shared.ContainersList, error) { + containersList := shared.ContainersList{} + if err := shared.ListAndProcessResources[*shared.DeploymentWrapper](ctx, r.Client, &appsv1.DeploymentList{}, &containersList); err != nil { + return shared.ContainersList{}, err + } + if err := shared.ListAndProcessResources[*shared.JobWrapper](ctx, r.Client, &batchv1.JobList{}, &containersList); err != nil { + return shared.ContainersList{}, err + } + if err := shared.ListAndProcessResources[*shared.DaemonSetWrapper](ctx, r.Client, &appsv1.DaemonSetList{}, &containersList); err != nil { + return shared.ContainersList{}, err + } + if err := shared.ListAndProcessResources[*shared.CronJobWrapper](ctx, r.Client, &batchv1.CronJobList{}, &containersList); err != nil { + return shared.ContainersList{}, err + } + + if len(clusterImageExport.Spec.Includes) > 0 { + containersList = shared.IncludeOnlyImages(containersList, clusterImageExport.Spec.Includes) + } + + if len(clusterImageExport.Spec.Excludes) > 0 { + containersList = shared.RemoveExcludedImages(containersList, clusterImageExport.Spec.Excludes) + } + + containersList = shared.RemoveDuplicates(containersList) + l.Info("List of containers in the cluster", "containers", containersList) + + return containersList, nil +} + +func (r *ClusterImageExportReconciler) handleDeletion(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) (ctrl.Result, error) { + l := log.FromContext(ctx) + + if controllerutil.ContainsFinalizer(clusterImageExport, clusterImageExportFinalizer) { + // Run the cleanup job + if err := r.runCleanupJob(ctx, clusterImageExport); err != nil { + l.Error(err, "Failed to run cleanup job") + return ctrl.Result{}, err + } + + // Remove the finalizer + controllerutil.RemoveFinalizer(clusterImageExport, clusterImageExportFinalizer) + if err := r.Update(ctx, clusterImageExport); err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} + +func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) error { + l := log.FromContext(ctx) + + normalisedImageName := "cleanup-" + shared.NormalizeImageName(clusterImageExport.Name) + + defaultCommands := []string{} + + if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_S3 { + s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3) + additionalCommands := []string{ + "./cleanup.py " + strings.Join(s3Params, " ") + " 's3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.ObjectMeta.Name + "/'", + } + defaultCommands = append(defaultCommands, additionalCommands...) + } else if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_FILE { + additionalCommands := []string{ + "./cleanup.py" + "'" + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.ObjectMeta.Name + "/'", + } + defaultCommands = append(defaultCommands, additionalCommands...) + } + + jobParams := shared.JobParams{ + Name: normalisedImageName, + Namespace: clusterImageExport.Namespace, + Image: shared.BACKUP_JOB_IMAGE, + Commands: defaultCommands, + } + + cleanupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil }) + + if err := r.Create(ctx, cleanupJob); err != nil { + l.Error(err, "Failed to create cleanup job") + return err + } + + l.Info("Created cleanup job") + + go func() { + if err := r.waitForJobCompletionAndDelete(ctx, cleanupJob); err != nil { + l.Error(err, "Failed to wait for job completion and delete") + } + }() + return nil +} + +func (r *ClusterImageExportReconciler) waitForJobCompletionAndDelete(ctx context.Context, job *v1batch.Job) error { + l := log.FromContext(ctx) + key := client.ObjectKeyFromObject(job) + + // Wait for the job to complete + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if err := r.Get(ctx, key, job); err != nil { + return err + } + + if job.Status.Succeeded > 0 { + // Job completed successfully, delete it + l.Info("Cleanup job completed, deleting", "job", job.Name) + if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { + return err + } + return nil + } + + if job.Status.Failed > 0 { + // Job failed, log the error but still delete the job + l.Error(nil, "Cleanup job failed", "job", job.Name) + if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { + return err + } + return fmt.Errorf("cleanup job failed: %s", job.Name) + } + + // Job still running, wait and check again + time.Sleep(5 * time.Second) + } + } +} diff --git a/internal/controller/raczylo.com/clusterimageexport_controller_test.go b/internal/controller/raczylo.com/clusterimageexport_controller_test.go new file mode 100644 index 0000000..1b38cfc --- /dev/null +++ b/internal/controller/raczylo.com/clusterimageexport_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package raczylocom + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" +) + +var _ = Describe("ClusterImageExport Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + clusterimageexport := &raczylocomv1.ClusterImageExport{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ClusterImageExport") + err := k8sClient.Get(ctx, typeNamespacedName, clusterimageexport) + if err != nil && errors.IsNotFound(err) { + resource := &raczylocomv1.ClusterImageExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &raczylocomv1.ClusterImageExport{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ClusterImageExport") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ClusterImageExportReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/raczylo.com/suite_test.go b/internal/controller/raczylo.com/suite_test.go new file mode 100644 index 0000000..f69e781 --- /dev/null +++ b/internal/controller/raczylo.com/suite_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package raczylocom + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = raczylocomv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/semver.yaml b/semver.yaml new file mode 100644 index 0000000..08181d8 --- /dev/null +++ b/semver.yaml @@ -0,0 +1,5 @@ +version: 1 +force: + minor: 1 + existing: false + strict: false diff --git a/shared/definitions.go b/shared/definitions.go new file mode 100644 index 0000000..e4a8f6c --- /dev/null +++ b/shared/definitions.go @@ -0,0 +1,98 @@ +package shared + +import ( + "regexp" + "strings" +) + +const ( + // JOB IMAGES + BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/docker-image-management:v0.0.6" + + // AVAILABLE STATUSES + STATUS_PENDING = "PENDING" + STATUS_STARTING = "STARTING" + STATUS_RETRYING = "RETRYING" + STATUS_RUNNING = "RUNNING" + STATUS_FAILED = "FAILED" + STATUS_SUCCESS = "COMPLETED" + STATUS_PRESENT = "PRESENT" + + // STORAGE DEFINITIONS + STORAGE_S3 = "S3" + STORAGE_FILE = "FILE" +) + +type Container struct { + Image string `json:"image"` + Tag string `json:"tag"` + Sha string `json:"sha"` + FullName string `json:"fullName"` +} + +type ContainersList struct { + Containers []Container `json:"containers"` +} + +func RemoveDuplicates(containersList ContainersList) ContainersList { + // remove duplicates from the list + encountered := map[Container]bool{} + result := ContainersList{} + for v := range containersList.Containers { + if !encountered[containersList.Containers[v]] { + encountered[containersList.Containers[v]] = true + result.Containers = append(result.Containers, containersList.Containers[v]) + } + } + return result +} + +func RemoveExcludedImages(containers ContainersList, excludes []string) ContainersList { + // remove excluded images from the list + result := ContainersList{} + for _, container := range containers.Containers { + excluded := false + for _, exclude := range excludes { + if strings.Contains(strings.ToLower(container.Image), strings.ToLower(exclude)) { + excluded = true + break + } + } + if !excluded { + result.Containers = append(result.Containers, container) + } + } + return result +} + +func IncludeOnlyImages(containers ContainersList, includes []string) ContainersList { + // include only images from the list + result := ContainersList{} + for _, container := range containers.Containers { + included := false + for _, include := range includes { + if strings.Contains(strings.ToLower(container.Image), strings.ToLower(include)) { + included = true + break + } + } + if included { + result.Containers = append(result.Containers, container) + } + } + return result +} + +var imageNameRegexp = regexp.MustCompile(`[/:@&=+$,\?%\{\}\[\]\\^~#\s]`) +var imageNameRegexpReplace = regexp.MustCompile(`-+`) + +func NormalizeImageName(name string) string { + // Replace special characters with hyphens + normalized := imageNameRegexp.ReplaceAllString(name, "-") + + // Remove consecutive hyphens + normalized = imageNameRegexpReplace.ReplaceAllString(normalized, "-") + + // Trim leading and trailing hyphens + return strings.Trim(normalized, "-") +} diff --git a/shared/jobs.go b/shared/jobs.go new file mode 100644 index 0000000..f1b8620 --- /dev/null +++ b/shared/jobs.go @@ -0,0 +1,82 @@ +package shared + +import ( + "fmt" + "strings" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" +) + +type JobParams struct { + Name string + Namespace string + Image string + Commands []string + EnvVars []corev1.EnvVar + OwnerReferences []metav1.OwnerReference +} + +func CreateJob[T any](params JobParams, setupFunc func(T) []string) *batchv1.Job { + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: params.Name, + Namespace: params.Namespace, + OwnerReferences: params.OwnerReferences, + Labels: map[string]string{ + "app": "image-export", + }, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "image-export", + }, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + Containers: []corev1.Container{ + { + Name: "export", + Image: params.Image, + TTY: true, + Command: []string{ + "bash", + "-c", + strings.Join(params.Commands, " && "), + }, + Env: params.EnvVars, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + }, + }, + }, + } +} + +func SetupS3Params(s3Config raczylocomv1.ClusterImageStorageS3) []string { + params := []string{} + if s3Config.UseRole { + params = append(params, "--use-role") + } else { + params = append(params, fmt.Sprintf("--aws_access_key_id='%s'", s3Config.AccessKey)) + params = append(params, fmt.Sprintf("--aws_secret_access_key='%s'", s3Config.SecretKey)) + } + if s3Config.RoleARN != "" { + params = append(params, fmt.Sprintf("--role_name='%s'", s3Config.RoleARN)) + } + if s3Config.Endpoint != "" { + params = append(params, fmt.Sprintf("--endpoint_url='%s'", s3Config.Endpoint)) + } + if s3Config.Region != "" { + params = append(params, fmt.Sprintf("--region=%s", s3Config.Region)) + } + return params +} diff --git a/shared/k8s.go b/shared/k8s.go new file mode 100644 index 0000000..aef9c68 --- /dev/null +++ b/shared/k8s.go @@ -0,0 +1,142 @@ +package shared + +import ( + "context" + "fmt" + "strings" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + raczylocomv1 "raczylo.com/kubernetes-images-sync-operator/api/raczylo.com/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type K8sResource interface { + GetPodSpec() *corev1.PodSpec +} + +// Wrapper types +type DeploymentWrapper appsv1.Deployment +type JobWrapper batchv1.Job +type DaemonSetWrapper appsv1.DaemonSet +type CronJobWrapper batchv1.CronJob + +// Implement the K8sResource interface for wrapper types +func (d *DeploymentWrapper) GetPodSpec() *corev1.PodSpec { return &d.Spec.Template.Spec } +func (j *JobWrapper) GetPodSpec() *corev1.PodSpec { return &j.Spec.Template.Spec } +func (ds *DaemonSetWrapper) GetPodSpec() *corev1.PodSpec { return &ds.Spec.Template.Spec } +func (cj *CronJobWrapper) GetPodSpec() *corev1.PodSpec { + return &cj.Spec.JobTemplate.Spec.Template.Spec +} + +func processContainerName(containerName string) (Container, error) { + cnt := Container{} + parts := strings.Split(containerName, "@") + if len(parts) > 2 { + return cnt, fmt.Errorf("invalid container name format: %s", containerName) + } + imageAndTag := strings.Split(parts[0], ":") + cnt.Image = imageAndTag[0] + if len(imageAndTag) > 2 { + return cnt, fmt.Errorf("invalid image:tag format: %s", parts[0]) + } + if len(imageAndTag) == 2 { + cnt.Tag = imageAndTag[1] + } + if len(parts) == 2 { + shaParts := strings.SplitN(parts[1], ":", 2) + if len(shaParts) != 2 || (shaParts[0] != "sha" && shaParts[0] != "sha256") { + return cnt, fmt.Errorf("invalid SHA format: %s", parts[1]) + } + cnt.Sha = parts[1] + } + cnt.FullName = containerName + // if tag is empty and sha is empty - use tag 'latest' + if cnt.Sha == "" && cnt.Tag == "" { + cnt.Tag = "latest" + } + + if cnt.Image == "" { + return cnt, fmt.Errorf("image name is required") + } + return cnt, nil +} + +func processContainers[T K8sResource](resource T, containersList *ContainersList) error { + podSpec := resource.GetPodSpec() + if podSpec == nil { + return fmt.Errorf("nil PodSpec") + } + + allContainers := append(podSpec.Containers, podSpec.InitContainers...) + for _, container := range allContainers { + if err := processContainer(container.Image, containersList); err != nil { + return err + } + } + + for _, container := range podSpec.EphemeralContainers { + if err := processContainer(container.EphemeralContainerCommon.Image, containersList); err != nil { + return err + } + } + + return nil +} + +// processContainer handles the processing of a single container image +func processContainer(image string, containersList *ContainersList) error { + cnt, err := processContainerName(image) + if err != nil { + return fmt.Errorf("failed to process container name: %s - %w", image, err) + } + containersList.Containers = append(containersList.Containers, cnt) + return nil +} + +// listAndProcessResources is a generic function to list and process K8s resources +func ListAndProcessResources[T K8sResource, L client.ObjectList](ctx context.Context, r client.Client, list L, containersList *ContainersList) error { + if err := r.List(ctx, list, &client.ListOptions{}); err != nil { + return fmt.Errorf("failed to list resources: %w", err) + } + + switch typedList := any(list).(type) { + case *appsv1.DeploymentList: + for i := range typedList.Items { + if err := processContainers((*DeploymentWrapper)(&typedList.Items[i]), containersList); err != nil { + return err + } + } + case *batchv1.JobList: + for i := range typedList.Items { + if err := processContainers((*JobWrapper)(&typedList.Items[i]), containersList); err != nil { + return err + } + } + case *appsv1.DaemonSetList: + for i := range typedList.Items { + if err := processContainers((*DaemonSetWrapper)(&typedList.Items[i]), containersList); err != nil { + return err + } + } + case *batchv1.CronJobList: + for i := range typedList.Items { + if err := processContainers((*CronJobWrapper)(&typedList.Items[i]), containersList); err != nil { + return err + } + } + default: + return fmt.Errorf("unsupported list type: %T", list) + } + + return nil +} + +func SetupIndexers(mgr manager.Manager) error { + return mgr.GetFieldIndexer().IndexField(context.Background(), &raczylocomv1.ClusterImage{}, "spec.exportName", func(rawObj client.Object) []string { + clusterImage := rawObj.(*raczylocomv1.ClusterImage) + return []string{clusterImage.Spec.ExportName} + }) +} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go new file mode 100644 index 0000000..0ac390e --- /dev/null +++ b/test/e2e/e2e_suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + _, _ = fmt.Fprintf(GinkgoWriter, "Starting kubernetes-images-sync-operator suite\n") + RunSpecs(t, "e2e suite") +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 0000000..d9cffaa --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "os/exec" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "raczylo.com/kubernetes-images-sync-operator/test/utils" +) + +const namespace = "kubernetes-images-sync-operator-system" + +var _ = Describe("controller", Ordered, func() { + BeforeAll(func() { + By("installing prometheus operator") + Expect(utils.InstallPrometheusOperator()).To(Succeed()) + + By("installing the cert-manager") + Expect(utils.InstallCertManager()).To(Succeed()) + + By("creating manager namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + AfterAll(func() { + By("uninstalling the Prometheus manager bundle") + utils.UninstallPrometheusOperator() + + By("uninstalling the cert-manager bundle") + utils.UninstallCertManager() + + By("removing manager namespace") + cmd := exec.Command("kubectl", "delete", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + Context("Operator", func() { + It("should run successfully", func() { + var controllerPodName string + var err error + + // projectimage stores the name of the image used in the example + var projectimage = "example.com/kubernetes-images-sync-operator:v0.0.1" + + By("building the manager(Operator) image") + cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("loading the the manager(Operator) image on Kind") + err = utils.LoadImageToKindClusterWithName(projectimage) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("installing CRDs") + cmd = exec.Command("make", "install") + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("deploying the cm-raczylo-com") + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the cm-raczylo-com pod is running as expected") + verifyControllerUp := func() error { + // Get pod name + + cmd = exec.Command("kubectl", "get", + "pods", "-l", "control-plane=cm-raczylo-com", + "-o", "go-template={{ range .items }}"+ + "{{ if not .metadata.deletionTimestamp }}"+ + "{{ .metadata.name }}"+ + "{{ \"\\n\" }}{{ end }}{{ end }}", + "-n", namespace, + ) + + podOutput, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + podNames := utils.GetNonEmptyLines(string(podOutput)) + if len(podNames) != 1 { + return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) + } + controllerPodName = podNames[0] + ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("cm-raczylo-com")) + + // Validate pod status + cmd = exec.Command("kubectl", "get", + "pods", controllerPodName, "-o", "jsonpath={.status.phase}", + "-n", namespace, + ) + status, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if string(status) != "Running" { + return fmt.Errorf("controller pod in %s status", status) + } + return nil + } + EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) + + }) + }) +}) diff --git a/test/utils/utils.go b/test/utils/utils.go new file mode 100644 index 0000000..6b96ab5 --- /dev/null +++ b/test/utils/utils.go @@ -0,0 +1,140 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "os" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo/v2" //nolint:golint,revive +) + +const ( + prometheusOperatorVersion = "v0.72.0" + prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + + "releases/download/%s/bundle.yaml" + + certmanagerVersion = "v1.14.4" + certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" +) + +func warnError(err error) { + _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) +} + +// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. +func InstallPrometheusOperator() error { + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "create", "-f", url) + _, err := Run(cmd) + return err +} + +// Run executes the provided command within this context +func Run(cmd *exec.Cmd) ([]byte, error) { + dir, _ := GetProjectDir() + cmd.Dir = dir + + if err := os.Chdir(cmd.Dir); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) + } + + cmd.Env = append(os.Environ(), "GO111MODULE=on") + command := strings.Join(cmd.Args, " ") + _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + output, err := cmd.CombinedOutput() + if err != nil { + return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + } + + return output, nil +} + +// UninstallPrometheusOperator uninstalls the prometheus +func UninstallPrometheusOperator() { + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + +// UninstallCertManager uninstalls the cert manager +func UninstallCertManager() { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + +// InstallCertManager installs the cert manager bundle. +func InstallCertManager() error { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "apply", "-f", url) + if _, err := Run(cmd); err != nil { + return err + } + // Wait for cert-manager-webhook to be ready, which can take time if cert-manager + // was re-installed after uninstalling on a cluster. + cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", + "--for", "condition=Available", + "--namespace", "cert-manager", + "--timeout", "5m", + ) + + _, err := Run(cmd) + return err +} + +// LoadImageToKindClusterWithName loads a local docker image to the kind cluster +func LoadImageToKindClusterWithName(name string) error { + cluster := "kind" + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { + cluster = v + } + kindOptions := []string{"load", "docker-image", name, "--name", cluster} + cmd := exec.Command("kind", kindOptions...) + _, err := Run(cmd) + return err +} + +// GetNonEmptyLines converts given command output string into individual objects +// according to line breakers, and ignores the empty elements in it. +func GetNonEmptyLines(output string) []string { + var res []string + elements := strings.Split(output, "\n") + for _, element := range elements { + if element != "" { + res = append(res, element) + } + } + + return res +} + +// GetProjectDir will return the directory where the project is +func GetProjectDir() (string, error) { + wd, err := os.Getwd() + if err != nil { + return wd, err + } + wd = strings.Replace(wd, "/test/e2e", "", -1) + return wd, nil +} diff --git a/update-version.sh b/update-version.sh new file mode 100755 index 0000000..6c88942 --- /dev/null +++ b/update-version.sh @@ -0,0 +1,8 @@ +#!/bin/bash +if [[ "$OSTYPE" == "darwin"* ]]; then + find chart/ -type f -exec sed -i '' "s/0.0.0/$1/g" {} + + find chart/values.yaml -type f -exec sed -i '' "s/repository: controller/$2/g" {} + +else + find chart/ -type f -exec sed -i "s/0.0.0/$1/g" {} + + find chart/values.yaml -type f -exec sed -i "s/repository: controller/$2/g" {} + +fi \ No newline at end of file