From d3fa8b107a7a19bf00496b378c0d3905e1daa9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E7=BF=94=E5=AE=87?= Date: Sat, 24 Jan 2026 02:27:39 +0800 Subject: [PATCH] feat: add Kubernetes operator and Helm charts Add Kubernetes ecosystem support for NexusGate: - Helm charts (charts/nexusgate/): - Complete deployment templates for NexusGate - PostgreSQL and Redis as dependencies - HPA, PDB, and Ingress support - Operator CRD and RBAC templates - Kubernetes Operator (operator/): - Go-based operator using kubebuilder - NexusGateApp CRD for declarative configuration - Automatic API key provisioning via NexusGate API - E2E test suite Co-Authored-By: Claude Opus 4.5 --- charts/nexusgate/.helmignore | 23 ++ charts/nexusgate/Chart.lock | 9 + charts/nexusgate/Chart.yaml | 34 ++ charts/nexusgate/charts/postgresql-16.4.1.tgz | Bin 0 -> 81412 bytes charts/nexusgate/charts/redis-20.6.2.tgz | Bin 0 -> 105179 bytes charts/nexusgate/templates/NOTES.txt | 93 +++++ charts/nexusgate/templates/_helpers.tpl | 239 +++++++++++++ charts/nexusgate/templates/configmap.yaml | 17 + charts/nexusgate/templates/deployment.yaml | 131 +++++++ charts/nexusgate/templates/hpa.yaml | 32 ++ charts/nexusgate/templates/ingress.yaml | 41 +++ charts/nexusgate/templates/operator-crd.yaml | 165 +++++++++ .../templates/operator-deployment.yaml | 62 ++++ charts/nexusgate/templates/operator-rbac.yaml | 88 +++++ charts/nexusgate/templates/pdb.yaml | 18 + charts/nexusgate/templates/secret.yaml | 41 +++ charts/nexusgate/templates/service.yaml | 22 ++ .../nexusgate/templates/serviceaccount.yaml | 12 + charts/nexusgate/values.yaml | 317 ++++++++++++++++ operator/.devcontainer/devcontainer.json | 25 ++ operator/.devcontainer/post-install.sh | 23 ++ operator/.dockerignore | 11 + operator/.github/workflows/lint.yml | 23 ++ operator/.github/workflows/test-e2e.yml | 32 ++ operator/.github/workflows/test.yml | 23 ++ operator/.gitignore | 30 ++ operator/.golangci.yml | 52 +++ operator/Dockerfile | 31 ++ operator/Makefile | 250 +++++++++++++ operator/PROJECT | 21 ++ operator/README.md | 135 +++++++ operator/api/v1alpha1/groupversion_info.go | 36 ++ operator/api/v1alpha1/nexusgateapp_types.go | 155 ++++++++ .../api/v1alpha1/zz_generated.deepcopy.go | 142 ++++++++ operator/cmd/main.go | 244 +++++++++++++ .../gateway.nexusgate.io_nexusgateapps.yaml | 195 ++++++++++ operator/config/crd/kustomization.yaml | 16 + operator/config/crd/kustomizeconfig.yaml | 19 + .../default/cert_metrics_manager_patch.yaml | 30 ++ operator/config/default/kustomization.yaml | 234 ++++++++++++ .../config/default/manager_metrics_patch.yaml | 4 + operator/config/default/metrics_service.yaml | 18 + operator/config/manager/config.yaml | 22 ++ operator/config/manager/kustomization.yaml | 4 + operator/config/manager/manager.yaml | 117 ++++++ operator/config/manager/secret.yaml | 16 + .../network-policy/allow-metrics-traffic.yaml | 27 ++ .../config/network-policy/kustomization.yaml | 2 + operator/config/prometheus/kustomization.yaml | 11 + operator/config/prometheus/monitor.yaml | 27 ++ .../config/prometheus/monitor_tls_patch.yaml | 19 + operator/config/rbac/kustomization.yaml | 28 ++ .../config/rbac/leader_election_role.yaml | 40 +++ .../rbac/leader_election_role_binding.yaml | 15 + operator/config/rbac/metrics_auth_role.yaml | 17 + .../rbac/metrics_auth_role_binding.yaml | 12 + operator/config/rbac/metrics_reader_role.yaml | 9 + .../config/rbac/nexusgateapp_admin_role.yaml | 27 ++ .../config/rbac/nexusgateapp_editor_role.yaml | 33 ++ .../config/rbac/nexusgateapp_viewer_role.yaml | 29 ++ operator/config/rbac/role.yaml | 51 +++ operator/config/rbac/role_binding.yaml | 15 + operator/config/rbac/service_account.yaml | 8 + .../config/rbac/user/example-binding.yaml | 32 ++ .../config/rbac/user/nexusgate-app-admin.yaml | 32 ++ .../samples/example-app-deployment.yaml | 104 ++++++ .../gateway_v1alpha1_nexusgateapp.yaml | 25 ++ operator/config/samples/install-config.yaml | 66 ++++ operator/config/samples/kustomization.yaml | 4 + operator/go.mod | 100 ++++++ operator/go.sum | 259 ++++++++++++++ operator/hack/boilerplate.go.txt | 15 + .../controller/nexusgateapp_controller.go | 303 ++++++++++++++++ .../nexusgateapp_controller_test.go | 84 +++++ operator/internal/controller/suite_test.go | 116 ++++++ operator/internal/nexusgate/client.go | 198 ++++++++++ operator/test/e2e/e2e_suite_test.go | 92 +++++ operator/test/e2e/e2e_test.go | 337 ++++++++++++++++++ operator/test/utils/utils.go | 226 ++++++++++++ 79 files changed, 5615 insertions(+) create mode 100644 charts/nexusgate/.helmignore create mode 100644 charts/nexusgate/Chart.lock create mode 100644 charts/nexusgate/Chart.yaml create mode 100644 charts/nexusgate/charts/postgresql-16.4.1.tgz create mode 100644 charts/nexusgate/charts/redis-20.6.2.tgz create mode 100644 charts/nexusgate/templates/NOTES.txt create mode 100644 charts/nexusgate/templates/_helpers.tpl create mode 100644 charts/nexusgate/templates/configmap.yaml create mode 100644 charts/nexusgate/templates/deployment.yaml create mode 100644 charts/nexusgate/templates/hpa.yaml create mode 100644 charts/nexusgate/templates/ingress.yaml create mode 100644 charts/nexusgate/templates/operator-crd.yaml create mode 100644 charts/nexusgate/templates/operator-deployment.yaml create mode 100644 charts/nexusgate/templates/operator-rbac.yaml create mode 100644 charts/nexusgate/templates/pdb.yaml create mode 100644 charts/nexusgate/templates/secret.yaml create mode 100644 charts/nexusgate/templates/service.yaml create mode 100644 charts/nexusgate/templates/serviceaccount.yaml create mode 100644 charts/nexusgate/values.yaml create mode 100644 operator/.devcontainer/devcontainer.json create mode 100644 operator/.devcontainer/post-install.sh create mode 100644 operator/.dockerignore create mode 100644 operator/.github/workflows/lint.yml create mode 100644 operator/.github/workflows/test-e2e.yml create mode 100644 operator/.github/workflows/test.yml create mode 100644 operator/.gitignore create mode 100644 operator/.golangci.yml create mode 100644 operator/Dockerfile create mode 100644 operator/Makefile create mode 100644 operator/PROJECT create mode 100644 operator/README.md create mode 100644 operator/api/v1alpha1/groupversion_info.go create mode 100644 operator/api/v1alpha1/nexusgateapp_types.go create mode 100644 operator/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 operator/cmd/main.go create mode 100644 operator/config/crd/bases/gateway.nexusgate.io_nexusgateapps.yaml create mode 100644 operator/config/crd/kustomization.yaml create mode 100644 operator/config/crd/kustomizeconfig.yaml create mode 100644 operator/config/default/cert_metrics_manager_patch.yaml create mode 100644 operator/config/default/kustomization.yaml create mode 100644 operator/config/default/manager_metrics_patch.yaml create mode 100644 operator/config/default/metrics_service.yaml create mode 100644 operator/config/manager/config.yaml create mode 100644 operator/config/manager/kustomization.yaml create mode 100644 operator/config/manager/manager.yaml create mode 100644 operator/config/manager/secret.yaml create mode 100644 operator/config/network-policy/allow-metrics-traffic.yaml create mode 100644 operator/config/network-policy/kustomization.yaml create mode 100644 operator/config/prometheus/kustomization.yaml create mode 100644 operator/config/prometheus/monitor.yaml create mode 100644 operator/config/prometheus/monitor_tls_patch.yaml create mode 100644 operator/config/rbac/kustomization.yaml create mode 100644 operator/config/rbac/leader_election_role.yaml create mode 100644 operator/config/rbac/leader_election_role_binding.yaml create mode 100644 operator/config/rbac/metrics_auth_role.yaml create mode 100644 operator/config/rbac/metrics_auth_role_binding.yaml create mode 100644 operator/config/rbac/metrics_reader_role.yaml create mode 100644 operator/config/rbac/nexusgateapp_admin_role.yaml create mode 100644 operator/config/rbac/nexusgateapp_editor_role.yaml create mode 100644 operator/config/rbac/nexusgateapp_viewer_role.yaml create mode 100644 operator/config/rbac/role.yaml create mode 100644 operator/config/rbac/role_binding.yaml create mode 100644 operator/config/rbac/service_account.yaml create mode 100644 operator/config/rbac/user/example-binding.yaml create mode 100644 operator/config/rbac/user/nexusgate-app-admin.yaml create mode 100644 operator/config/samples/example-app-deployment.yaml create mode 100644 operator/config/samples/gateway_v1alpha1_nexusgateapp.yaml create mode 100644 operator/config/samples/install-config.yaml create mode 100644 operator/config/samples/kustomization.yaml create mode 100644 operator/go.mod create mode 100644 operator/go.sum create mode 100644 operator/hack/boilerplate.go.txt create mode 100644 operator/internal/controller/nexusgateapp_controller.go create mode 100644 operator/internal/controller/nexusgateapp_controller_test.go create mode 100644 operator/internal/controller/suite_test.go create mode 100644 operator/internal/nexusgate/client.go create mode 100644 operator/test/e2e/e2e_suite_test.go create mode 100644 operator/test/e2e/e2e_test.go create mode 100644 operator/test/utils/utils.go diff --git a/charts/nexusgate/.helmignore b/charts/nexusgate/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/nexusgate/.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/charts/nexusgate/Chart.lock b/charts/nexusgate/Chart.lock new file mode 100644 index 0000000..c806dba --- /dev/null +++ b/charts/nexusgate/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 16.4.1 +- name: redis + repository: https://charts.bitnami.com/bitnami + version: 20.6.2 +digest: sha256:cfe1430ac1c4e64797d80879173085fea117691b9bd52f82e3825520b65b3441 +generated: "2026-01-17T21:07:26.76951+08:00" diff --git a/charts/nexusgate/Chart.yaml b/charts/nexusgate/Chart.yaml new file mode 100644 index 0000000..908ad05 --- /dev/null +++ b/charts/nexusgate/Chart.yaml @@ -0,0 +1,34 @@ +apiVersion: v2 +name: nexusgate +description: A Helm chart for NexusGate - OpenAI-compatible API Gateway for LLM providers +type: application +version: 0.1.0 +appVersion: "1.0.0" + +home: https://github.com/EM-GeekLab/NexusGate +sources: + - https://github.com/EM-GeekLab/NexusGate + +maintainers: + - name: EM-GeekLab + url: https://github.com/EM-GeekLab + +keywords: + - ai + - llm + - openai + - api-gateway + - kubernetes + +dependencies: + # PostgreSQL for persistent storage + - name: postgresql + version: "16.4.1" + repository: "https://charts.bitnami.com/bitnami" + condition: postgresql.enabled + + # Redis for rate limiting and caching + - name: redis + version: "20.6.2" + repository: "https://charts.bitnami.com/bitnami" + condition: redis.enabled diff --git a/charts/nexusgate/charts/postgresql-16.4.1.tgz b/charts/nexusgate/charts/postgresql-16.4.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f9a34214be0d641e04ba2d50d543f184a72bd543 GIT binary patch literal 81412 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 CFC+p0 literal 0 HcmV?d00001 diff --git a/charts/nexusgate/charts/redis-20.6.2.tgz b/charts/nexusgate/charts/redis-20.6.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..383bfb04db2fb817672770f86fb6080b19c6d351 GIT binary patch literal 105179 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\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=manager-role 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 setup-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 + +# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. +# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. +# CertManager is installed by default; skip with: +# - CERT_MANAGER_INSTALL_SKIP=true +KIND_CLUSTER ?= operator-test-e2e + +.PHONY: setup-test-e2e +setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist + @command -v $(KIND) >/dev/null 2>&1 || { \ + echo "Kind is not installed. Please install Kind manually."; \ + exit 1; \ + } + @case "$$($(KIND) get clusters)" in \ + *"$(KIND_CLUSTER)"*) \ + echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ + *) \ + echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ + $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ + esac + +.PHONY: test-e2e +test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. + KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v + $(MAKE) cleanup-test-e2e + +.PHONY: cleanup-test-e2e +cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests + @$(KIND) delete cluster --name $(KIND_CLUSTER) + +.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 + +.PHONY: lint-config +lint-config: golangci-lint ## Verify golangci-lint linter configuration + "$(GOLANGCI_LINT)" config verify + +##@ 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} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# 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,linux/s390x,linux/ppc64le +.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 operator-builder + $(CONTAINER_TOOL) buildx use operator-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm 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} + "$(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. + @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ + if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi + +.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. + @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ + if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi + +.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 +KIND ?= kind +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.7.1 +CONTROLLER_TOOLS_VERSION ?= v0.19.0 + +#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) +ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \ + [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \ + printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/') + +#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) +ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ + [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ + printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') + +GOLANGCI_LINT_VERSION ?= v2.5.0 +.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: setup-envtest +setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. + @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." + @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \ + echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ + exit 1; \ + } + +.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/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +# 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)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f "$(1)" ;\ +GOBIN="$(LOCALBIN)" go install $${package} ;\ +mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ +} ;\ +ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" +endef + +define gomodver +$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) +endef diff --git a/operator/PROJECT b/operator/PROJECT new file mode 100644 index 0000000..5b990ff --- /dev/null +++ b/operator/PROJECT @@ -0,0 +1,21 @@ +# 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 +cliVersion: 4.10.1 +domain: nexusgate.io +layout: +- go.kubebuilder.io/v4 +projectName: operator +repo: github.com/EM-GeekLab/nexusgate-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: nexusgate.io + group: gateway + kind: NexusGateApp + path: github.com/EM-GeekLab/nexusgate-operator/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/operator/README.md b/operator/README.md new file mode 100644 index 0000000..796d4a6 --- /dev/null +++ b/operator/README.md @@ -0,0 +1,135 @@ +# 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.24.6+ +- 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=/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=/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 the options to release and provide this solution to the users. + +### By providing a bundle with all YAML files + +1. Build the installer for the image built and published in the registry: + +```sh +make build-installer IMG=/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//operator//dist/install.yaml +``` + +### By providing a Helm Chart + +1. Build the chart using the optional helm plugin + +```sh +kubebuilder edit --plugins=helm/v2-alpha +``` + +2. See that a chart was generated under 'dist/chart', and users +can obtain this solution from there. + +**NOTE:** If you change the project, you need to update the Helm Chart +using the same command above to sync the latest changes. Furthermore, +if you create webhooks, you need to use the above command with +the '--force' flag and manually ensure that any custom configuration +previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' +is manually re-applied afterwards. + +## 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 2026. + +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/operator/api/v1alpha1/groupversion_info.go b/operator/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..b90bc6e --- /dev/null +++ b/operator/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2026. + +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 v1alpha1 contains API Schema definitions for the gateway v1alpha1 API group. +// +kubebuilder:object:generate=true +// +groupName=gateway.nexusgate.io +package v1alpha1 + +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: "gateway.nexusgate.io", Version: "v1alpha1"} + + // 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/operator/api/v1alpha1/nexusgateapp_types.go b/operator/api/v1alpha1/nexusgateapp_types.go new file mode 100644 index 0000000..ea7c3fc --- /dev/null +++ b/operator/api/v1alpha1/nexusgateapp_types.go @@ -0,0 +1,155 @@ +/* +Copyright 2026. + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeletionPolicy defines what happens to the API key when the NexusGateApp is deleted +// +kubebuilder:validation:Enum=Revoke;Retain +type DeletionPolicy string + +const ( + // DeletionPolicyRevoke revokes the API key when the NexusGateApp is deleted + DeletionPolicyRevoke DeletionPolicy = "Revoke" + // DeletionPolicyRetain keeps the API key when the NexusGateApp is deleted + DeletionPolicyRetain DeletionPolicy = "Retain" +) + +// SecretRef defines the reference to the Secret where the API key will be stored +type SecretRef struct { + // Name is the name of the Secret + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Namespace is the namespace of the Secret (defaults to the NexusGateApp's namespace) + // +optional + Namespace string `json:"namespace,omitempty"` + + // Key is the key in the Secret data where the API key will be stored + // +kubebuilder:default="NEXUSGATE_API_KEY" + // +optional + Key string `json:"key,omitempty"` +} + +// NexusGateAppSpec defines the desired state of NexusGateApp +type NexusGateAppSpec struct { + // AppName is the application name used as identifier in NexusGate + // This will be stored in the API key's externalId field as k8s/{cluster}/{namespace}/{appName} + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + AppName string `json:"appName"` + + // SecretRef defines where to store the API key + // +kubebuilder:validation:Required + SecretRef SecretRef `json:"secretRef"` + + // DeletionPolicy defines what happens to the API key when this resource is deleted + // +kubebuilder:default=Revoke + // +optional + DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"` +} + +// NexusGateAppPhase defines the current phase of the NexusGateApp +// +kubebuilder:validation:Enum=Pending;Ready;Error;Deleting +type NexusGateAppPhase string + +const ( + // PhasePending indicates the resource is being processed + PhasePending NexusGateAppPhase = "Pending" + // PhaseReady indicates the API key is provisioned and synced + PhaseReady NexusGateAppPhase = "Ready" + // PhaseError indicates an error occurred + PhaseError NexusGateAppPhase = "Error" + // PhaseDeleting indicates the resource is being deleted + PhaseDeleting NexusGateAppPhase = "Deleting" +) + +// NexusGateAppStatus defines the observed state of NexusGateApp +type NexusGateAppStatus struct { + // Phase indicates the current phase of the NexusGateApp + // +optional + Phase NexusGateAppPhase `json:"phase,omitempty"` + + // APIKeyID is the ID of the API key in NexusGate database + // +optional + APIKeyID int `json:"apiKeyId,omitempty"` + + // APIKeyPrefix is the masked prefix of the API key (e.g., sk-xxxx...xxxx) + // +optional + APIKeyPrefix string `json:"apiKeyPrefix,omitempty"` + + // SecretSynced indicates whether the Secret has been successfully synced + // +optional + SecretSynced bool `json:"secretSynced,omitempty"` + + // LastSyncTime is the last time the resource was successfully synced + // +optional + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` + + // Message provides human-readable status information + // +optional + Message string `json:"message,omitempty"` + + // Conditions represent the current state of the NexusGateApp resource + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="App",type=string,JSONPath=`.spec.appName` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Secret",type=string,JSONPath=`.spec.secretRef.name` +// +kubebuilder:printcolumn:name="Synced",type=boolean,JSONPath=`.status.secretSynced` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// NexusGateApp is the Schema for the nexusgateapps API +type NexusGateApp struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // spec defines the desired state of NexusGateApp + // +required + Spec NexusGateAppSpec `json:"spec"` + + // status defines the observed state of NexusGateApp + // +optional + Status NexusGateAppStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// NexusGateAppList contains a list of NexusGateApp +type NexusGateAppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []NexusGateApp `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NexusGateApp{}, &NexusGateAppList{}) +} diff --git a/operator/api/v1alpha1/zz_generated.deepcopy.go b/operator/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..1a7baa5 --- /dev/null +++ b/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,142 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2026. + +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 v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + 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 *NexusGateApp) DeepCopyInto(out *NexusGateApp) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NexusGateApp. +func (in *NexusGateApp) DeepCopy() *NexusGateApp { + if in == nil { + return nil + } + out := new(NexusGateApp) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NexusGateApp) 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 *NexusGateAppList) DeepCopyInto(out *NexusGateAppList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NexusGateApp, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NexusGateAppList. +func (in *NexusGateAppList) DeepCopy() *NexusGateAppList { + if in == nil { + return nil + } + out := new(NexusGateAppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NexusGateAppList) 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 *NexusGateAppSpec) DeepCopyInto(out *NexusGateAppSpec) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NexusGateAppSpec. +func (in *NexusGateAppSpec) DeepCopy() *NexusGateAppSpec { + if in == nil { + return nil + } + out := new(NexusGateAppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NexusGateAppStatus) DeepCopyInto(out *NexusGateAppStatus) { + *out = *in + if in.LastSyncTime != nil { + in, out := &in.LastSyncTime, &out.LastSyncTime + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NexusGateAppStatus. +func (in *NexusGateAppStatus) DeepCopy() *NexusGateAppStatus { + if in == nil { + return nil + } + out := new(NexusGateAppStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} diff --git a/operator/cmd/main.go b/operator/cmd/main.go new file mode 100644 index 0000000..9c57e21 --- /dev/null +++ b/operator/cmd/main.go @@ -0,0 +1,244 @@ +/* +Copyright 2026. + +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" + + gatewayv1alpha1 "github.com/EM-GeekLab/nexusgate-operator/api/v1alpha1" + "github.com/EM-GeekLab/nexusgate-operator/internal/controller" + "github.com/EM-GeekLab/nexusgate-operator/internal/nexusgate" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(gatewayv1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +// nolint:gocyclo +func main() { + var metricsAddr string + var metricsCertPath, metricsCertName, metricsCertKey string + var webhookCertPath, webhookCertName, webhookCertKey string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + + // NexusGate configuration + var nexusgateURL string + var nexusgateToken string + var clusterName string + + flag.StringVar(&nexusgateURL, "nexusgate-url", "", "The URL of the NexusGate API server (required). Can also be set via NEXUSGATE_URL env var.") + flag.StringVar(&nexusgateToken, "nexusgate-token", "", "The admin token for NexusGate API (required). Can also be set via NEXUSGATE_ADMIN_TOKEN env var.") + flag.StringVar(&clusterName, "cluster-name", "default", "The name of this Kubernetes cluster (used in external ID). Can also be set via CLUSTER_NAME env var.") + + 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.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + 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))) + + // Read NexusGate configuration from environment variables if not set via flags + if nexusgateURL == "" { + nexusgateURL = os.Getenv("NEXUSGATE_URL") + } + if nexusgateToken == "" { + nexusgateToken = os.Getenv("NEXUSGATE_ADMIN_TOKEN") + } + if clusterName == "default" { + if envCluster := os.Getenv("CLUSTER_NAME"); envCluster != "" { + clusterName = envCluster + } + } + + // Validate required configuration + if nexusgateURL == "" { + setupLog.Error(nil, "NexusGate URL is required. Set via --nexusgate-url flag or NEXUSGATE_URL env var") + os.Exit(1) + } + if nexusgateToken == "" { + setupLog.Error(nil, "NexusGate admin token is required. Set via --nexusgate-token flag or NEXUSGATE_ADMIN_TOKEN env var") + os.Exit(1) + } + + // Create NexusGate client + nexusgateClient := nexusgate.NewClient(nexusgateURL, nexusgateToken) + setupLog.Info("NexusGate client configured", "url", nexusgateURL, "cluster", clusterName) + + // 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) + } + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + webhookServerOptions := webhook.Options{ + TLSOpts: webhookTLSOpts, + } + + if len(webhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) + + webhookServerOptions.CertDir = webhookCertPath + webhookServerOptions.CertName = webhookCertName + webhookServerOptions.KeyName = webhookCertKey + } + + webhookServer := webhook.NewServer(webhookServerOptions) + + // 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.22.4/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + 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.22.4/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(metricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) + + metricsServerOptions.CertDir = metricsCertPath + metricsServerOptions.CertName = metricsCertName + metricsServerOptions.KeyName = metricsCertKey + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "617b4010.nexusgate.io", + // 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 := (&controller.NexusGateAppReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + NexusGate: nexusgateClient, + ClusterName: clusterName, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "NexusGateApp") + 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/operator/config/crd/bases/gateway.nexusgate.io_nexusgateapps.yaml b/operator/config/crd/bases/gateway.nexusgate.io_nexusgateapps.yaml new file mode 100644 index 0000000..8d8268c --- /dev/null +++ b/operator/config/crd/bases/gateway.nexusgate.io_nexusgateapps.yaml @@ -0,0 +1,195 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: nexusgateapps.gateway.nexusgate.io +spec: + group: gateway.nexusgate.io + names: + kind: NexusGateApp + listKind: NexusGateAppList + plural: nexusgateapps + singular: nexusgateapp + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.appName + name: App + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .spec.secretRef.name + name: Secret + type: string + - jsonPath: .status.secretSynced + name: Synced + type: boolean + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: NexusGateApp is the Schema for the nexusgateapps 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: spec defines the desired state of NexusGateApp + properties: + appName: + description: |- + AppName is the application name used as identifier in NexusGate + This will be stored in the API key's externalId field as k8s/{cluster}/{namespace}/{appName} + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + deletionPolicy: + default: Revoke + description: DeletionPolicy defines what happens to the API key when + this resource is deleted + enum: + - Revoke + - Retain + type: string + secretRef: + description: SecretRef defines where to store the API key + properties: + key: + default: NEXUSGATE_API_KEY + description: Key is the key in the Secret data where the API key + will be stored + type: string + name: + description: Name is the name of the Secret + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the Secret (defaults + to the NexusGateApp's namespace) + type: string + required: + - name + type: object + required: + - appName + - secretRef + type: object + status: + description: status defines the observed state of NexusGateApp + properties: + apiKeyId: + description: APIKeyID is the ID of the API key in NexusGate database + type: integer + apiKeyPrefix: + description: APIKeyPrefix is the masked prefix of the API key (e.g., + sk-xxxx...xxxx) + type: string + conditions: + description: Conditions represent the current state of the NexusGateApp + resource + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastSyncTime: + description: LastSyncTime is the last time the resource was successfully + synced + format: date-time + type: string + message: + description: Message provides human-readable status information + type: string + phase: + description: Phase indicates the current phase of the NexusGateApp + enum: + - Pending + - Ready + - Error + - Deleting + type: string + secretSynced: + description: SecretSynced indicates whether the Secret has been successfully + synced + type: boolean + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/operator/config/crd/kustomization.yaml b/operator/config/crd/kustomization.yaml new file mode 100644 index 0000000..29eb10d --- /dev/null +++ b/operator/config/crd/kustomization.yaml @@ -0,0 +1,16 @@ +# 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/gateway.nexusgate.io_nexusgateapps.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 + +# [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/operator/config/crd/kustomizeconfig.yaml b/operator/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..ec5c150 --- /dev/null +++ b/operator/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/operator/config/default/cert_metrics_manager_patch.yaml b/operator/config/default/cert_metrics_manager_patch.yaml new file mode 100644 index 0000000..d975015 --- /dev/null +++ b/operator/config/default/cert_metrics_manager_patch.yaml @@ -0,0 +1,30 @@ +# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. + +# Add the volumeMount for the metrics-server certs +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + mountPath: /tmp/k8s-metrics-server/metrics-certs + name: metrics-certs + readOnly: true + +# Add the --metrics-cert-path argument for the metrics server +- op: add + path: /spec/template/spec/containers/0/args/- + value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs + +# Add the metrics-server certs volume configuration +- op: add + path: /spec/template/spec/volumes/- + value: + name: metrics-certs + secret: + secretName: metrics-server-cert + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key diff --git a/operator/config/default/kustomization.yaml b/operator/config/default/kustomization.yaml new file mode 100644 index 0000000..d335e90 --- /dev/null +++ b/operator/config/default/kustomization.yaml @@ -0,0 +1,234 @@ +# Adds namespace to all resources. +namespace: 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: 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 +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 + +# Uncomment the patches line if you enable Metrics and CertManager +# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. +# This patch will protect the metrics with certManager self-signed certs. +#- path: cert_metrics_manager_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 +# target: +# kind: Deployment + +# [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: # Uncomment the following block to enable certificates for metrics +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.name +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 0 +# create: true + +# - source: +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.namespace +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 1 +# create: true + +# - source: # Uncomment the following block if you have any webhook +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # Name of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# 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 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true + +# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) +# 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 +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true + +# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# 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 +# fieldPath: .metadata.name +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true + +# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# +kubebuilder:scaffold:crdkustomizecainjectionns +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# +kubebuilder:scaffold:crdkustomizecainjectionname diff --git a/operator/config/default/manager_metrics_patch.yaml b/operator/config/default/manager_metrics_patch.yaml new file mode 100644 index 0000000..2aaef65 --- /dev/null +++ b/operator/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/operator/config/default/metrics_service.yaml b/operator/config/default/metrics_service.yaml new file mode 100644 index 0000000..1f4155a --- /dev/null +++ b/operator/config/default/metrics_service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager + app.kubernetes.io/name: operator diff --git a/operator/config/manager/config.yaml b/operator/config/manager/config.yaml new file mode 100644 index 0000000..42e2d38 --- /dev/null +++ b/operator/config/manager/config.yaml @@ -0,0 +1,22 @@ +# ConfigMap for NexusGate Operator configuration +apiVersion: v1 +kind: ConfigMap +metadata: + name: nexusgate-operator-config + namespace: system + labels: + app.kubernetes.io/name: nexusgate-operator + app.kubernetes.io/component: config +data: + # NexusGate API URL + # For in-cluster deployment, use K8s DNS format: + # http://..svc.cluster.local: + # Examples: + # http://nexusgate.nexusgate.svc.cluster.local:3000 + # http://nexusgate-server.default.svc:3000 + nexusgate-url: "http://nexusgate.nexusgate.svc.cluster.local:3000" + + # Cluster name for external ID generation + # This is used to create unique external IDs: k8s/{cluster}/{namespace}/{appName} + # Useful when multiple K8s clusters connect to the same NexusGate instance + cluster-name: "default" diff --git a/operator/config/manager/kustomization.yaml b/operator/config/manager/kustomization.yaml new file mode 100644 index 0000000..a5e4dba --- /dev/null +++ b/operator/config/manager/kustomization.yaml @@ -0,0 +1,4 @@ +resources: +- manager.yaml +- config.yaml +- secret.yaml diff --git a/operator/config/manager/manager.yaml b/operator/config/manager/manager.yaml new file mode 100644 index 0000000..80d86e2 --- /dev/null +++ b/operator/config/manager/manager.yaml @@ -0,0 +1,117 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: operator + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + app.kubernetes.io/name: operator + 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: + # Projects are configured by default to adhere to the "restricted" Pod Security Standards. + # This ensures that deployments meet the highest security requirements for Kubernetes. + # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + image: controller:latest + name: manager + ports: [] + env: + # NexusGate URL - use K8s DNS for in-cluster communication + # Format: http://..svc.cluster.local: + - name: NEXUSGATE_URL + valueFrom: + configMapKeyRef: + name: nexusgate-operator-config + key: nexusgate-url + # Admin token from Secret + - name: NEXUSGATE_ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: nexusgate-operator-secret + key: admin-token + # Cluster name for external ID generation + - name: CLUSTER_NAME + valueFrom: + configMapKeyRef: + name: nexusgate-operator-config + key: cluster-name + securityContext: + readOnlyRootFilesystem: true + 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 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + volumeMounts: [] + volumes: [] + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/operator/config/manager/secret.yaml b/operator/config/manager/secret.yaml new file mode 100644 index 0000000..6041848 --- /dev/null +++ b/operator/config/manager/secret.yaml @@ -0,0 +1,16 @@ +# Secret for NexusGate Operator credentials +# IMPORTANT: Replace the placeholder with your actual admin token +apiVersion: v1 +kind: Secret +metadata: + name: nexusgate-operator-secret + namespace: system + labels: + app.kubernetes.io/name: nexusgate-operator + app.kubernetes.io/component: config +type: Opaque +stringData: + # NexusGate Admin API token + # This token must have permission to manage API keys + # You can create this token in NexusGate UI or via API + admin-token: "YOUR_ADMIN_TOKEN_HERE" diff --git a/operator/config/network-policy/allow-metrics-traffic.yaml b/operator/config/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 0000000..d3ac983 --- /dev/null +++ b/operator/config/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,27 @@ +# This NetworkPolicy allows ingress traffic +# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those +# namespaces are able to gather data from the metrics endpoint. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: allow-metrics-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: operator + 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/operator/config/network-policy/kustomization.yaml b/operator/config/network-policy/kustomization.yaml new file mode 100644 index 0000000..ec0fb5e --- /dev/null +++ b/operator/config/network-policy/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- allow-metrics-traffic.yaml diff --git a/operator/config/prometheus/kustomization.yaml b/operator/config/prometheus/kustomization.yaml new file mode 100644 index 0000000..fdc5481 --- /dev/null +++ b/operator/config/prometheus/kustomization.yaml @@ -0,0 +1,11 @@ +resources: +- monitor.yaml + +# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus +# to securely reference certificates created and managed by cert-manager. +# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml +# to mount the "metrics-server-cert" secret in the Manager Deployment. +#patches: +# - path: monitor_tls_patch.yaml +# target: +# kind: ServiceMonitor diff --git a/operator/config/prometheus/monitor.yaml b/operator/config/prometheus/monitor.yaml new file mode 100644 index 0000000..b73583e --- /dev/null +++ b/operator/config/prometheus/monitor.yaml @@ -0,0 +1,27 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-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, exposing the system to potential man-in-the-middle attacks. + # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. + # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, + # which securely references the certificate from the 'metrics-server-cert' secret. + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: operator diff --git a/operator/config/prometheus/monitor_tls_patch.yaml b/operator/config/prometheus/monitor_tls_patch.yaml new file mode 100644 index 0000000..5bf84ce --- /dev/null +++ b/operator/config/prometheus/monitor_tls_patch.yaml @@ -0,0 +1,19 @@ +# Patch for Prometheus ServiceMonitor to enable secure TLS configuration +# using certificates managed by cert-manager +- op: replace + path: /spec/endpoints/0/tlsConfig + value: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc + insecureSkipVerify: false + ca: + secret: + name: metrics-server-cert + key: ca.crt + cert: + secret: + name: metrics-server-cert + key: tls.crt + keySecret: + name: metrics-server-cert + key: tls.key diff --git a/operator/config/rbac/kustomization.yaml b/operator/config/rbac/kustomization.yaml new file mode 100644 index 0000000..5fa1a77 --- /dev/null +++ b/operator/config/rbac/kustomization.yaml @@ -0,0 +1,28 @@ +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, "Admin", "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the operator itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- nexusgateapp_admin_role.yaml +- nexusgateapp_editor_role.yaml +- nexusgateapp_viewer_role.yaml + diff --git a/operator/config/rbac/leader_election_role.yaml b/operator/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..507e52b --- /dev/null +++ b/operator/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: operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +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/operator/config/rbac/leader_election_role_binding.yaml b/operator/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..c60ecc7 --- /dev/null +++ b/operator/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: operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/operator/config/rbac/metrics_auth_role.yaml b/operator/config/rbac/metrics_auth_role.yaml new file mode 100644 index 0000000..32d2e4e --- /dev/null +++ b/operator/config/rbac/metrics_auth_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-auth-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/operator/config/rbac/metrics_auth_role_binding.yaml b/operator/config/rbac/metrics_auth_role_binding.yaml new file mode 100644 index 0000000..e775d67 --- /dev/null +++ b/operator/config/rbac/metrics_auth_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: metrics-auth-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metrics-auth-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/operator/config/rbac/metrics_reader_role.yaml b/operator/config/rbac/metrics_reader_role.yaml new file mode 100644 index 0000000..51a75db --- /dev/null +++ b/operator/config/rbac/metrics_reader_role.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/operator/config/rbac/nexusgateapp_admin_role.yaml b/operator/config/rbac/nexusgateapp_admin_role.yaml new file mode 100644 index 0000000..eb61ae9 --- /dev/null +++ b/operator/config/rbac/nexusgateapp_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over gateway.nexusgate.io. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: nexusgateapp-admin-role +rules: +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps + verbs: + - '*' +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps/status + verbs: + - get diff --git a/operator/config/rbac/nexusgateapp_editor_role.yaml b/operator/config/rbac/nexusgateapp_editor_role.yaml new file mode 100644 index 0000000..1d07904 --- /dev/null +++ b/operator/config/rbac/nexusgateapp_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the gateway.nexusgate.io. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: nexusgateapp-editor-role +rules: +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps/status + verbs: + - get diff --git a/operator/config/rbac/nexusgateapp_viewer_role.yaml b/operator/config/rbac/nexusgateapp_viewer_role.yaml new file mode 100644 index 0000000..b4309c2 --- /dev/null +++ b/operator/config/rbac/nexusgateapp_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to gateway.nexusgate.io resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: nexusgateapp-viewer-role +rules: +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps + verbs: + - get + - list + - watch +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps/status + verbs: + - get diff --git a/operator/config/rbac/role.yaml b/operator/config/rbac/role.yaml new file mode 100644 index 0000000..f244eda --- /dev/null +++ b/operator/config/rbac/role.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps/finalizers + verbs: + - update +- apiGroups: + - gateway.nexusgate.io + resources: + - nexusgateapps/status + verbs: + - get + - patch + - update diff --git a/operator/config/rbac/role_binding.yaml b/operator/config/rbac/role_binding.yaml new file mode 100644 index 0000000..5d27960 --- /dev/null +++ b/operator/config/rbac/role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/operator/config/rbac/service_account.yaml b/operator/config/rbac/service_account.yaml new file mode 100644 index 0000000..3567d2f --- /dev/null +++ b/operator/config/rbac/service_account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/operator/config/rbac/user/example-binding.yaml b/operator/config/rbac/user/example-binding.yaml new file mode 100644 index 0000000..b40331f --- /dev/null +++ b/operator/config/rbac/user/example-binding.yaml @@ -0,0 +1,32 @@ +# Example: Bind nexusgate-app-admin role to a specific group +# Uncomment and modify according to your organization's needs +# +# apiVersion: rbac.authorization.k8s.io/v1 +# kind: ClusterRoleBinding +# metadata: +# name: ai-team-nexusgate-admin +# subjects: +# - kind: Group +# name: ai-platform-admins # Your LDAP/AD group name +# apiGroup: rbac.authorization.k8s.io +# roleRef: +# kind: ClusterRole +# name: nexusgate-app-admin +# apiGroup: rbac.authorization.k8s.io +# --- +# Example: Namespace-scoped binding (RoleBinding) +# This limits the user to only create NexusGateApps in a specific namespace +# +# apiVersion: rbac.authorization.k8s.io/v1 +# kind: RoleBinding +# metadata: +# name: dev-team-nexusgate-admin +# namespace: dev-apps +# subjects: +# - kind: Group +# name: dev-team +# apiGroup: rbac.authorization.k8s.io +# roleRef: +# kind: ClusterRole +# name: nexusgate-app-admin +# apiGroup: rbac.authorization.k8s.io diff --git a/operator/config/rbac/user/nexusgate-app-admin.yaml b/operator/config/rbac/user/nexusgate-app-admin.yaml new file mode 100644 index 0000000..9030a04 --- /dev/null +++ b/operator/config/rbac/user/nexusgate-app-admin.yaml @@ -0,0 +1,32 @@ +# ClusterRole for users who can manage NexusGateApp resources +# This role should be bound to users/groups who need to provision AI capabilities +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nexusgate-app-admin + labels: + app.kubernetes.io/name: nexusgate-operator + app.kubernetes.io/component: rbac +rules: + - apiGroups: ["gateway.nexusgate.io"] + resources: ["nexusgateapps"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["gateway.nexusgate.io"] + resources: ["nexusgateapps/status"] + verbs: ["get"] +--- +# ClusterRole for read-only access to NexusGateApp resources +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nexusgate-app-viewer + labels: + app.kubernetes.io/name: nexusgate-operator + app.kubernetes.io/component: rbac +rules: + - apiGroups: ["gateway.nexusgate.io"] + resources: ["nexusgateapps"] + verbs: ["get", "list", "watch"] + - apiGroups: ["gateway.nexusgate.io"] + resources: ["nexusgateapps/status"] + verbs: ["get"] diff --git a/operator/config/samples/example-app-deployment.yaml b/operator/config/samples/example-app-deployment.yaml new file mode 100644 index 0000000..ff4ee3c --- /dev/null +++ b/operator/config/samples/example-app-deployment.yaml @@ -0,0 +1,104 @@ +# Complete example: NexusGateApp + Application Deployment +# This shows how to provision an API key and use it in your application + +--- +# Step 1: Create a NexusGateApp to provision the API key +apiVersion: gateway.nexusgate.io/v1alpha1 +kind: NexusGateApp +metadata: + name: my-chatbot + namespace: production +spec: + appName: "my-chatbot" + secretRef: + name: my-chatbot-credentials + key: OPENAI_API_KEY + deletionPolicy: Revoke + +--- +# Step 2: Deploy your application using the provisioned Secret +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-chatbot + namespace: production +spec: + replicas: 2 + selector: + matchLabels: + app: my-chatbot + template: + metadata: + labels: + app: my-chatbot + spec: + containers: + - name: chatbot + image: my-chatbot:latest + env: + # API key from NexusGateApp-managed Secret + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: my-chatbot-credentials + key: OPENAI_API_KEY + + # NexusGate URL using K8s DNS (in-cluster) + # Format: http://..svc.cluster.local: + - name: OPENAI_API_BASE + value: "http://nexusgate.nexusgate.svc.cluster.local:3000/v1" + + # Alternative: Short DNS format (also works within cluster) + # value: "http://nexusgate.nexusgate.svc:3000/v1" + + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + +--- +# Optional: ConfigMap for centralized NexusGate URL configuration +# Applications can reference this instead of hardcoding the URL +apiVersion: v1 +kind: ConfigMap +metadata: + name: nexusgate-config + namespace: production +data: + # In-cluster K8s DNS URL + api-base-url: "http://nexusgate.nexusgate.svc.cluster.local:3000/v1" + +--- +# Example using ConfigMap for NexusGate URL +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-chatbot-v2 + namespace: production +spec: + replicas: 2 + selector: + matchLabels: + app: my-chatbot-v2 + template: + metadata: + labels: + app: my-chatbot-v2 + spec: + containers: + - name: chatbot + image: my-chatbot:latest + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: my-chatbot-credentials + key: OPENAI_API_KEY + - name: OPENAI_API_BASE + valueFrom: + configMapKeyRef: + name: nexusgate-config + key: api-base-url diff --git a/operator/config/samples/gateway_v1alpha1_nexusgateapp.yaml b/operator/config/samples/gateway_v1alpha1_nexusgateapp.yaml new file mode 100644 index 0000000..ca690d2 --- /dev/null +++ b/operator/config/samples/gateway_v1alpha1_nexusgateapp.yaml @@ -0,0 +1,25 @@ +# Example NexusGateApp resource +# This will automatically create an API key in NexusGate and sync it to a Secret +apiVersion: gateway.nexusgate.io/v1alpha1 +kind: NexusGateApp +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: nexusgateapp-sample + namespace: default +spec: + # Application name - used as identifier in NexusGate + # The external ID will be: k8s/{cluster}/{namespace}/{appName} + appName: "my-chatbot" + + # Reference to the Secret where the API key will be stored + secretRef: + name: my-chatbot-api-key + # namespace: default # Optional: defaults to same namespace as the NexusGateApp + key: OPENAI_API_KEY # Optional: defaults to NEXUSGATE_API_KEY + + # What happens when this resource is deleted + # Revoke: The API key will be revoked in NexusGate (default) + # Retain: The API key will be kept in NexusGate + deletionPolicy: Revoke diff --git a/operator/config/samples/install-config.yaml b/operator/config/samples/install-config.yaml new file mode 100644 index 0000000..6b0cb47 --- /dev/null +++ b/operator/config/samples/install-config.yaml @@ -0,0 +1,66 @@ +# Installation Configuration Examples for NexusGate Operator +# Choose the appropriate configuration based on your deployment scenario + +--- +# Scenario 1: NexusGate in the same cluster (recommended) +# Use Kubernetes DNS for service discovery +apiVersion: v1 +kind: ConfigMap +metadata: + name: nexusgate-operator-config + namespace: nexusgate-system +data: + # K8s DNS format: http://..svc.cluster.local: + # Short format also works: http://..svc: + nexusgate-url: "http://nexusgate.nexusgate.svc.cluster.local:3000" + cluster-name: "production" + +--- +# Scenario 2: NexusGate in different namespace +apiVersion: v1 +kind: ConfigMap +metadata: + name: nexusgate-operator-config + namespace: nexusgate-system +data: + # Reference service in another namespace + nexusgate-url: "http://nexusgate-server.ai-platform.svc.cluster.local:3000" + cluster-name: "production" + +--- +# Scenario 3: NexusGate external to the cluster +apiVersion: v1 +kind: ConfigMap +metadata: + name: nexusgate-operator-config + namespace: nexusgate-system +data: + # External URL (e.g., via Ingress or LoadBalancer) + nexusgate-url: "https://nexusgate.example.com" + cluster-name: "production-k8s" + +--- +# Scenario 4: Multi-cluster setup +# Each cluster should have a unique cluster-name +apiVersion: v1 +kind: ConfigMap +metadata: + name: nexusgate-operator-config + namespace: nexusgate-system +data: + # Central NexusGate instance + nexusgate-url: "https://nexusgate.internal.example.com" + # Unique name per cluster for external ID: k8s/{cluster}/{namespace}/{appName} + cluster-name: "us-west-2-prod" + +--- +# Admin Token Secret (required for all scenarios) +# IMPORTANT: Replace with your actual admin token +apiVersion: v1 +kind: Secret +metadata: + name: nexusgate-operator-secret + namespace: nexusgate-system +type: Opaque +stringData: + admin-token: "sk-your-admin-token-here" diff --git a/operator/config/samples/kustomization.yaml b/operator/config/samples/kustomization.yaml new file mode 100644 index 0000000..751cb7b --- /dev/null +++ b/operator/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- gateway_v1alpha1_nexusgateapp.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/operator/go.mod b/operator/go.mod new file mode 100644 index 0000000..fbe5beb --- /dev/null +++ b/operator/go.mod @@ -0,0 +1,100 @@ +module github.com/EM-GeekLab/nexusgate-operator + +go 1.24.6 + +require ( + github.com/onsi/ginkgo/v2 v2.22.0 + github.com/onsi/gomega v1.36.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/controller-runtime v0.22.4 +) + +require ( + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // 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.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.2 // 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.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // 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.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.26.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/apiserver v0.34.1 // indirect + k8s.io/component-base v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/operator/go.sum b/operator/go.sum new file mode 100644 index 0000000..3797258 --- /dev/null +++ b/operator/go.sum @@ -0,0 +1,259 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +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/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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/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.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +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/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +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.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/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.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.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/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +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-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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.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.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= +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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/operator/hack/boilerplate.go.txt b/operator/hack/boilerplate.go.txt new file mode 100644 index 0000000..9786798 --- /dev/null +++ b/operator/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2026. + +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/operator/internal/controller/nexusgateapp_controller.go b/operator/internal/controller/nexusgateapp_controller.go new file mode 100644 index 0000000..805de15 --- /dev/null +++ b/operator/internal/controller/nexusgateapp_controller.go @@ -0,0 +1,303 @@ +/* +Copyright 2026. + +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 controller + +import ( + "context" + "fmt" + "time" + + corev1 "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" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + gatewayv1alpha1 "github.com/EM-GeekLab/nexusgate-operator/api/v1alpha1" + "github.com/EM-GeekLab/nexusgate-operator/internal/nexusgate" +) + +const ( + // finalizerName is the finalizer used by this controller + finalizerName = "nexusgateapp.gateway.nexusgate.io/finalizer" + // requeueInterval is the default requeue interval for periodic sync + requeueInterval = 5 * time.Minute +) + +// NexusGateAppReconciler reconciles a NexusGateApp object +type NexusGateAppReconciler struct { + client.Client + Scheme *runtime.Scheme + NexusGate *nexusgate.Client + ClusterName string +} + +// +kubebuilder:rbac:groups=gateway.nexusgate.io,resources=nexusgateapps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=gateway.nexusgate.io,resources=nexusgateapps/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=gateway.nexusgate.io,resources=nexusgateapps/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *NexusGateAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + // Fetch the NexusGateApp instance + var app gatewayv1alpha1.NexusGateApp + if err := r.Get(ctx, req.NamespacedName, &app); err != nil { + if errors.IsNotFound(err) { + // Object not found, return without requeuing + return ctrl.Result{}, nil + } + log.Error(err, "unable to fetch NexusGateApp") + return ctrl.Result{}, err + } + + // Handle deletion + if !app.DeletionTimestamp.IsZero() { + return r.handleDeletion(ctx, &app) + } + + // Add finalizer if not present + if !controllerutil.ContainsFinalizer(&app, finalizerName) { + controllerutil.AddFinalizer(&app, finalizerName) + if err := r.Update(ctx, &app); err != nil { + log.Error(err, "unable to add finalizer") + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + + // Reconcile the NexusGateApp + return r.reconcile(ctx, &app) +} + +// reconcile handles the main reconciliation logic +func (r *NexusGateAppReconciler) reconcile(ctx context.Context, app *gatewayv1alpha1.NexusGateApp) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + // Update status to Pending if not set + if app.Status.Phase == "" { + app.Status.Phase = gatewayv1alpha1.PhasePending + if err := r.Status().Update(ctx, app); err != nil { + return ctrl.Result{}, err + } + } + + // Build external ID + externalID := r.buildExternalID(app) + log.Info("reconciling NexusGateApp", "externalID", externalID) + + // Ensure API key exists in NexusGate + apiKeyResp, err := r.NexusGate.EnsureAPIKey(ctx, externalID, app.Spec.AppName) + if err != nil { + log.Error(err, "failed to ensure API key") + return r.updateStatusError(ctx, app, fmt.Sprintf("failed to ensure API key: %v", err)) + } + + log.Info("API key ensured", "id", apiKeyResp.ID, "created", apiKeyResp.Created) + + // Sync the API key to the Secret + if err := r.syncSecret(ctx, app, apiKeyResp.Key); err != nil { + log.Error(err, "failed to sync secret") + return r.updateStatusError(ctx, app, fmt.Sprintf("failed to sync secret: %v", err)) + } + + // Update status to Ready + now := metav1.Now() + app.Status.Phase = gatewayv1alpha1.PhaseReady + app.Status.APIKeyID = apiKeyResp.ID + app.Status.APIKeyPrefix = maskAPIKey(apiKeyResp.Key) + app.Status.SecretSynced = true + app.Status.LastSyncTime = &now + app.Status.Message = "API key provisioned and synced successfully" + + if err := r.Status().Update(ctx, app); err != nil { + log.Error(err, "failed to update status") + return ctrl.Result{}, err + } + + // Requeue for periodic sync to detect revoked keys + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + +// handleDeletion handles the deletion of the NexusGateApp +func (r *NexusGateAppReconciler) handleDeletion(ctx context.Context, app *gatewayv1alpha1.NexusGateApp) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + if !controllerutil.ContainsFinalizer(app, finalizerName) { + return ctrl.Result{}, nil + } + + // Update status to Deleting + app.Status.Phase = gatewayv1alpha1.PhaseDeleting + if err := r.Status().Update(ctx, app); err != nil { + return ctrl.Result{}, err + } + + // Handle deletion based on policy + if app.Spec.DeletionPolicy == gatewayv1alpha1.DeletionPolicyRevoke { + externalID := r.buildExternalID(app) + log.Info("revoking API key due to deletion policy", "externalID", externalID) + + // Get the API key first + apiKeyResp, err := r.NexusGate.GetAPIKeyByExternalID(ctx, externalID) + if err != nil { + log.Error(err, "failed to get API key for revocation") + // Continue with deletion even if we can't get the key + } else if apiKeyResp != nil && !apiKeyResp.Revoked { + // Revoke the key + if err := r.NexusGate.RevokeAPIKey(ctx, apiKeyResp.Key); err != nil { + log.Error(err, "failed to revoke API key") + // Continue with deletion even if revocation fails + } else { + log.Info("API key revoked successfully") + } + } + } else { + log.Info("retaining API key due to deletion policy") + } + + // Remove finalizer + controllerutil.RemoveFinalizer(app, finalizerName) + if err := r.Update(ctx, app); err != nil { + return ctrl.Result{}, err + } + + log.Info("NexusGateApp deleted successfully") + return ctrl.Result{}, nil +} + +// syncSecret creates or updates the Secret with the API key +func (r *NexusGateAppReconciler) syncSecret(ctx context.Context, app *gatewayv1alpha1.NexusGateApp, apiKey string) error { + log := logf.FromContext(ctx) + + // Determine the secret namespace + secretNamespace := app.Spec.SecretRef.Namespace + if secretNamespace == "" { + secretNamespace = app.Namespace + } + + // Determine the secret key + secretKey := app.Spec.SecretRef.Key + if secretKey == "" { + secretKey = "NEXUSGATE_API_KEY" + } + + secretName := types.NamespacedName{ + Name: app.Spec.SecretRef.Name, + Namespace: secretNamespace, + } + + // Try to get existing secret + var existingSecret corev1.Secret + err := r.Get(ctx, secretName, &existingSecret) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get existing secret: %w", err) + } + + if errors.IsNotFound(err) { + // Create new secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: app.Spec.SecretRef.Name, + Namespace: secretNamespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "nexusgate-operator", + "nexusgate.io/app-name": app.Spec.AppName, + }, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + secretKey: apiKey, + }, + } + + // Set owner reference only if the secret is in the same namespace + if secretNamespace == app.Namespace { + if err := controllerutil.SetControllerReference(app, secret, r.Scheme); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + if err := r.Create(ctx, secret); err != nil { + return fmt.Errorf("failed to create secret: %w", err) + } + log.Info("secret created", "name", secretName.Name, "namespace", secretName.Namespace) + } else { + // Update existing secret + if existingSecret.Data == nil { + existingSecret.Data = make(map[string][]byte) + } + existingSecret.Data[secretKey] = []byte(apiKey) + + // Ensure labels are set + if existingSecret.Labels == nil { + existingSecret.Labels = make(map[string]string) + } + existingSecret.Labels["app.kubernetes.io/managed-by"] = "nexusgate-operator" + existingSecret.Labels["nexusgate.io/app-name"] = app.Spec.AppName + + if err := r.Update(ctx, &existingSecret); err != nil { + return fmt.Errorf("failed to update secret: %w", err) + } + log.Info("secret updated", "name", secretName.Name, "namespace", secretName.Namespace) + } + + return nil +} + +// updateStatusError updates the status to Error state +func (r *NexusGateAppReconciler) updateStatusError(ctx context.Context, app *gatewayv1alpha1.NexusGateApp, message string) (ctrl.Result, error) { + app.Status.Phase = gatewayv1alpha1.PhaseError + app.Status.Message = message + app.Status.SecretSynced = false + + if err := r.Status().Update(ctx, app); err != nil { + return ctrl.Result{}, err + } + + // Requeue with backoff for retry + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil +} + +// buildExternalID builds the external ID for the API key +func (r *NexusGateAppReconciler) buildExternalID(app *gatewayv1alpha1.NexusGateApp) string { + return fmt.Sprintf("k8s/%s/%s/%s", r.ClusterName, app.Namespace, app.Spec.AppName) +} + +// maskAPIKey returns a masked version of the API key for display +func maskAPIKey(key string) string { + if len(key) <= 8 { + return "***" + } + return key[:7] + "..." + key[len(key)-4:] +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NexusGateAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&gatewayv1alpha1.NexusGateApp{}). + Owns(&corev1.Secret{}). + Named("nexusgateapp"). + Complete(r) +} diff --git a/operator/internal/controller/nexusgateapp_controller_test.go b/operator/internal/controller/nexusgateapp_controller_test.go new file mode 100644 index 0000000..3558cf5 --- /dev/null +++ b/operator/internal/controller/nexusgateapp_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2026. + +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 controller + +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" + + gatewayv1alpha1 "github.com/EM-GeekLab/nexusgate-operator/api/v1alpha1" +) + +var _ = Describe("NexusGateApp 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 + } + nexusgateapp := &gatewayv1alpha1.NexusGateApp{} + + BeforeEach(func() { + By("creating the custom resource for the Kind NexusGateApp") + err := k8sClient.Get(ctx, typeNamespacedName, nexusgateapp) + if err != nil && errors.IsNotFound(err) { + resource := &gatewayv1alpha1.NexusGateApp{ + 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 := &gatewayv1alpha1.NexusGateApp{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance NexusGateApp") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &NexusGateAppReconciler{ + 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/operator/internal/controller/suite_test.go b/operator/internal/controller/suite_test.go new file mode 100644 index 0000000..48bec89 --- /dev/null +++ b/operator/internal/controller/suite_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2026. + +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 controller + +import ( + "context" + "os" + "path/filepath" + "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" + + gatewayv1alpha1 "github.com/EM-GeekLab/nexusgate-operator/api/v1alpha1" + // +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 ( + ctx context.Context + cancel context.CancelFunc + testEnv *envtest.Environment + cfg *rest.Config + k8sClient client.Client +) + +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()) + + var err error + err = gatewayv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + // Retrieve the first found binary directory to allow running tests from IDEs + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + 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()) +}) + +// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. +// ENVTEST-based tests depend on specific binaries, usually located in paths set by +// controller-runtime. When running tests directly (e.g., via an IDE) without using +// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. +// +// This function streamlines the process by finding the required binaries, similar to +// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are +// properly set up, run 'make setup-envtest' beforehand. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + logf.Log.Error(err, "Failed to read directory", "path", basePath) + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} diff --git a/operator/internal/nexusgate/client.go b/operator/internal/nexusgate/client.go new file mode 100644 index 0000000..6808bf3 --- /dev/null +++ b/operator/internal/nexusgate/client.go @@ -0,0 +1,198 @@ +/* +Copyright 2026. + +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 nexusgate + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +// Client is a NexusGate Admin API client +type Client struct { + baseURL string + adminToken string + httpClient *http.Client +} + +// APIKeyResponse represents the response from the API key endpoints +type APIKeyResponse struct { + Key string `json:"key"` + ID int `json:"id"` + Created bool `json:"created"` + ExternalID *string `json:"externalId"` + Revoked bool `json:"revoked"` +} + +// EnsureAPIKeyRequest represents the request body for ensuring an API key +type EnsureAPIKeyRequest struct { + Comment string `json:"comment,omitempty"` + RpmLimit int `json:"rpmLimit,omitempty"` + TpmLimit int `json:"tpmLimit,omitempty"` +} + +// NewClient creates a new NexusGate Admin API client +func NewClient(baseURL, adminToken string) *Client { + return &Client{ + baseURL: baseURL, + adminToken: adminToken, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// EnsureAPIKey ensures an API key exists for the given external ID (idempotent) +// If the key doesn't exist, it creates one. If it exists, it returns the existing key. +func (c *Client) EnsureAPIKey(ctx context.Context, externalID, comment string) (*APIKeyResponse, error) { + endpoint := fmt.Sprintf("%s/admin/apiKey/by-external-id/%s", c.baseURL, url.PathEscape(externalID)) + + reqBody := EnsureAPIKeyRequest{ + Comment: comment, + } + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.adminToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var result APIKeyResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} + +// GetAPIKeyByExternalID retrieves an API key by its external ID +func (c *Client) GetAPIKeyByExternalID(ctx context.Context, externalID string) (*APIKeyResponse, error) { + endpoint := fmt.Sprintf("%s/admin/apiKey/by-external-id/%s", c.baseURL, url.PathEscape(externalID)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.adminToken) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil // Key not found + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var result APIKeyResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} + +// RevokeAPIKey revokes an API key by its key value +func (c *Client) RevokeAPIKey(ctx context.Context, key string) error { + endpoint := fmt.Sprintf("%s/admin/apiKey/%s", c.baseURL, url.PathEscape(key)) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.adminToken) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + // Key already doesn't exist, consider it as success + return nil + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + return nil +} + +// HealthCheck performs a health check against the NexusGate API +func (c *Client) HealthCheck(ctx context.Context) error { + endpoint := fmt.Sprintf("%s/admin/apiKey", c.baseURL) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.adminToken) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health check failed: status %d", resp.StatusCode) + } + + return nil +} diff --git a/operator/test/e2e/e2e_suite_test.go b/operator/test/e2e/e2e_suite_test.go new file mode 100644 index 0000000..c2fc62b --- /dev/null +++ b/operator/test/e2e/e2e_suite_test.go @@ -0,0 +1,92 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2026. + +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" + "os/exec" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/EM-GeekLab/nexusgate-operator/test/utils" +) + +var ( + // Optional Environment Variables: + // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. + // These variables are useful if CertManager is already installed, avoiding + // re-installation and conflicts. + skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" + // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster + isCertManagerAlreadyInstalled = false + + // projectImage is the name of the image which will be build and loaded + // with the code source changes to be tested. + projectImage = "example.com/operator:v0.0.1" +) + +// TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, +// temporary environment to validate project changes with the purpose of being used in CI jobs. +// The default setup requires Kind, builds/loads the Manager Docker image locally, and installs +// CertManager. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + _, _ = fmt.Fprintf(GinkgoWriter, "Starting operator integration test suite\n") + RunSpecs(t, "e2e suite") +} + +var _ = BeforeSuite(func() { + 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(), "Failed to build the manager(Operator) image") + + // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is + // built and available before running the tests. Also, remove the following block. + By("loading the manager(Operator) image on Kind") + err = utils.LoadImageToKindClusterWithName(projectImage) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") + + // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. + // To prevent errors when tests run in environments with CertManager already installed, + // we check for its presence before execution. + // Setup CertManager before the suite if not skipped and if not already installed + if !skipCertManagerInstall { + By("checking if cert manager is installed already") + isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() + if !isCertManagerAlreadyInstalled { + _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") + Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") + } + } +}) + +var _ = AfterSuite(func() { + // Teardown CertManager after the suite if not skipped and if it was not already installed + if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { + _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") + utils.UninstallCertManager() + } +}) diff --git a/operator/test/e2e/e2e_test.go b/operator/test/e2e/e2e_test.go new file mode 100644 index 0000000..c4f0605 --- /dev/null +++ b/operator/test/e2e/e2e_test.go @@ -0,0 +1,337 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2026. + +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 ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/EM-GeekLab/nexusgate-operator/test/utils" +) + +// namespace where the project is deployed in +const namespace = "operator-system" + +// serviceAccountName created for the project +const serviceAccountName = "operator-controller-manager" + +// metricsServiceName is the name of the metrics service of the project +const metricsServiceName = "operator-controller-manager-metrics-service" + +// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data +const metricsRoleBindingName = "operator-metrics-binding" + +var _ = Describe("Manager", Ordered, func() { + var controllerPodName string + + // Before running the tests, set up the environment by creating the namespace, + // enforce the restricted security policy to the namespace, installing CRDs, + // and deploying the controller. + BeforeAll(func() { + By("creating manager namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") + + By("labeling the namespace to enforce the restricted security policy") + cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, + "pod-security.kubernetes.io/enforce=restricted") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") + + By("installing CRDs") + cmd = exec.Command("make", "install") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") + + By("deploying the controller-manager") + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") + }) + + // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, + // and deleting the namespace. + AfterAll(func() { + By("cleaning up the curl pod for metrics") + cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) + _, _ = utils.Run(cmd) + + By("undeploying the controller-manager") + cmd = exec.Command("make", "undeploy") + _, _ = utils.Run(cmd) + + By("uninstalling CRDs") + cmd = exec.Command("make", "uninstall") + _, _ = utils.Run(cmd) + + By("removing manager namespace") + cmd = exec.Command("kubectl", "delete", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + // After each test, check for failures and collect logs, events, + // and pod descriptions for debugging. + AfterEach(func() { + specReport := CurrentSpecReport() + if specReport.Failed() { + By("Fetching controller manager pod logs") + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) + controllerLogs, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) + } + + By("Fetching Kubernetes events") + cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") + eventsOutput, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) + } + + By("Fetching curl-metrics logs") + cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) + metricsOutput, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) + } + + By("Fetching controller manager pod description") + cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) + podDescription, err := utils.Run(cmd) + if err == nil { + fmt.Println("Pod description:\n", podDescription) + } else { + fmt.Println("Failed to describe controller pod") + } + } + }) + + SetDefaultEventuallyTimeout(2 * time.Minute) + SetDefaultEventuallyPollingInterval(time.Second) + + Context("Manager", func() { + It("should run successfully", func() { + By("validating that the controller-manager pod is running as expected") + verifyControllerUp := func(g Gomega) { + // Get the name of the controller-manager pod + cmd := exec.Command("kubectl", "get", + "pods", "-l", "control-plane=controller-manager", + "-o", "go-template={{ range .items }}"+ + "{{ if not .metadata.deletionTimestamp }}"+ + "{{ .metadata.name }}"+ + "{{ \"\\n\" }}{{ end }}{{ end }}", + "-n", namespace, + ) + + podOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") + podNames := utils.GetNonEmptyLines(podOutput) + g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") + controllerPodName = podNames[0] + g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) + + // Validate the pod's status + cmd = exec.Command("kubectl", "get", + "pods", controllerPodName, "-o", "jsonpath={.status.phase}", + "-n", namespace, + ) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") + } + Eventually(verifyControllerUp).Should(Succeed()) + }) + + It("should ensure the metrics endpoint is serving metrics", func() { + By("creating a ClusterRoleBinding for the service account to allow access to metrics") + cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, + "--clusterrole=operator-metrics-reader", + fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), + ) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") + + By("validating that the metrics service is available") + cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") + + By("getting the service account token") + token, err := serviceAccountToken() + Expect(err).NotTo(HaveOccurred()) + Expect(token).NotTo(BeEmpty()) + + By("ensuring the controller pod is ready") + verifyControllerPodReady := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal("True"), "Controller pod not ready") + } + Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed()) + + By("verifying that the controller manager is serving the metrics server") + verifyMetricsServerStarted := func(g Gomega) { + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Serving metrics server"), + "Metrics server not yet started") + } + Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed()) + + // +kubebuilder:scaffold:e2e-metrics-webhooks-readiness + + By("creating the curl-metrics pod to access the metrics endpoint") + cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", + "--namespace", namespace, + "--image=curlimages/curl:latest", + "--overrides", + fmt.Sprintf(`{ + "spec": { + "containers": [{ + "name": "curl", + "image": "curlimages/curl:latest", + "command": ["/bin/sh", "-c"], + "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], + "securityContext": { + "readOnlyRootFilesystem": true, + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + } + }], + "serviceAccountName": "%s" + } + }`, token, metricsServiceName, namespace, serviceAccountName)) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") + + By("waiting for the curl-metrics pod to complete.") + verifyCurlUp := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", + "-o", "jsonpath={.status.phase}", + "-n", namespace) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") + } + Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) + + By("getting the metrics by checking curl-metrics logs") + verifyMetricsAvailable := func(g Gomega) { + metricsOutput, err := getMetricsOutput() + g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") + g.Expect(metricsOutput).NotTo(BeEmpty()) + g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) + } + Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed()) + }) + + // +kubebuilder:scaffold:e2e-webhooks-checks + + // TODO: Customize the e2e test suite with scenarios specific to your project. + // Consider applying sample/CR(s) and check their status and/or verifying + // the reconciliation by using the metrics, i.e.: + // metricsOutput, err := getMetricsOutput() + // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") + // Expect(metricsOutput).To(ContainSubstring( + // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, + // strings.ToLower(), + // )) + }) +}) + +// serviceAccountToken returns a token for the specified service account in the given namespace. +// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request +// and parsing the resulting token from the API response. +func serviceAccountToken() (string, error) { + const tokenRequestRawString = `{ + "apiVersion": "authentication.k8s.io/v1", + "kind": "TokenRequest" + }` + + // Temporary file to store the token request + secretName := fmt.Sprintf("%s-token-request", serviceAccountName) + tokenRequestFile := filepath.Join("/tmp", secretName) + err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) + if err != nil { + return "", err + } + + var out string + verifyTokenCreation := func(g Gomega) { + // Execute kubectl command to create the token + cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( + "/api/v1/namespaces/%s/serviceaccounts/%s/token", + namespace, + serviceAccountName, + ), "-f", tokenRequestFile) + + output, err := cmd.CombinedOutput() + g.Expect(err).NotTo(HaveOccurred()) + + // Parse the JSON output to extract the token + var token tokenRequest + err = json.Unmarshal(output, &token) + g.Expect(err).NotTo(HaveOccurred()) + + out = token.Status.Token + } + Eventually(verifyTokenCreation).Should(Succeed()) + + return out, err +} + +// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. +func getMetricsOutput() (string, error) { + By("getting the curl-metrics logs") + cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) + return utils.Run(cmd) +} + +// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, +// containing only the token field that we need to extract. +type tokenRequest struct { + Status struct { + Token string `json:"token"` + } `json:"status"` +} diff --git a/operator/test/utils/utils.go b/operator/test/utils/utils.go new file mode 100644 index 0000000..495bc7f --- /dev/null +++ b/operator/test/utils/utils.go @@ -0,0 +1,226 @@ +/* +Copyright 2026. + +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 ( + "bufio" + "bytes" + "fmt" + "os" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck +) + +const ( + certmanagerVersion = "v1.19.1" + certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" + + defaultKindBinary = "kind" + defaultKindCluster = "kind" +) + +func warnError(err error) { + _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) +} + +// Run executes the provided command within this context +func Run(cmd *exec.Cmd) (string, error) { + dir, _ := GetProjectDir() + cmd.Dir = dir + + if err := os.Chdir(cmd.Dir); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) + } + + cmd.Env = append(os.Environ(), "GO111MODULE=on") + command := strings.Join(cmd.Args, " ") + _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) + } + + return string(output), nil +} + +// 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) + } + + // Delete leftover leases in kube-system (not cleaned by default) + kubeSystemLeases := []string{ + "cert-manager-cainjector-leader-election", + "cert-manager-controller", + } + for _, lease := range kubeSystemLeases { + cmd = exec.Command("kubectl", "delete", "lease", lease, + "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0") + 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 +} + +// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed +// by verifying the existence of key CRDs related to Cert Manager. +func IsCertManagerCRDsInstalled() bool { + // List of common Cert Manager CRDs + certManagerCRDs := []string{ + "certificates.cert-manager.io", + "issuers.cert-manager.io", + "clusterissuers.cert-manager.io", + "certificaterequests.cert-manager.io", + "orders.acme.cert-manager.io", + "challenges.acme.cert-manager.io", + } + + // Execute the kubectl command to get all CRDs + cmd := exec.Command("kubectl", "get", "crds") + output, err := Run(cmd) + if err != nil { + return false + } + + // Check if any of the Cert Manager CRDs are present + crdList := GetNonEmptyLines(output) + for _, crd := range certManagerCRDs { + for _, line := range crdList { + if strings.Contains(line, crd) { + return true + } + } + } + + return false +} + +// LoadImageToKindClusterWithName loads a local docker image to the kind cluster +func LoadImageToKindClusterWithName(name string) error { + cluster := defaultKindCluster + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { + cluster = v + } + kindOptions := []string{"load", "docker-image", name, "--name", cluster} + kindBinary := defaultKindBinary + if v, ok := os.LookupEnv("KIND"); ok { + kindBinary = v + } + cmd := exec.Command(kindBinary, 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, fmt.Errorf("failed to get current working directory: %w", err) + } + wd = strings.ReplaceAll(wd, "/test/e2e", "") + return wd, nil +} + +// UncommentCode searches for target in the file and remove the comment prefix +// of the target content. The target content may span multiple lines. +func UncommentCode(filename, target, prefix string) error { + // false positive + // nolint:gosec + content, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file %q: %w", filename, err) + } + strContent := string(content) + + idx := strings.Index(strContent, target) + if idx < 0 { + return fmt.Errorf("unable to find the code %q to be uncomment", target) + } + + out := new(bytes.Buffer) + _, err = out.Write(content[:idx]) + if err != nil { + return fmt.Errorf("failed to write to output: %w", err) + } + + scanner := bufio.NewScanner(bytes.NewBufferString(target)) + if !scanner.Scan() { + return nil + } + for { + if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { + return fmt.Errorf("failed to write to output: %w", err) + } + // Avoid writing a newline in case the previous line was the last in target. + if !scanner.Scan() { + break + } + if _, err = out.WriteString("\n"); err != nil { + return fmt.Errorf("failed to write to output: %w", err) + } + } + + if _, err = out.Write(content[idx+len(target):]); err != nil { + return fmt.Errorf("failed to write to output: %w", err) + } + + // false positive + // nolint:gosec + if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil { + return fmt.Errorf("failed to write file %q: %w", filename, err) + } + + return nil +}