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 +}