From 64091a6439911c928256e496f1e6d89e04b8767d Mon Sep 17 00:00:00 2001 From: "M. Essam" Date: Wed, 29 Jan 2025 23:44:09 +0200 Subject: [PATCH] Add initial version with help and admission controller (#1) - It adds a helm chart that will be hosted in the Github pages URL of this repository - an admission controller operator - Basic documentation for installing the operator, configuring CRDs and example pod configuration --- .dockerignore | 3 + .github/workflows/docker.yml | 69 +++ .github/workflows/helm.yml | 36 ++ .github/workflows/lint.yml | 23 + .github/workflows/test-chart.yml | 68 +++ .github/workflows/test-e2e.yml | 35 ++ .github/workflows/test.yml | 23 + .gitignore | 20 +- .golangci.yml | 47 ++ .goreleaser.yaml | 83 +++ Dockerfile | 35 ++ Dockerfile.release | 8 + Makefile | 212 ++++++++ PROJECT | 32 ++ README.md | 124 +++++ api/v1/groupversion_info.go | 36 ++ api/v1/nbsetupkey_types.go | 90 ++++ api/v1/zz_generated.deepcopy.go | 123 +++++ cmd/main.go | 204 ++++++++ dist/install.yaml | 466 +++++++++++++++++ go.mod | 73 +++ go.sum | 190 +++++++ hack/boilerplate.go.txt | 0 helm/netbird-operator/.helmignore | 23 + helm/netbird-operator/Chart.yaml | 6 + .../crds/netbird.io_nbsetupkeys.yaml | 116 +++++ helm/netbird-operator/templates/NOTES.txt | 0 helm/netbird-operator/templates/_helpers.tpl | 102 ++++ .../templates/deployment.yaml | 103 ++++ helm/netbird-operator/templates/rbac.yaml | 116 +++++ helm/netbird-operator/templates/service.yaml | 33 ++ .../templates/serviceaccount.yaml | 13 + helm/netbird-operator/templates/webhook.yaml | 134 +++++ helm/netbird-operator/values.yaml | 130 +++++ internal/controller/nbsetupkey_controller.go | 156 ++++++ .../controller/nbsetupkey_controller_test.go | 200 ++++++++ internal/controller/suite_test.go | 116 +++++ internal/webhook/v1/nbsetupkey_webhook.go | 113 +++++ .../webhook/v1/nbsetupkey_webhook_test.go | 124 +++++ internal/webhook/v1/pod_webhook.go | 127 +++++ internal/webhook/v1/pod_webhook_test.go | 130 +++++ internal/webhook/v1/webhook_suite_test.go | 176 +++++++ test/e2e/e2e_suite_test.go | 83 +++ test/e2e/e2e_test.go | 472 ++++++++++++++++++ test/utils/utils.go | 174 +++++++ 45 files changed, 4638 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/helm.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test-chart.yml create mode 100644 .github/workflows/test-e2e.yml create mode 100644 .github/workflows/test.yml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile create mode 100644 Dockerfile.release create mode 100644 Makefile create mode 100644 PROJECT create mode 100644 README.md create mode 100644 api/v1/groupversion_info.go create mode 100644 api/v1/nbsetupkey_types.go create mode 100644 api/v1/zz_generated.deepcopy.go create mode 100644 cmd/main.go create mode 100644 dist/install.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100644 helm/netbird-operator/.helmignore create mode 100644 helm/netbird-operator/Chart.yaml create mode 100644 helm/netbird-operator/crds/netbird.io_nbsetupkeys.yaml create mode 100644 helm/netbird-operator/templates/NOTES.txt create mode 100644 helm/netbird-operator/templates/_helpers.tpl create mode 100644 helm/netbird-operator/templates/deployment.yaml create mode 100644 helm/netbird-operator/templates/rbac.yaml create mode 100644 helm/netbird-operator/templates/service.yaml create mode 100644 helm/netbird-operator/templates/serviceaccount.yaml create mode 100644 helm/netbird-operator/templates/webhook.yaml create mode 100644 helm/netbird-operator/values.yaml create mode 100644 internal/controller/nbsetupkey_controller.go create mode 100644 internal/controller/nbsetupkey_controller_test.go create mode 100644 internal/controller/suite_test.go create mode 100644 internal/webhook/v1/nbsetupkey_webhook.go create mode 100644 internal/webhook/v1/nbsetupkey_webhook_test.go create mode 100644 internal/webhook/v1/pod_webhook.go create mode 100644 internal/webhook/v1/pod_webhook_test.go create mode 100644 internal/webhook/v1/webhook_suite_test.go create mode 100644 test/e2e/e2e_suite_test.go create mode 100644 test/e2e/e2e_test.go create mode 100644 test/utils/utils.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a3aab7a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..35b157e --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,69 @@ +name: Docker + +on: + push: + tags: + - "v*" + branches: + - main + pull_request: + +env: + GORELEASER_VER: "v2.3.2" + +jobs: + release: + env: + flags: "" + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Parse semver string + id: semver_parser + uses: booxmedialtd/ws-action-parse-semver@v1 + with: + input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }} + version_extractor_regex: '\/v(.*)$' + + - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + run: echo "flags=--snapshot" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # It is required for GoReleaser to work properly + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Login to Docker hub + if: github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Install OS build dependencies + run: sudo apt update && sudo apt install -y -q gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu + + - name: Install goversioninfo + run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: ${{ env.GORELEASER_VER }} + args: release --clean ${{ env.flags }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 0000000..7d412ab --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,36 @@ +name: Release Helm Chart and Carvel package + +on: + push: + paths: + # update this file to trigger helm chart release + - 'helm/netbird-operator/Chart.yaml' + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3.1.0 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v3.4 + with: + version: v3.4.2 + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.4.1 + with: + charts_dir: helm + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_RELEASE_NAME_TEMPLATE: "helm-v{{ .Version }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4951e33 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint + +on: + push: + pull_request: + +jobs: + lint: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run linter + uses: golangci/golangci-lint-action@v6 + with: + version: v1.63.4 diff --git a/.github/workflows/test-chart.yml b/.github/workflows/test-chart.yml new file mode 100644 index 0000000..a3bf8fd --- /dev/null +++ b/.github/workflows/test-chart.yml @@ -0,0 +1,68 @@ +name: Test Chart + +on: + push: + pull_request: + +jobs: + test-e2e: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install the latest version of kind + run: | + curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 + chmod +x ./kind + sudo mv ./kind /usr/local/bin/kind + + - name: Verify kind installation + run: kind version + + - name: Create kind cluster + run: kind create cluster + + - name: Prepare operator + run: | + go mod tidy + make docker-build IMG=netbirdio/kubernetes-operator:debug + kind load docker-image netbirdio/kubernetes-operator:debug + + - name: Install Helm + run: | + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Verify Helm installation + run: helm version + + - name: Lint Helm Chart + run: | + helm lint ./helm/netbird-operator + + - name: Install cert-manager via Helm + run: | + helm repo add jetstack https://charts.jetstack.io + helm repo update + helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true + + - name: Wait for cert-manager to be ready + run: | + kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager + kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector + kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook + + - name: Install Helm chart for project + run: | + helm install test-chart --create-namespace --namespace netbird --set 'operator.image.tag=debug' ./helm/netbird-operator + + - name: Check Helm release status + run: | + helm status test-chart --namespace netbird + \ No newline at end of file diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml new file mode 100644 index 0000000..1da94d2 --- /dev/null +++ b/.github/workflows/test-e2e.yml @@ -0,0 +1,35 @@ +name: E2E Tests + +on: + push: + pull_request: + +jobs: + test-e2e: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install the latest version of kind + run: | + curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 + chmod +x ./kind + sudo mv ./kind /usr/local/bin/kind + + - name: Verify kind installation + run: kind version + + - name: Create kind cluster + run: kind create cluster + + - name: Running Test e2e + run: | + go mod tidy + make test-e2e diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fc2e80d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + test: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Running Tests + run: | + go mod tidy + make test diff --git a/.gitignore b/.gitignore index 6f72f89..ada68ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib +bin/* +Dockerfile.cross # Test binary, built with `go test -c` *.test @@ -14,12 +13,15 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -# Dependency directories (remove the comment below to include it) -# vendor/ - # Go workspace file go.work -go.work.sum -# env file -.env +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6b29746 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,47 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - copyloopvar + - ginkgolinter + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + revive: + rules: + - name: comment-spacings diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..2d9ad71 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,83 @@ +version: 2 + +project_name: kubernetes-operator + +builds: + - id: manager + dir: cmd + binary: manager + env: [CGO_ENABLED=0] + goos: + - linux + goarch: + - arm + - amd64 + - arm64 + ldflags: + - -s -w + mod_timestamp: "{{ .CommitTimestamp }}" + +dockers: + - image_templates: + - netbirdio/kubernetes-operator:{{ .Version }}-amd64 + ids: + - manager + goarch: amd64 + dockerfile: Dockerfile.release + use: buildx + skip_push: false + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=maintainer=dev@netbird.io" + - image_templates: + - netbirdio/kubernetes-operator:{{ .Version }}-arm64v8 + ids: + - manager + goarch: arm64 + dockerfile: Dockerfile.release + use: buildx + skip_push: false + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=maintainer=dev@netbird.io" + - image_templates: + - netbirdio/kubernetes-operator:{{ .Version }}-arm + ids: + - manager + goarch: arm + goarm: 6 + dockerfile: Dockerfile.release + use: buildx + skip_push: false + build_flag_templates: + - "--platform=linux/arm" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=maintainer=dev@netbird.io" + +docker_manifests: + - name_template: netbirdio/kubernetes-operator:{{ .Version }} + skip_push: false + image_templates: + - netbirdio/kubernetes-operator:{{ .Version }}-arm64v8 + - netbirdio/kubernetes-operator:{{ .Version }}-arm + - netbirdio/kubernetes-operator:{{ .Version }}-amd64 + + - name_template: netbirdio/kubernetes-operator:latest + image_templates: + - netbirdio/kubernetes-operator:{{ .Version }}-arm64v8 + - netbirdio/kubernetes-operator:{{ .Version }}-arm + - netbirdio/kubernetes-operator:{{ .Version }}-amd64 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e3e7b0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# This dockerfile is used for tests and local builds + +# Build the manager binary +FROM docker.io/golang:1.23 AS builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/ internal/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags="-w -s" -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Dockerfile.release b/Dockerfile.release new file mode 100644 index 0000000..be7aabc --- /dev/null +++ b/Dockerfile.release @@ -0,0 +1,8 @@ +# This dockerfile is used for goreleaser + +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9f40397 --- /dev/null +++ b/Makefile @@ -0,0 +1,212 @@ +# Image URL to use all building/pushing image targets +IMG ?= docker.io/netbirdio/kubernetes-operator:latest + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=helm/netbird-operator/crds + +.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 fmt vet setup-envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -v $$(go list ./... | grep -v /e2e) -coverprofile cover.out + +.PHONY: test-e2e +test-e2e: manifests fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. + @command -v kind >/dev/null 2>&1 || { \ + echo "Kind is not installed. Please install Kind manually."; \ + exit 1; \ + } + @kind get clusters | grep -q 'kind' || { \ + echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ + exit 1; \ + } + go test ./test/e2e/ -v -ginkgo.v + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +.PHONY: lint-config +lint-config: golangci-lint ## Verify golangci-lint linter configuration + $(GOLANGCI_LINT) config verify + +##@ Build + +.PHONY: build +build: manifests fmt vet ## Build manager binary. + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests 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 a consolidated YAML with CRDs and deployment. + mkdir -p dist + $(HELM) template --include-crds netbird-operator helm/netbird-operator > dist/install.yaml + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUBECTL) apply -f helm/netbird-operator/crds + +.PHONY: uninstall +uninstall: manifests ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUBECTL) delete -f helm/netbird-operator/crds + +.PHONY: deploy +deploy: manifests ## Deploy controller to the K8s cluster specified in ~/.kube/config. + $(HELM) install -n netbird --create-namespace netbird-operator --set operator.image.tag=$(word 2,$(subst :, ,${IMG})) helm/netbird-operator + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(HELM) uninstall -n netbird netbird-operator + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +HELM ?= helm + +## Tool Versions +CONTROLLER_TOOLS_VERSION ?= v0.17.1 +#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) +ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') +#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) +ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') +GOLANGCI_LINT_VERSION ?= v1.63.4 + +.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/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)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef diff --git a/PROJECT b/PROJECT new file mode 100644 index 0000000..42072a5 --- /dev/null +++ b/PROJECT @@ -0,0 +1,32 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: netbird.io +layout: +- helm.kubebuilder.io/v1-alpha +- go.kubebuilder.io/v4 +plugins: + helm.kubebuilder.io/v1-alpha: {} +projectName: kubernetes-operator +repo: github.com/netbirdio/kubernetes-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: netbird.io + kind: NBSetupKey + path: github.com/netbirdio/kubernetes-operator/api/v1 + version: v1 + webhooks: + validation: true + webhookVersion: v1 +- external: true + kind: Pod + path: k8s.io/api/core/v1 + version: v1 + webhooks: + defaulting: true + webhookVersion: v1 +version: "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc9f5c7 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# NetBird Kubernetes Operator +For easily provisioning access to Kubernetes resources using NetBird. + +## Description + +This operator enables easily provisioning NetBird access on kubernetes clusters, allowing users to access internal resources directly. + +## Getting Started + +### Prerequisites +- helm version 3+ +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. +- (Optional for Helm chart installation) Cert Manager. + +### To Deploy on the cluster + +**Using the install.yaml** + +```sh +kubectl create namespace netbird +kubectl apply -n netbird -f https://github.com/netbirdio/kubernetes-operator/releases/latest/dist/install.yaml +``` + +**Using the Helm Chart** + +```sh +helm repo add netbirdio https://netbirdio.github.io/kubernetes-operator +helm install -n netbird netbird-operator netbirdio/netbird-operator +``` + +For more options, check the default values by running +```sh +helm show values netbirdio/netbird-operator +``` + +### To Uninstall +**Using install.yaml** + +```sh +kubectl delete -n netbird -f https://github.com/netbirdio/kubernetes-operator/releases/latest/dist/install.yaml +kubectl delete namespace netbird +``` + +**Using helm** + +```sh +helm uninstall -n netbird netbird-operator +``` + +### Provision pods with NetBird access + +1. Create a Setup Key in your [NetBird console](https://docs.netbird.io/how-to/register-machines-using-setup-keys#using-setup-keys). +1. Create a Secret object in the namespace where you need to provision NetBird access (secret name and field can be anything). +```yaml +apiVersion: v1 +stringData: + setupkey: EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE +kind: Secret +metadata: + name: test +``` +1. Create an NBSetupKey object referring to your secret. +```yaml +apiVersion: netbird.io/v1 +kind: NBSetupKey +metadata: + name: test +spec: + # Optional, overrides management URL for this setupkey only + # defaults to https://api.netbird.io + managementURL: https://netbird.example.com + secretKeyRef: + name: test # Required + key: setupkey # Required +``` +1. Annotate the pods you need to inject NetBird into with `netbird.io/setup-key`. +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + annotations: + netbird.io/setup-key: test # Must match the name of an NBSetupKey object in the same namespace + spec: + containers: + - image: yourimage + name: container + +``` + +## Contributing + +### Prerequisites + +To be able to develop on this project, you need to have the following tools installed: + +- [Git](https://git-scm.com/). +- [Make](https://www.gnu.org/software/make/). +- [Go programming language](https://golang.org/dl/). +- [Docker CE](https://www.docker.com/community-edition). +- [Kubernetes cluster (v1.16+)](https://kubernetes.io/docs/setup/). [KIND](https://github.com/kubernetes-sigs/kind) is recommended. +- [Kubebuilder](https://book.kubebuilder.io/). + +### Running tests + +**Running unit tests** +```sh +make test +``` + +**Running E2E tests** +```sh +kind create cluster # If not already created, you can check with `kind get clusters` +make test-e2e +``` \ No newline at end of file diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go new file mode 100644 index 0000000..1b9fcb9 --- /dev/null +++ b/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the v1 API group. +// +kubebuilder:object:generate=true +// +groupName=netbird.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "netbird.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1/nbsetupkey_types.go b/api/v1/nbsetupkey_types.go new file mode 100644 index 0000000..1678b41 --- /dev/null +++ b/api/v1/nbsetupkey_types.go @@ -0,0 +1,90 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NBSetupKeyConditionType is a valid value for PodCondition.Type +type NBSetupKeyConditionType string + +// These are built-in conditions of pod. An application may use a custom condition not listed here. +const ( + // Ready indicates whether NBSetupKey is valid and ready to use. + Ready NBSetupKeyConditionType = "Ready" +) + +// NBSetupKeySpec defines the desired state of NBSetupKey. +type NBSetupKeySpec struct { + // SecretKeyRef is a reference to the secret containing the setup key + SecretKeyRef corev1.SecretKeySelector `json:"secretKeyRef"` + // ManagementURL optional, override operator management URL + ManagementURL string `json:"managementURL,omitempty"` +} + +// NBSetupKeyStatus defines the observed state of NBSetupKey. +type NBSetupKeyStatus struct { + Conditions []NBSetupKeyCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=conditions"` +} + +// NBSetupKeyCondition defines a condition in NBSetupKey status. +type NBSetupKeyCondition struct { + // Type is the type of the condition. + Type NBSetupKeyConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=NBSetupKeyConditionType"` + // Status is the status of the condition. + // Can be True, False, Unknown. + Status corev1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=ConditionStatus"` + // Last time we probed the condition. + // +optional + LastProbeTime metav1.Time `json:"lastProbeTime,omitempty" protobuf:"bytes,3,opt,name=lastProbeTime"` + // Last time the condition transitioned from one status to another. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastTransitionTime"` + // Unique, one-word, CamelCase reason for the condition's last transition. + // +optional + Reason string `json:"reason,omitempty" protobuf:"bytes,5,opt,name=reason"` + // Human-readable message indicating details about last transition. + // +optional + Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// NBSetupKey is the Schema for the nbsetupkeys API. +type NBSetupKey struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NBSetupKeySpec `json:"spec,omitempty"` + Status NBSetupKeyStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// NBSetupKeyList contains a list of NBSetupKey. +type NBSetupKeyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NBSetupKey `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NBSetupKey{}, &NBSetupKeyList{}) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..fdfc726 --- /dev/null +++ b/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,123 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NBSetupKey) DeepCopyInto(out *NBSetupKey) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NBSetupKey. +func (in *NBSetupKey) DeepCopy() *NBSetupKey { + if in == nil { + return nil + } + out := new(NBSetupKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NBSetupKey) 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 *NBSetupKeyCondition) DeepCopyInto(out *NBSetupKeyCondition) { + *out = *in + in.LastProbeTime.DeepCopyInto(&out.LastProbeTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NBSetupKeyCondition. +func (in *NBSetupKeyCondition) DeepCopy() *NBSetupKeyCondition { + if in == nil { + return nil + } + out := new(NBSetupKeyCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NBSetupKeyList) DeepCopyInto(out *NBSetupKeyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NBSetupKey, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NBSetupKeyList. +func (in *NBSetupKeyList) DeepCopy() *NBSetupKeyList { + if in == nil { + return nil + } + out := new(NBSetupKeyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NBSetupKeyList) 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 *NBSetupKeySpec) DeepCopyInto(out *NBSetupKeySpec) { + *out = *in + in.SecretKeyRef.DeepCopyInto(&out.SecretKeyRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NBSetupKeySpec. +func (in *NBSetupKeySpec) DeepCopy() *NBSetupKeySpec { + if in == nil { + return nil + } + out := new(NBSetupKeySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NBSetupKeyStatus) DeepCopyInto(out *NBSetupKeyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]NBSetupKeyCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NBSetupKeyStatus. +func (in *NBSetupKeyStatus) DeepCopy() *NBSetupKeyStatus { + if in == nil { + return nil + } + out := new(NBSetupKeyStatus) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..d1a392b --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,204 @@ +/* +Copyright 2025. + +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" + "path/filepath" + + // 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/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + corev1 "k8s.io/api/core/v1" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" + "github.com/netbirdio/kubernetes-operator/internal/controller" + webhookk8siov1 "github.com/netbirdio/kubernetes-operator/internal/webhook/v1" + webhooknetbirdiov1 "github.com/netbirdio/kubernetes-operator/internal/webhook/v1" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(netbirdiov1.AddToScheme(scheme)) + utilruntime.Must(corev1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +// nolint:gocyclo +func main() { + // NB Specific flags + var ( + managementURL string + clientImage string + ) + flag.StringVar(&managementURL, "netbird-management-url", "https://api.netbird.io", "Management service URL") + flag.StringVar(&clientImage, "netbird-client-image", "netbirdio/netbird:latest", "Image for netbird client container") + + // Controller generic flags + var ( + metricsAddr string + webhookCertPath string + webhookCertName string + webhookCertKey string + enableLeaderElection bool + probeAddr string + enableHTTP2 bool + enableWebhooks bool + tlsOpts []func(*tls.Config) + ) + + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.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.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.BoolVar(&enableWebhooks, "enable-webhooks", true, "If set, enable Mutating and Validating webhooks.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + // Create watcher for webhooks certificates + var webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + 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) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(webhookCertPath, webhookCertName), + filepath.Join(webhookCertPath, webhookCertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: webhookTLSOpts, + }) + + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + TLSOpts: tlsOpts, + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "operator.netbird.io", + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + nbSetupKeyController := &controller.NBSetupKeyReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + if err = nbSetupKeyController.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "NBSetupKey") + os.Exit(1) + } + + if enableWebhooks { + if err = webhookk8siov1.SetupPodWebhookWithManager(mgr, managementURL, clientImage); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Pod") + os.Exit(1) + } + + if err = webhooknetbirdiov1.SetupNBSetupKeyWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "NBSetupKey") + os.Exit(1) + } + } + // +kubebuilder:scaffold:builder + + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } + + 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/dist/install.yaml b/dist/install.yaml new file mode 100644 index 0000000..6b1698f --- /dev/null +++ b/dist/install.yaml @@ -0,0 +1,466 @@ +--- +# Source: netbird-operator/crds/netbird.io_nbsetupkeys.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 + name: nbsetupkeys.netbird.io +spec: + group: netbird.io + names: + kind: NBSetupKey + listKind: NBSetupKeyList + plural: nbsetupkeys + singular: nbsetupkey + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: NBSetupKey is the Schema for the nbsetupkeys 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: NBSetupKeySpec defines the desired state of NBSetupKey. + properties: + managementURL: + description: ManagementURL optional, override operator management + URL + type: string + secretKeyRef: + description: SecretKeyRef is a reference to the secret containing + the setup key + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - secretKeyRef + type: object + status: + description: NBSetupKeyStatus defines the observed state of NBSetupKey. + properties: + ready: + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} + +--- +# Source: netbird-operator/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: netbird-operator + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +automountServiceAccountToken: true +--- +# Source: netbird-operator/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: netbird-operator + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +rules: +- apiGroups: + - netbird.io + resources: + - nbsetupkeys + verbs: + - get + - list + - watch +- apiGroups: + - netbird.io + resources: + - nbsetupkeys/finalizers + verbs: + - update +- apiGroups: + - netbird.io + resources: + - nbsetupkeys/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +--- +# Source: netbird-operator/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: netbird-operator + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: netbird-operator +subjects: +- kind: ServiceAccount + name: netbird-operator + namespace: default +--- +# Source: netbird-operator/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: netbird-operator + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +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 +--- +# Source: netbird-operator/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: netbird-operator + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: netbird-operator +subjects: +- kind: ServiceAccount + name: netbird-operator + namespace: default +--- +# Source: netbird-operator/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: netbird-operator-metrics + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator +--- +# Source: netbird-operator/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: netbird-operator-webhook-service + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 9443 + selector: + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator +--- +# Source: netbird-operator/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: netbird-operator + labels: + app.kubernetes.io/component: operator + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + template: + metadata: + labels: + app.kubernetes.io/component: operator + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm + spec: + serviceAccountName: netbird-operator + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: netbird-operator + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: "docker.io/netbirdio/kubernetes-operator:v0.1.0" + imagePullPolicy: IfNotPresent + command: + - /manager + args: + - --metrics-bind-address=:8080 + - --leader-elect + - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + ports: + - name: webhook-server + containerPort: 443 + protocol: TCP + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 8081 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 20 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readyz + port: 8081 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + {} + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + volumes: + - name: webhook-certs + secret: + defaultMode: 420 + secretName: netbird-operator-tls +--- +# Source: netbird-operator/templates/webhook.yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: netbird-operator-serving-cert + namespace: default + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + dnsNames: + - netbird-operator-webhook-service.default.svc + - netbird-operator-webhook-service.default.svc.cluster.local + issuerRef: + kind: Issuer + name: netbird-operator-selfsigned-issuer + secretName: netbird-operator-tls +--- +# Source: netbird-operator/templates/webhook.yaml +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: netbird-operator-selfsigned-issuer + namespace: default + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + selfSigned: {} +--- +# Source: netbird-operator/templates/webhook.yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: default/netbird-operator-serving-cert + name: netbird-operator-mpod-webhook + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +webhooks: +- clientConfig: + service: + name: netbird-operator-webhook-service + namespace: default + path: /mutate--v1-pod + failurePolicy: Fail + name: mpod-v1.netbird.io + admissionReviewVersions: + - v1 + objectSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: NotIn + values: + - netbird-operator + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + resources: + - pods + sideEffects: None +--- +# Source: netbird-operator/templates/webhook.yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: default/netbird-operator-serving-cert + name: netbird-operator-vnbsetupkey-webhook + labels: + helm.sh/chart: netbird-operator-0.1.0 + app.kubernetes.io/name: netbird-operator + app.kubernetes.io/instance: netbird-operator + app.kubernetes.io/version: "v0.1.0" + app.kubernetes.io/managed-by: Helm +webhooks: +- clientConfig: + service: + name: netbird-operator-webhook-service + namespace: default + path: /validate-netbird-io-v1-nbsetupkey + failurePolicy: Fail + name: vnbsetupkey-v1.netbird.io + admissionReviewVersions: + - v1 + rules: + - apiGroups: + - netbird.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - "*/*" + sideEffects: None diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7548b0f --- /dev/null +++ b/go.mod @@ -0,0 +1,73 @@ +module github.com/netbirdio/kubernetes-operator + +go 1.23.0 + +godebug default=go1.23 + +require ( + github.com/google/uuid v1.6.0 + github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/gomega v1.35.1 + k8s.io/api v0.32.0 + k8s.io/apimachinery v0.32.0 + k8s.io/client-go v0.32.0 + sigs.k8s.io/controller-runtime v0.20.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.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/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.35.1 // 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/apiextensions-apiserver v0.32.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..834b820 --- /dev/null +++ b/go.sum @@ -0,0 +1,190 @@ +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.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= +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/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.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/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= +k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= +k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= +k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= +k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= +k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= +k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= +sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..e69de29 diff --git a/helm/netbird-operator/.helmignore b/helm/netbird-operator/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/netbird-operator/.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/helm/netbird-operator/Chart.yaml b/helm/netbird-operator/Chart.yaml new file mode 100644 index 0000000..7ee86c9 --- /dev/null +++ b/helm/netbird-operator/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: netbird-operator +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "v0.1.0" diff --git a/helm/netbird-operator/crds/netbird.io_nbsetupkeys.yaml b/helm/netbird-operator/crds/netbird.io_nbsetupkeys.yaml new file mode 100644 index 0000000..559c660 --- /dev/null +++ b/helm/netbird-operator/crds/netbird.io_nbsetupkeys.yaml @@ -0,0 +1,116 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 + name: nbsetupkeys.netbird.io +spec: + group: netbird.io + names: + kind: NBSetupKey + listKind: NBSetupKeyList + plural: nbsetupkeys + singular: nbsetupkey + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: NBSetupKey is the Schema for the nbsetupkeys 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: NBSetupKeySpec defines the desired state of NBSetupKey. + properties: + managementURL: + description: ManagementURL optional, override operator management + URL + type: string + secretKeyRef: + description: SecretKeyRef is a reference to the secret containing + the setup key + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - secretKeyRef + type: object + status: + description: NBSetupKeyStatus defines the observed state of NBSetupKey. + properties: + conditions: + items: + description: NBSetupKeyCondition defines a condition in NBSetupKey + status. + properties: + lastProbeTime: + description: Last time we probed the condition. + format: date-time + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: Human-readable message indicating details about + last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's + last transition. + type: string + status: + description: |- + Status is the status of the condition. + Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/netbird-operator/templates/NOTES.txt b/helm/netbird-operator/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/helm/netbird-operator/templates/_helpers.tpl b/helm/netbird-operator/templates/_helpers.tpl new file mode 100644 index 0000000..a5c0b15 --- /dev/null +++ b/helm/netbird-operator/templates/_helpers.tpl @@ -0,0 +1,102 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "netbird-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "netbird-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "netbird-operator.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "netbird-operator.labels" -}} +helm.sh/chart: {{ include "netbird-operator.chart" . }} +{{ include "netbird-operator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "netbird-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "netbird-operator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "netbird-operator.serviceAccountName" -}} +{{- if .Values.operator.serviceAccount.create }} +{{- default (include "netbird-operator.fullname" .) .Values.operator.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.operator.serviceAccount.name }} +{{- end }} +{{- end }} + + +{{/* +Create the name of the webhook service +*/}} +{{- define "netbird-operator.webhookService" -}} +{{- printf "%s-webhook-service" (include "netbird-operator.fullname" .) -}} +{{- end -}} + +{{/* +Create the name of the webhook cert secret +*/}} +{{- define "netbird-operator.webhookCertSecret" -}} +{{- printf "%s-tls" (include "netbird-operator.fullname" .) -}} +{{- end -}} + +{{/* +Generate certificates for webhook +*/}} +{{- define "netbird-operator.webhookCerts" -}} +{{- $serviceName := (include "netbird-operator.webhookService" .) -}} +{{- $secretName := (include "netbird-operator.webhookCertSecret" .) -}} +{{- $secret := lookup "v1" "Secret" .Release.Namespace $secretName -}} +{{- if (and .Values.webhook.tls.caCert .Values.webhook.tls.cert .Values.webhook.tls.key) -}} +caCert: {{ .Values.webhook.tls.caCert | b64enc }} +clientCert: {{ .Values.webhook.tls.cert | b64enc }} +clientKey: {{ .Values.webhook.tls.key | b64enc }} +{{- else if and .Values.keepTLSSecret $secret -}} +caCert: {{ index $secret.data "ca.crt" }} +clientCert: {{ index $secret.data "tls.crt" }} +clientKey: {{ index $secret.data "tls.key" }} +{{- else -}} +{{- $altNames := list (printf "%s.%s" $serviceName .Release.Namespace) (printf "%s.%s.svc" $serviceName .Release.Namespace) (printf "%s.%s.svc.%s" $serviceName .Release.Namespace .Values.webhook.cluster.dnsDomain) -}} +{{- $ca := genCA "netbird-operator-ca" 3650 -}} +{{- $cert := genSignedCert (include "netbird-operator.fullname" .) nil $altNames 3650 $ca -}} +caCert: {{ $ca.Cert | b64enc }} +clientCert: {{ $cert.Cert | b64enc }} +clientKey: {{ $cert.Key | b64enc }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm/netbird-operator/templates/deployment.yaml b/helm/netbird-operator/templates/deployment.yaml new file mode 100644 index 0000000..ad39a46 --- /dev/null +++ b/helm/netbird-operator/templates/deployment.yaml @@ -0,0 +1,103 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "netbird-operator.fullname" . }} + labels: + app.kubernetes.io/component: operator + {{- include "netbird-operator.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.operator.replicaCount }} + selector: + matchLabels: + {{- include "netbird-operator.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.operator.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + app.kubernetes.io/component: operator + {{- include "netbird-operator.labels" . | nindent 8 }} + {{- with .Values.operator.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.operator.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "netbird-operator.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.operator.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.operator.securityContext | nindent 12 }} + image: "{{ .Values.operator.image.registry }}/{{ .Values.operator.image.repository }}:{{ .Values.operator.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.operator.image.pullPolicy }} + command: + - /manager + args: + {{- if .Values.operator.metrics.enabled }} + - --metrics-bind-address=:{{ .Values.operator.metrics.port}} + {{- end }} + - --leader-elect + - --health-probe-bind-address=:{{ .Values.operator.livenessProbe.port }} + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + {{- if .Values.managementURL }} + - --netbird-management-url={{.Values.managementURL}} + {{- end }} + ports: + - name: webhook-server + containerPort: {{ .Values.webhook.service.port }} + protocol: TCP + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: {{ .Values.operator.livenessProbe.port }} + scheme: HTTP + initialDelaySeconds: {{ .Values.operator.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.operator.livenessProbe.periodSeconds }} + successThreshold: {{ .Values.operator.livenessProbe.successThreshold }} + timeoutSeconds: {{ .Values.operator.livenessProbe.timeoutSeconds }} + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readyz + port: {{ .Values.operator.readinessProbe.port }} + scheme: HTTP + initialDelaySeconds: {{ .Values.operator.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.operator.readinessProbe.periodSeconds }} + successThreshold: {{ .Values.operator.readinessProbe.successThreshold }} + timeoutSeconds: {{ .Values.operator.readinessProbe.timeoutSeconds }} + resources: + {{- toYaml .Values.operator.resources | nindent 12 }} + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + {{- with .Values.operator.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: webhook-certs + secret: + defaultMode: 420 + secretName: {{ template "netbird-operator.webhookCertSecret" . }} + {{- with .Values.operator.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.operator.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.operator.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.operator.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/netbird-operator/templates/rbac.yaml b/helm/netbird-operator/templates/rbac.yaml new file mode 100644 index 0000000..75abd35 --- /dev/null +++ b/helm/netbird-operator/templates/rbac.yaml @@ -0,0 +1,116 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "netbird-operator.fullname" . }} + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - netbird.io + resources: + - nbsetupkeys + verbs: + - get + - list + - watch +- apiGroups: + - netbird.io + resources: + - nbsetupkeys/finalizers + verbs: + - update +- apiGroups: + - netbird.io + resources: + - nbsetupkeys/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch +{{- if .Values.clusterSecretsPermissions.allowAllSecrets }} +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +{{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "netbird-operator.fullname" . }} + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "netbird-operator.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "netbird-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "netbird-operator.fullname" . }} + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "netbird-operator.fullname" . }} + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "netbird-operator.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "netbird-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/helm/netbird-operator/templates/service.yaml b/helm/netbird-operator/templates/service.yaml new file mode 100644 index 0000000..f5f0b9c --- /dev/null +++ b/helm/netbird-operator/templates/service.yaml @@ -0,0 +1,33 @@ +{{- if .Values.operator.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "netbird-operator.fullname" . }}-metrics + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.operator.metrics.type }} + ports: + - name: http + port: {{ .Values.operator.metrics.port }} + protocol: TCP + targetPort: {{ .Values.operator.metrics.port }} + selector: + {{- include "netbird-operator.selectorLabels" . | nindent 4 }} +{{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "netbird-operator.webhookService" . }} + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.webhook.service.type }} + ports: + - name: https + port: {{ .Values.webhook.service.port }} + protocol: TCP + targetPort: {{ .Values.webhook.service.targetPort }} + selector: + {{- include "netbird-operator.selectorLabels" . | nindent 4 }} diff --git a/helm/netbird-operator/templates/serviceaccount.yaml b/helm/netbird-operator/templates/serviceaccount.yaml new file mode 100644 index 0000000..3f3836d --- /dev/null +++ b/helm/netbird-operator/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.operator.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "netbird-operator.serviceAccountName" . }} + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} + {{- with .Values.operator.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.operator.serviceAccount.automount }} +{{- end }} diff --git a/helm/netbird-operator/templates/webhook.yaml b/helm/netbird-operator/templates/webhook.yaml new file mode 100644 index 0000000..e716b22 --- /dev/null +++ b/helm/netbird-operator/templates/webhook.yaml @@ -0,0 +1,134 @@ +{{ $tls := fromYaml ( include "netbird-operator.webhookCerts" . ) }} +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: +{{- if $.Values.webhook.enableCertManager }} + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "netbird-operator.fullname" . }}-serving-cert +{{- end }} + name: {{ include "netbird-operator.fullname" . }}-mpod-webhook + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +webhooks: +- clientConfig: + {{- if not $.Values.webhook.enableCertManager -}} + caBundle: {{ $tls.caCert }} + {{ end }} + service: + name: {{ template "netbird-operator.webhookService" . }} + namespace: {{ $.Release.Namespace }} + path: /mutate--v1-pod + failurePolicy: Fail + name: mpod-v1.netbird.io + admissionReviewVersions: + - v1 + {{- if .Values.webhook.namespaceSelectors }} + namespaceSelector: + matchExpressions: + {{ toYaml .Values.webhook.namespaceSelectors | nindent 4 }} + {{ end }} + objectSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: NotIn + values: + - {{ include "netbird-operator.name" . }} + {{- if .Values.webhook.objectSelector.matchExpressions }} + {{- toYaml .Values.webhook.objectSelector.matchExpressions | nindent 4 }} + {{- end }} + {{- if .Values.webhook.objectSelector.matchLabels }} + matchLabels: + {{- toYaml .Values.webhook.objectSelector.matchLabels | nindent 6 }} + {{- end }} + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + resources: + - pods + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: +{{- if $.Values.webhook.enableCertManager }} + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "netbird-operator.fullname" . }}-serving-cert +{{- end }} + name: {{ include "netbird-operator.fullname" . }}-vnbsetupkey-webhook + labels: + {{- include "netbird-operator.labels" . | nindent 4 }} +webhooks: +- clientConfig: + {{- if not $.Values.webhook.enableCertManager -}} + caBundle: {{ $tls.caCert }} + {{ end }} + service: + name: {{ template "netbird-operator.webhookService" . }} + namespace: {{ $.Release.Namespace }} + path: /validate-netbird-io-v1-nbsetupkey + failurePolicy: Fail + name: vnbsetupkey-v1.netbird.io + admissionReviewVersions: + - v1 + {{- if .Values.webhook.namespaceSelectors }} + namespaceSelector: + matchExpressions: + {{ toYaml .Values.webhook.namespaceSelectors | nindent 4 }} + {{ end }} + rules: + - apiGroups: + - netbird.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - "*/*" + sideEffects: None +--- +{{- if not $.Values.webhook.enableCertManager }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "netbird-operator.webhookCertSecret" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "netbird-operator.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ $tls.caCert }} + tls.crt: {{ $tls.clientCert }} + tls.key: {{ $tls.clientKey }} +{{- else }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ template "netbird-operator.fullname" . }}-serving-cert + namespace: {{ .Release.Namespace }} + labels: +{{ include "netbird-operator.labels" . | indent 4 }} +spec: + dnsNames: + - {{ template "netbird-operator.webhookService" . }}.{{ .Release.Namespace }}.svc + - {{ template "netbird-operator.webhookService" . }}.{{ .Release.Namespace }}.svc.{{ .Values.webhook.cluster.dnsDomain }} + issuerRef: + kind: Issuer + name: {{ template "netbird-operator.fullname" . }}-selfsigned-issuer + secretName: {{ template "netbird-operator.webhookCertSecret" . }} +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ template "netbird-operator.fullname" . }}-selfsigned-issuer + namespace: {{ .Release.Namespace }} + labels: +{{ include "netbird-operator.labels" . | indent 4 }} +spec: + selfSigned: {} +{{- end }} \ No newline at end of file diff --git a/helm/netbird-operator/values.yaml b/helm/netbird-operator/values.yaml new file mode 100644 index 0000000..bf3c61a --- /dev/null +++ b/helm/netbird-operator/values.yaml @@ -0,0 +1,130 @@ +clusterSecretsPermissions: + allowAllSecrets: true + +webhook: + service: + type: ClusterIP + port: 443 + targetPort: 9443 + + cluster: + # Cluster DNS domain (required for requesting TLS certificates) + dnsDomain: cluster.local + + # TLS configuration for webhook + tls: {} + + # Use cert-manager to provision webhook certificates + enableCertManager: true + + namespaceSelectors: [] + # - key: foo + # operator: In + # values: + # - bar + + objectSelector: + matchExpressions: [] + # - key: app.kubernetes.io/name + # operator: NotIn + # values: + # - foo + +operator: + # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + replicaCount: 1 + + # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ + image: + # Set operator image registry + registry: docker.io + # Set operator image repository + repository: netbirdio/kubernetes-operator + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + + metrics: + enabled: true + type: ClusterIP + port: 8080 + + # This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + imagePullSecrets: [] + # This is to override the chart name. + nameOverride: "" + fullnameOverride: "" + + #This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ + serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + + # This is for setting Kubernetes Annotations to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # This is for setting Kubernetes Labels to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + + + # This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ + service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 9443 + + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + successThreshold: 1 + timeoutSeconds: 1 + + readinessProbe: + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + + # Additional volumes on the output Deployment definition. + volumes: [] + + # Additional volumeMounts on the output Deployment definition. + volumeMounts: [] + + nodeSelector: {} + + tolerations: [] + + affinity: {} diff --git a/internal/controller/nbsetupkey_controller.go b/internal/controller/nbsetupkey_controller.go new file mode 100644 index 0000000..6669d8d --- /dev/null +++ b/internal/controller/nbsetupkey_controller.go @@ -0,0 +1,156 @@ +/* +Copyright 2025. + +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" + + "github.com/google/uuid" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "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/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" +) + +// NBSetupKeyReconciler reconciles a NBSetupKey object +type NBSetupKeyReconciler struct { + client.Client + Scheme *runtime.Scheme + ReferencedSecrets map[string]types.NamespacedName +} + +// 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 *NBSetupKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + nbSetupKey := netbirdiov1.NBSetupKey{} + err := r.Get(ctx, req.NamespacedName, &nbSetupKey) + if err != nil { + ctrl.Log.Error(fmt.Errorf("internalError"), "error getting NBSetupKey", "err", err, "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, nil + } + + if nbSetupKey.Spec.SecretKeyRef.Name == "" || nbSetupKey.Spec.SecretKeyRef.Key == "" { + ctrl.Log.Error(fmt.Errorf("invalid NBSetupKey"), "secretKeyRef must contain both secret name and secret key", "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, r.setStatus(ctx, &nbSetupKey, netbirdiov1.NBSetupKeyStatus{ + Conditions: []netbirdiov1.NBSetupKeyCondition{ + { + Type: netbirdiov1.Ready, + Status: corev1.ConditionFalse, + LastProbeTime: v1.Now(), + Reason: "InvalidConfig", + Message: "secretKeyRef must contain both secret name and secret key.", + }, + }, + }) + } + + // Handle updated secret name + for k, v := range r.ReferencedSecrets { + if v == req.NamespacedName { + delete(r.ReferencedSecrets, k) + break + } + } + r.ReferencedSecrets[fmt.Sprintf("%s/%s", nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)] = req.NamespacedName + + secret := corev1.Secret{} + err = r.Get(ctx, types.NamespacedName{Namespace: nbSetupKey.Namespace, Name: nbSetupKey.Spec.SecretKeyRef.Name}, &secret) + if err != nil { + if !errors.IsNotFound(err) { + ctrl.Log.Error(fmt.Errorf("internalError"), "error getting secret", "err", err, "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, err + } + ctrl.Log.Error(fmt.Errorf("invalid NBSetupKey"), "secret referenced not found", "err", err, "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, r.setStatus(ctx, &nbSetupKey, netbirdiov1.NBSetupKeyStatus{Conditions: []netbirdiov1.NBSetupKeyCondition{{ + Type: netbirdiov1.Ready, + Status: corev1.ConditionFalse, + LastProbeTime: v1.Now(), + Reason: "SecretNotExists", + Message: "Referenced secret does not exist", + }}}) + } + + uuidBytes, ok := secret.Data[nbSetupKey.Spec.SecretKeyRef.Key] + if !ok { + ctrl.Log.Error(fmt.Errorf("invalid NBSetupKey"), "secret key referenced not found", "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, r.setStatus(ctx, &nbSetupKey, netbirdiov1.NBSetupKeyStatus{Conditions: []netbirdiov1.NBSetupKeyCondition{{ + Type: netbirdiov1.Ready, + Status: corev1.ConditionFalse, + LastProbeTime: v1.Now(), + Reason: "SecretKeyNotExists", + Message: "Referenced secret key does not exist", + }}}) + } + + _, err = uuid.Parse(string(uuidBytes)) + if err != nil { + ctrl.Log.Error(fmt.Errorf("invalid NBSetupKey"), "setupKey is not a valid UUID", "err", err, "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, r.setStatus(ctx, &nbSetupKey, netbirdiov1.NBSetupKeyStatus{Conditions: []netbirdiov1.NBSetupKeyCondition{{ + Type: netbirdiov1.Ready, + Status: corev1.ConditionFalse, + LastProbeTime: v1.Now(), + Reason: "InvalidSetupKey", + Message: "Referenced secret is not a valid SetupKey", + }}}) + } + return ctrl.Result{}, r.setStatus(ctx, &nbSetupKey, netbirdiov1.NBSetupKeyStatus{Conditions: []netbirdiov1.NBSetupKeyCondition{{ + Type: netbirdiov1.Ready, + Status: corev1.ConditionTrue, + LastProbeTime: v1.Now(), + }}}) +} + +func (r *NBSetupKeyReconciler) setStatus(ctx context.Context, nbsetupkey *netbirdiov1.NBSetupKey, status netbirdiov1.NBSetupKeyStatus) error { + nbsetupkey.Status = status + err := r.Status().Update(ctx, nbsetupkey) + return err +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NBSetupKeyReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.ReferencedSecrets = make(map[string]types.NamespacedName) + + return ctrl.NewControllerManagedBy(mgr). + For(&netbirdiov1.NBSetupKey{}). + Named("nbsetupkey"). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + if v, ok := r.ReferencedSecrets[fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())]; ok { + return []reconcile.Request{ + { + NamespacedName: v, + }, + } + } + + return nil + }), + ). // Trigger reconciliation when the labeled Busybox resource changes + Complete(r) +} diff --git a/internal/controller/nbsetupkey_controller_test.go b/internal/controller/nbsetupkey_controller_test.go new file mode 100644 index 0000000..2b73889 --- /dev/null +++ b/internal/controller/nbsetupkey_controller_test.go @@ -0,0 +1,200 @@ +/* +Copyright 2025. + +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" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" +) + +var _ = Describe("NBSetupKey Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + nbsetupkey := &netbirdiov1.NBSetupKey{} + secret := &v1.Secret{} + + BeforeEach(func() { + By("creating the custom resource for the Kind NBSetupKey") + err := k8sClient.Get(ctx, typeNamespacedName, nbsetupkey) + resource := &netbirdiov1.NBSetupKey{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: netbirdiov1.NBSetupKeySpec{ + SecretKeyRef: v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: resourceName, + }, + Key: "setupkey", + }, + }, + } + if err == nil { + Expect(k8sClient.Delete(ctx, nbsetupkey)).To(Succeed()) + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }) + + AfterEach(func() { + resource := &netbirdiov1.NBSetupKey{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance NBSetupKey") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + When("No secret present", func() { + It("should set status to not ready", func() { + controllerReconciler := &NBSetupKeyReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ReferencedSecrets: make(map[string]types.NamespacedName), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, typeNamespacedName, nbsetupkey) + Expect(err).NotTo(HaveOccurred()) + + Expect(nbsetupkey.Status.Conditions).NotTo(BeNil()) + Expect(nbsetupkey.Status.Conditions).To(HaveLen(1)) + Expect(nbsetupkey.Status.Conditions[0].Status).To(Equal(v1.ConditionFalse)) + Expect(nbsetupkey.Status.Conditions[0].Reason).To(Equal("SecretNotExists")) + Expect(controllerReconciler.ReferencedSecrets).To(HaveKey("default/test-resource")) + }) + }) + + When("Secret present", Ordered, func() { + createSecret := func(secretkey, setupkey string) { + resource := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: resourceName, + }, + Data: map[string][]byte{ + secretkey: []byte(setupkey), + }, + } + + secret = &v1.Secret{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: resourceName}, secret) + if err == nil { + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + + When("secret is invalid", func() { + It("should set status to not ready", func() { + createSecret("setupkey", "invalid-key") + + controllerReconciler := &NBSetupKeyReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ReferencedSecrets: make(map[string]types.NamespacedName), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, typeNamespacedName, nbsetupkey) + Expect(err).NotTo(HaveOccurred()) + + Expect(nbsetupkey.Status.Conditions).NotTo(BeNil()) + Expect(nbsetupkey.Status.Conditions).To(HaveLen(1)) + Expect(nbsetupkey.Status.Conditions[0].Status).To(Equal(v1.ConditionFalse)) + Expect(nbsetupkey.Status.Conditions[0].Reason).To(Equal("InvalidSetupKey")) + Expect(controllerReconciler.ReferencedSecrets).To(HaveKey("default/test-resource")) + }) + }) + + When("secret key is missing", func() { + It("should set status to not ready", func() { + createSecret("key", "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE") + + controllerReconciler := &NBSetupKeyReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ReferencedSecrets: make(map[string]types.NamespacedName), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, typeNamespacedName, nbsetupkey) + Expect(err).NotTo(HaveOccurred()) + + Expect(nbsetupkey.Status.Conditions).NotTo(BeNil()) + Expect(nbsetupkey.Status.Conditions).To(HaveLen(1)) + Expect(nbsetupkey.Status.Conditions[0].Status).To(Equal(v1.ConditionFalse)) + Expect(nbsetupkey.Status.Conditions[0].Reason).To(Equal("SecretKeyNotExists")) + Expect(controllerReconciler.ReferencedSecrets).To(HaveKey("default/test-resource")) + }) + }) + + When("secret is valid", func() { + It("should set status to ready", func() { + createSecret("setupkey", "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE") + + controllerReconciler := &NBSetupKeyReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ReferencedSecrets: make(map[string]types.NamespacedName), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, typeNamespacedName, nbsetupkey) + Expect(err).NotTo(HaveOccurred()) + + Expect(nbsetupkey.Status.Conditions).NotTo(BeNil()) + Expect(nbsetupkey.Status.Conditions).To(HaveLen(1)) + Expect(nbsetupkey.Status.Conditions[0].Status).To(Equal(v1.ConditionTrue)) + Expect(controllerReconciler.ReferencedSecrets).To(HaveKey("default/test-resource")) + }) + }) + }) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go new file mode 100644 index 0000000..ec0def0 --- /dev/null +++ b/internal/controller/suite_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2025. + +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" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + 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 = netbirdiov1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "helm", "netbird-operator", "crds")}, + 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/internal/webhook/v1/nbsetupkey_webhook.go b/internal/webhook/v1/nbsetupkey_webhook.go new file mode 100644 index 0000000..13dc3e7 --- /dev/null +++ b/internal/webhook/v1/nbsetupkey_webhook.go @@ -0,0 +1,113 @@ +package v1 + +import ( + "context" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/google/uuid" + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" +) + +// nolint:unused +// log is for logging in this package. +var nbsetupkeylog = logf.Log.WithName("nbsetupkey-resource") + +// SetupNBSetupKeyWebhookWithManager registers the webhook for NBSetupKey in the manager. +func SetupNBSetupKeyWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBSetupKey{}). + WithValidator(&NBSetupKeyCustomValidator{client: mgr.GetClient()}). + Complete() +} + +// NBSetupKeyCustomValidator struct is responsible for validating the NBSetupKey resource +// when it is created, updated, or deleted. +type NBSetupKeyCustomValidator struct { + client client.Client +} + +var _ webhook.CustomValidator = &NBSetupKeyCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type NBSetupKey. +func (v *NBSetupKeyCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + nbSetupKey, ok := obj.(*netbirdiov1.NBSetupKey) + if !ok { + return nil, fmt.Errorf("expected a NBSetupKey object but got %T", obj) + } + nbsetupkeylog.Info("Validating NBSetupKey", "namespace", nbSetupKey.Namespace, "name", nbSetupKey.Name) + + if nbSetupKey.Spec.SecretKeyRef.Name == "" { + return nil, fmt.Errorf("spec.secretKeyRef.name is required") + } + + if nbSetupKey.Spec.SecretKeyRef.Key == "" { + return nil, fmt.Errorf("spec.secretKeyRef.key is required") + } + + var secret corev1.Secret + err := v.client.Get(ctx, types.NamespacedName{Namespace: nbSetupKey.Namespace, Name: nbSetupKey.Spec.SecretKeyRef.Name}, &secret) + if err != nil { + if errors.IsNotFound(err) { + return admission.Warnings{fmt.Sprintf("secret %s/%s not found", nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)}, nil + } + return nil, err + } + + uuidBytes, ok := secret.Data[nbSetupKey.Spec.SecretKeyRef.Key] + if !ok { + return admission.Warnings{fmt.Sprintf("key %s in secret %s/%s not found", nbSetupKey.Spec.SecretKeyRef.Key, nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)}, nil + } + + _, err = uuid.Parse(string(uuidBytes)) + if err != nil { + return admission.Warnings{fmt.Sprintf("setupkey %s in secret %s/%s is not a valid setup key", nbSetupKey.Spec.SecretKeyRef.Key, nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)}, nil + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type NBSetupKey. +func (v *NBSetupKeyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + return v.ValidateCreate(ctx, newObj) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type NBSetupKey. +func (v *NBSetupKeyCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + nbSetupKey, ok := obj.(*netbirdiov1.NBSetupKey) + if !ok { + return nil, fmt.Errorf("expected a NBSetupKey object but got %T", obj) + } + nbsetupkeylog.Info("Validating NBSetupKey deletion", "namespace", nbSetupKey.Namespace, "name", nbSetupKey.Name) + + var pods corev1.PodList + err := v.client.List(ctx, &pods, client.InNamespace(nbSetupKey.Namespace)) + if err != nil { + return nil, err + } + + //nolint:prealloc + var invalidPods []string + for _, p := range pods.Items { + // If annotation doesn't exist, or doesn't match NBSetupKey being deleted, ignore + if v, ok := p.Annotations[setupKeyAnnotation]; !ok || v != nbSetupKey.Name { + continue + } + invalidPods = append(invalidPods, p.Name) + } + + if len(invalidPods) > 0 { + return nil, fmt.Errorf("NBSetupKey is in-use by %d pods: %s", len(invalidPods), strings.Join(invalidPods, ",")) + } + + return nil, nil +} diff --git a/internal/webhook/v1/nbsetupkey_webhook_test.go b/internal/webhook/v1/nbsetupkey_webhook_test.go new file mode 100644 index 0000000..a4eedc8 --- /dev/null +++ b/internal/webhook/v1/nbsetupkey_webhook_test.go @@ -0,0 +1,124 @@ +package v1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" +) + +var _ = Describe("NBSetupKey Webhook", func() { + var ( + obj *netbirdiov1.NBSetupKey + validator NBSetupKeyCustomValidator + resourceName = "test" + secret *corev1.Secret + ) + + BeforeEach(func() { + obj = &netbirdiov1.NBSetupKey{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + validator = NBSetupKeyCustomValidator{ + client: k8sClient, + } + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + }) + + AfterEach(func() { + }) + + Context("When creating or updating NBSetupKey under Validating Webhook", func() { + When("secretKeyRef is empty", func() { + It("Should fail", func() { + obj.Spec = netbirdiov1.NBSetupKeySpec{} + warnings, err := validator.ValidateCreate(context.Background(), obj) + Expect(err).To(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + }) + + When("secret doesn't exist", func() { + It("Should fail", func() { + obj.Spec = netbirdiov1.NBSetupKeySpec{ + SecretKeyRef: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resourceName, + }, + Key: "setupkey", + }, + } + warnings, err := validator.ValidateCreate(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).NotTo(BeEmpty()) + }) + }) + + Context("secret exists", Ordered, func() { + createSecret := func(secretkey, setupkey string) { + resource := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: resourceName, + }, + Data: map[string][]byte{ + secretkey: []byte(setupkey), + }, + } + + secret = &corev1.Secret{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: resourceName}, secret) + if err == nil { + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + + BeforeEach(func() { + obj.Spec = netbirdiov1.NBSetupKeySpec{ + SecretKeyRef: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resourceName, + }, + Key: "setupkey", + }, + } + }) + + When("secret key doesn't exist", func() { + It("Should fail", func() { + createSecret("wrongkey", "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE") + warnings, err := validator.ValidateCreate(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).NotTo(BeEmpty()) + }) + }) + When("setup key is invalid", func() { + It("Should fail", func() { + createSecret("setupkey", "EEEEEEEE") + warnings, err := validator.ValidateCreate(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).NotTo(BeEmpty()) + }) + }) + When("setup key is valid", func() { + It("Should allow creation", func() { + createSecret("setupkey", "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE") + warnings, err := validator.ValidateCreate(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + }) + }) + }) + +}) diff --git a/internal/webhook/v1/pod_webhook.go b/internal/webhook/v1/pod_webhook.go new file mode 100644 index 0000000..d2b89f0 --- /dev/null +++ b/internal/webhook/v1/pod_webhook.go @@ -0,0 +1,127 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "fmt" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +const ( + setupKeyAnnotation = "netbird.io/setup-key" +) + +// nolint:unused +// log is for logging in this package. +var podlog = logf.Log.WithName("pod-resource") + +// SetupPodWebhookWithManager registers the webhook for Pod in the manager. +func SetupPodWebhookWithManager(mgr ctrl.Manager, managementURL, clientImage string) error { + return ctrl.NewWebhookManagedBy(mgr).For(&corev1.Pod{}). + WithDefaulter(&PodNetbirdInjector{ + client: mgr.GetClient(), + managementURL: managementURL, + clientImage: clientImage, + }). + Complete() +} + +// PodNetbirdInjector struct is responsible for setting default values on the custom resource of the +// Kind Pod when those are created or updated. +type PodNetbirdInjector struct { + client client.Client + managementURL string + clientImage string +} + +var _ webhook.CustomDefaulter = &PodNetbirdInjector{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Pod. +func (d *PodNetbirdInjector) Default(ctx context.Context, obj runtime.Object) error { + pod, ok := obj.(*corev1.Pod) + + if !ok { + return fmt.Errorf("expected an Pod object but got %T", obj) + } + podlog.Info("Defaulting for Pod", "name", pod.GetName()) + + if pod.Annotations == nil || pod.Annotations[setupKeyAnnotation] == "" { + return nil + } + + var nbSetupKey netbirdiov1.NBSetupKey + err := d.client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: pod.Annotations[setupKeyAnnotation]}, &nbSetupKey) + if err != nil { + return err + } + + ready := false + for _, c := range nbSetupKey.Status.Conditions { + if c.Type == netbirdiov1.Ready { + ready = c.Status == corev1.ConditionTrue + } + } + if !ready { + return fmt.Errorf("NBSetupKey is not ready") + } + + managementURL := d.managementURL + if nbSetupKey.Spec.ManagementURL != "" { + managementURL = nbSetupKey.Spec.ManagementURL + } + + pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ + Name: "netbird", + Image: d.clientImage, + Args: []string{ + "--setup-key-file", + "/etc/nbkey", + "-m", + managementURL, + }, + Env: []corev1.EnvVar{ + { + Name: "NB_SETUP_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &nbSetupKey.Spec.SecretKeyRef, + }, + }, + { + Name: "NB_MANAGEMENT_URL", + Value: managementURL, + }, + }, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "NET_ADMIN", + }, + }, + }, + }) + + return nil +} diff --git a/internal/webhook/v1/pod_webhook_test.go b/internal/webhook/v1/pod_webhook_test.go new file mode 100644 index 0000000..7708cf4 --- /dev/null +++ b/internal/webhook/v1/pod_webhook_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("Pod Webhook", func() { + var ( + obj *corev1.Pod + defaulter PodNetbirdInjector + ) + + BeforeEach(func() { + obj = &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + Annotations: make(map[string]string), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test", + }, + }, + }, + } + defaulter = PodNetbirdInjector{ + client: k8sClient, + managementURL: "https://api.netbird.io", + clientImage: "netbirdio/netbird:latest", + } + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + }) + + AfterEach(func() { + }) + + Context("When creating Pod without annotation", func() { + It("Should not modify anything", func() { + err := defaulter.Default(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + Expect(obj.Spec.Containers).To(HaveLen(1)) + }) + }) + + Context("When creating Pod with annotation", func() { + BeforeEach(func() { + obj.Annotations[setupKeyAnnotation] = "test" + }) + + When("NBSetupKey doesn't exist", func() { + It("Should fail", func() { + Expect(defaulter.Default(context.Background(), obj)).To(HaveOccurred()) + Expect(obj.Spec.Containers).To(HaveLen(1)) + }) + }) + + When("NBSetupKey exists", Ordered, func() { + BeforeAll(func() { + sk := netbirdiov1.NBSetupKey{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: netbirdiov1.NBSetupKeySpec{ + SecretKeyRef: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test", + }, + Key: "test", + }, + }, + } + + err := k8sClient.Create(context.Background(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + }, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Create(context.Background(), &sk) + Expect(err).NotTo(HaveOccurred()) + + sk.Status = netbirdiov1.NBSetupKeyStatus{ + Conditions: []netbirdiov1.NBSetupKeyCondition{ + { + Type: netbirdiov1.Ready, + Status: corev1.ConditionTrue, + }, + }, + } + + err = k8sClient.Status().Update(context.Background(), &sk) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should inject NB container", func() { + Expect(defaulter.Default(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(obj.Spec.Containers).To(HaveLen(2)) + Expect(obj.Spec.Containers[1].Name).To(Equal("netbird")) + }) + }) + }) +}) diff --git a/internal/webhook/v1/webhook_suite_test.go b/internal/webhook/v1/webhook_suite_test.go new file mode 100644 index 0000000..1a1e554 --- /dev/null +++ b/internal/webhook/v1/webhook_suite_test.go @@ -0,0 +1,176 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + k8siov1 "k8s.io/api/core/v1" + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "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" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + ctx context.Context + cancel context.CancelFunc + k8sClient client.Client + cfg *rest.Config + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + var err error + scheme := apimachineryruntime.NewScheme() + err = k8siov1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = netbirdiov1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "helm", "netbird-operator", "crds")}, + ErrorIfCRDPathMissing: false, + + // WebhookInstallOptions: envtest.WebhookInstallOptions{ + // Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + // }, + } + + // 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}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager. + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupPodWebhookWithManager(mgr, "", "") + Expect(err).NotTo(HaveOccurred()) + + err = SetupNBSetupKeyWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready. + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + + return conn.Close() + }).Should(Succeed()) +}) + +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/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go new file mode 100644 index 0000000..512f52f --- /dev/null +++ b/test/e2e/e2e_suite_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2025. + +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/netbirdio/kubernetes-operator/test/utils" +) + +var ( + // Optional Environment Variables: + // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. + // This variable is 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 = "docker.io/netbirdio/kubernetes-operator:e2e" +) + +// 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 the purposed to be 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") + + 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") + + 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/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 0000000..540acd9 --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,472 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "os/exec" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/netbirdio/kubernetes-operator/test/utils" +) + +// namespace where the project is deployed in +const namespace = "netbird" + +// metricsServiceName is the name of the metrics service of the project +const metricsServiceName = "netbird-operator-metrics" + +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", fmt.Sprintf("IMG=%s", projectImage)) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") + + By("deploying the netbird-operator") + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) + out, err := utils.Run(cmd) + if err != nil { + fmt.Println(out) + } + Expect(err).NotTo(HaveOccurred(), "Failed to deploy the netbird-operator") + }) + + // 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 netbird-operator") + 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 netbird-operator pod is running as expected") + verifyControllerUp := func(g Gomega) { + // Get the name of the netbird-operator pod + cmd := exec.Command("kubectl", "get", + "pods", "-l", "app.kubernetes.io/component=operator,app.kubernetes.io/name=netbird-operator", + "-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 netbird-operator 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("netbird-operator")) + + // 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 netbird-operator pod status") + } + Eventually(verifyControllerUp).Should(Succeed()) + }) + + It("should ensure the metrics endpoint is serving metrics", func() { + 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("waiting for the metrics endpoint to be ready") + verifyMetricsEndpointReady := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("8080"), "Metrics endpoint is not ready") + } + Eventually(verifyMetricsEndpointReady).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("controller-runtime.metrics\tServing metrics server"), + "Metrics server not yet started") + } + Eventually(verifyMetricsServerStarted).Should(Succeed()) + + 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 http://%s.%s.svc.cluster.local:8080/metrics"], + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + } + }] + } + }`, metricsServiceName, namespace)) + _, 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") + metricsOutput := getMetricsOutput() + Expect(metricsOutput).To(ContainSubstring( + "controller_runtime_reconcile_total", + )) + }) + + It("should provisioned cert-manager", func() { + By("validating that cert-manager has the certificate Secret") + verifyCertManager := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "secrets", "netbird-operator-tls", "-n", namespace) + _, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + } + Eventually(verifyCertManager).Should(Succeed()) + }) + + It("should have CA injection for mutating webhooks", func() { + By("checking CA injection for mutating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "mutatingwebhookconfigurations.admissionregistration.k8s.io", + "netbird-operator-mpod-webhook", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + mwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(mwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + It("should have CA injection for validating webhooks", func() { + By("checking CA injection for validating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "validatingwebhookconfigurations.admissionregistration.k8s.io", + "netbird-operator-vnbsetupkey-webhook", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + Context("NBSetupKey", Ordered, func() { + Describe("Basic functionality", Ordered, func() { + BeforeAll(func() { + cmd := exec.Command( + "kubectl", "create", "secret", "generic", + "--from-literal", "sk=EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE", + "-n", "default", + "netbird-sk", + ) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterAll(func() { + cmd := exec.Command("kubectl", "delete", "--ignore-not-found", "secret", "netbird-sk") + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + cmd = exec.Command("kubectl", "delete", "--ignore-not-found", "NBSetupKey", "main") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should allow setupkey resource to be created", func() { + cmd := exec.Command("kubectl", "apply", "-f", "-") + cmd.Stdin = strings.NewReader(`{ + "apiVersion": "netbird.io/v1", + "kind": "NBSetupKey", + "metadata": { + "name": "main" + }, + "spec": { + "managementURL": "https://netbird.example.com", + "secretKeyRef": { + "name": "netbird-sk", + "key": "sk" + } + } + }`) + + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should set status.conditions[0].status to True", func() { + verifyNBSetupKeyStatus := func(g Gomega) { + cmd := exec.Command( + "kubectl", "get", + "nbsetupkeys", "main", + "-o", "jsonpath={.status.conditions[0].status}", + ) + vnbskOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(vnbskOutput).To(ContainSubstring("True")) + } + Eventually(verifyNBSetupKeyStatus).Should(Succeed()) + }) + + It("should inject netbird container into a new pod with annotation", func() { + cmd := exec.Command( + "kubectl", "run", "test-pod-inject", + "--dry-run=server", "--image=busybox", + "--annotations", "netbird.io/setup-key=main", + "-n", "default", + "-o", "jsonpath={.spec.containers[1].name}", + ) + out, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(out).To(ContainSubstring("netbird")) + }) + + It("should not inject netbird container into a new pod without annotation", func() { + cmd := exec.Command( + "kubectl", "run", "test-pod-inject", + "--dry-run=server", "--image=busybox", + "-n", "default", + "-o", "jsonpath={.spec.containers}", + ) + out, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(out).NotTo(ContainSubstring("netbird")) + }) + + It("should fail new pod with incorrect annotation", func() { + cmd := exec.Command( + "kubectl", "run", "test-pod-inject", + "--dry-run=server", "--image=busybox", + "--annotations", "netbird.io/setup-key=nothing", + "-n", "default", + "-o", "jsonpath={.spec.containers}", + ) + out, err := utils.Run(cmd) + Expect(err).To(HaveOccurred()) + Expect(out).To(ContainSubstring("admission")) + }) + }) + Describe("Post-create validation", Ordered, func() { + BeforeAll(func() { + cmd := exec.Command( + "kubectl", "create", "secret", "generic", + "--from-literal", "sk=EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE", + "-n", "default", + "netbird-sk", + ) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + cmd = exec.Command("kubectl", "apply", "-f", "-") + cmd.Stdin = strings.NewReader(`{ + "apiVersion": "netbird.io/v1", + "kind": "NBSetupKey", + "metadata": { + "name": "main" + }, + "spec": { + "managementURL": "https://netbird.example.com", + "secretKeyRef": { + "name": "netbird-sk", + "key": "sk" + } + } + }`) + + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterAll(func() { + cmd := exec.Command("kubectl", "delete", "--ignore-not-found", "secret", "netbird-sk") + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + cmd = exec.Command("kubectl", "delete", "--ignore-not-found", "NBSetupKey", "main") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should set status.conditions[0].status to True", func() { + verifyNBSetupKeyStatus := func(g Gomega) { + cmd := exec.Command( + "kubectl", "get", + "nbsetupkeys", "main", + "-o", "jsonpath={.status.conditions[0].status}", + ) + vnbskOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(vnbskOutput).To(ContainSubstring("True")) + } + Eventually(verifyNBSetupKeyStatus).Should(Succeed()) + }) + + It("should update status after secret is updated", func() { + cmd := exec.Command( + "kubectl", "apply", "-f", "-", + ) + cmd.Stdin = strings.NewReader(`{ + "kind": "Secret", + "apiVersion": "v1", + "metadata": { + "name": "netbird-sk" + }, + "stringData": { + "sk": "WewWewInvalidWewWew" + } + }`) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should update status.conditions[0].status to False", func() { + verifyNBSetupKeyStatus := func(g Gomega) { + cmd := exec.Command( + "kubectl", "get", + "nbsetupkeys", "main", + "-o", "jsonpath={.status.conditions[0].status}", + ) + vnbskOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(vnbskOutput).To(ContainSubstring("False")) + } + Eventually(verifyNBSetupKeyStatus).Should(Succeed()) + }) + }) + }) + + }) +}) + +// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. +func getMetricsOutput() string { + By("getting the curl-metrics logs") + cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) + metricsOutput, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") + Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) + return metricsOutput +} diff --git a/test/utils/utils.go b/test/utils/utils.go new file mode 100644 index 0000000..cbeff44 --- /dev/null +++ b/test/utils/utils.go @@ -0,0 +1,174 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "os" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo/v2" //nolint:golint,revive +) + +const ( + certmanagerVersion = "v1.16.3" + certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" +) + +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: %s\n", err) + } + + cmd.Env = append(os.Environ(), "GO111MODULE=on") + command := strings.Join(cmd.Args, " ") + _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + } + + 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) + } + + cmd = exec.Command( + "kubectl", "delete", "--ignore-not-found", + "leases.coordination.k8s.io", + "-n", "kube-system", + "cert-manager-controller", "cert-manager-cainjector-leader-election", + ) + + 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) + if err != nil { + return err + } + + cmd = exec.Command("kubectl", "wait", "leases/cert-manager-controller", + "--for", "jsonpath={.spec.holderIdentity}", + "-n", "kube-system", + "--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 := "kind" + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { + cluster = v + } + kindOptions := []string{"load", "docker-image", name, "--name", cluster} + cmd := exec.Command("kind", kindOptions...) + _, err := Run(cmd) + return err +} + +// GetNonEmptyLines converts given command output string into individual objects +// according to line breakers, and ignores the empty elements in it. +func GetNonEmptyLines(output string) []string { + var res []string + elements := strings.Split(output, "\n") + for _, element := range elements { + if element != "" { + res = append(res, element) + } + } + + return res +} + +// GetProjectDir will return the directory where the project is +func GetProjectDir() (string, error) { + wd, err := os.Getwd() + if err != nil { + return wd, err + } + wd = strings.Replace(wd, "/test/e2e", "", -1) + return wd, nil +}