···11+name: E2E Tests
22+33+on:
44+ push:
55+ pull_request:
66+77+jobs:
88+ test-e2e:
99+ name: Run on Ubuntu
1010+ runs-on: ubuntu-latest
1111+ steps:
1212+ - name: Clone the code
1313+ uses: actions/checkout@v4
1414+1515+ - name: Setup Go
1616+ uses: actions/setup-go@v5
1717+ with:
1818+ go-version-file: go.mod
1919+2020+ - name: Install the latest version of kind
2121+ run: |
2222+ curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
2323+ chmod +x ./kind
2424+ sudo mv ./kind /usr/local/bin/kind
2525+2626+ - name: Verify kind installation
2727+ run: kind version
2828+2929+ - name: Running Test e2e
3030+ run: |
3131+ go mod tidy
3232+ make test-e2e
+23
.github/workflows/test.yml
···11+name: Tests
22+33+on:
44+ push:
55+ pull_request:
66+77+jobs:
88+ test:
99+ name: Run on Ubuntu
1010+ runs-on: ubuntu-latest
1111+ steps:
1212+ - name: Clone the code
1313+ uses: actions/checkout@v4
1414+1515+ - name: Setup Go
1616+ uses: actions/setup-go@v5
1717+ with:
1818+ go-version-file: go.mod
1919+2020+ - name: Running Tests
2121+ run: |
2222+ go mod tidy
2323+ make test
+27
.gitignore
···11+# Binaries for programs and plugins
22+*.exe
33+*.exe~
44+*.dll
55+*.so
66+*.dylib
77+bin/*
88+Dockerfile.cross
99+1010+# Test binary, built with `go test -c`
1111+*.test
1212+1313+# Output of the go coverage tool, specifically when used with LiteIDE
1414+*.out
1515+1616+# Go workspace file
1717+go.work
1818+1919+# Kubernetes Generated files - skip generated files, except for vendored files
2020+!vendor/**/zz_generated.*
2121+2222+# editor and IDE paraphernalia
2323+.idea
2424+.vscode
2525+*.swp
2626+*.swo
2727+*~
···11+# Build the manager binary
22+FROM golang:1.24 AS builder
33+ARG TARGETOS
44+ARG TARGETARCH
55+66+WORKDIR /workspace
77+# Copy the Go Modules manifests
88+COPY go.mod go.mod
99+COPY go.sum go.sum
1010+# cache deps before building and copying source so that we don't need to re-download as much
1111+# and so that source changes don't invalidate our downloaded layer
1212+RUN go mod download
1313+1414+# Copy the go source
1515+COPY cmd/main.go cmd/main.go
1616+COPY api/ api/
1717+COPY internal/ internal/
1818+1919+# Build
2020+# the GOARCH has not a default value to allow the binary be built according to the host where the command
2121+# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
2222+# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
2323+# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
2424+RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
2525+2626+# Use distroless as minimal base image to package the manager binary
2727+# Refer to https://github.com/GoogleContainerTools/distroless for more details
2828+FROM gcr.io/distroless/static:nonroot
2929+WORKDIR /
3030+COPY --from=builder /workspace/manager .
3131+USER 65532:65532
3232+3333+ENTRYPOINT ["/manager"]
+238
Makefile
···11+# Image URL to use all building/pushing image targets
22+IMG ?= controller:latest
33+44+# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
55+ifeq (,$(shell go env GOBIN))
66+GOBIN=$(shell go env GOPATH)/bin
77+else
88+GOBIN=$(shell go env GOBIN)
99+endif
1010+1111+# CONTAINER_TOOL defines the container tool to be used for building images.
1212+# Be aware that the target commands are only tested with Docker which is
1313+# scaffolded by default. However, you might want to replace it to use other
1414+# tools. (i.e. podman)
1515+CONTAINER_TOOL ?= docker
1616+1717+# Setting SHELL to bash allows bash commands to be executed by recipes.
1818+# Options are set to exit when a recipe line exits non-zero or a piped command fails.
1919+SHELL = /usr/bin/env bash -o pipefail
2020+.SHELLFLAGS = -ec
2121+2222+.PHONY: all
2323+all: build
2424+2525+##@ General
2626+2727+# The help target prints out all targets with their descriptions organized
2828+# beneath their categories. The categories are represented by '##@' and the
2929+# target descriptions by '##'. The awk command is responsible for reading the
3030+# entire set of makefiles included in this invocation, looking for lines of the
3131+# file as xyz: ## something, and then pretty-format the target and help. Then,
3232+# if there's a line with ##@ something, that gets pretty-printed as a category.
3333+# More info on the usage of ANSI control characters for terminal formatting:
3434+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
3535+# More info on the awk command:
3636+# http://linuxcommand.org/lc3_adv_awk.php
3737+3838+.PHONY: help
3939+help: ## Display this help.
4040+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\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)
4141+4242+##@ Development
4343+4444+.PHONY: manifests
4545+manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
4646+ $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
4747+4848+.PHONY: generate
4949+generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
5050+ $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
5151+5252+.PHONY: fmt
5353+fmt: ## Run go fmt against code.
5454+ go fmt ./...
5555+5656+.PHONY: vet
5757+vet: ## Run go vet against code.
5858+ go vet ./...
5959+6060+.PHONY: test
6161+test: manifests generate fmt vet setup-envtest ## Run tests.
6262+ KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
6363+6464+# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
6565+# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
6666+# CertManager is installed by default; skip with:
6767+# - CERT_MANAGER_INSTALL_SKIP=true
6868+KIND_CLUSTER ?= pist-test-e2e
6969+7070+.PHONY: setup-test-e2e
7171+setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
7272+ @command -v $(KIND) >/dev/null 2>&1 || { \
7373+ echo "Kind is not installed. Please install Kind manually."; \
7474+ exit 1; \
7575+ }
7676+ @case "$$($(KIND) get clusters)" in \
7777+ *"$(KIND_CLUSTER)"*) \
7878+ echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
7979+ *) \
8080+ echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
8181+ $(KIND) create cluster --name $(KIND_CLUSTER) ;; \
8282+ esac
8383+8484+.PHONY: test-e2e
8585+test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
8686+ KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v
8787+ $(MAKE) cleanup-test-e2e
8888+8989+.PHONY: cleanup-test-e2e
9090+cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
9191+ @$(KIND) delete cluster --name $(KIND_CLUSTER)
9292+9393+.PHONY: lint
9494+lint: golangci-lint ## Run golangci-lint linter
9595+ $(GOLANGCI_LINT) run
9696+9797+.PHONY: lint-fix
9898+lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
9999+ $(GOLANGCI_LINT) run --fix
100100+101101+.PHONY: lint-config
102102+lint-config: golangci-lint ## Verify golangci-lint linter configuration
103103+ $(GOLANGCI_LINT) config verify
104104+105105+##@ Build
106106+107107+.PHONY: build
108108+build: manifests generate fmt vet ## Build manager binary.
109109+ go build -o bin/manager cmd/main.go
110110+111111+.PHONY: run
112112+run: manifests generate fmt vet ## Run a controller from your host.
113113+ go run ./cmd/main.go
114114+115115+# If you wish to build the manager image targeting other platforms you can use the --platform flag.
116116+# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
117117+# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
118118+.PHONY: docker-build
119119+docker-build: ## Build docker image with the manager.
120120+ $(CONTAINER_TOOL) build -t ${IMG} .
121121+122122+.PHONY: docker-push
123123+docker-push: ## Push docker image with the manager.
124124+ $(CONTAINER_TOOL) push ${IMG}
125125+126126+# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
127127+# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
128128+# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
129129+# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
130130+# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
131131+# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
132132+PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
133133+.PHONY: docker-buildx
134134+docker-buildx: ## Build and push docker image for the manager for cross-platform support
135135+ # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
136136+ sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
137137+ - $(CONTAINER_TOOL) buildx create --name pist-builder
138138+ $(CONTAINER_TOOL) buildx use pist-builder
139139+ - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
140140+ - $(CONTAINER_TOOL) buildx rm pist-builder
141141+ rm Dockerfile.cross
142142+143143+.PHONY: build-installer
144144+build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
145145+ mkdir -p dist
146146+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
147147+ $(KUSTOMIZE) build config/default > dist/install.yaml
148148+149149+##@ Deployment
150150+151151+ifndef ignore-not-found
152152+ ignore-not-found = false
153153+endif
154154+155155+.PHONY: install
156156+install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
157157+ $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f -
158158+159159+.PHONY: uninstall
160160+uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
161161+ $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
162162+163163+.PHONY: deploy
164164+deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
165165+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
166166+ $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
167167+168168+.PHONY: undeploy
169169+undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
170170+ $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
171171+172172+##@ Dependencies
173173+174174+## Location to install dependencies to
175175+LOCALBIN ?= $(shell pwd)/bin
176176+$(LOCALBIN):
177177+ mkdir -p $(LOCALBIN)
178178+179179+## Tool Binaries
180180+KUBECTL ?= kubectl
181181+KIND ?= kind
182182+KUSTOMIZE ?= $(LOCALBIN)/kustomize
183183+CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
184184+ENVTEST ?= $(LOCALBIN)/setup-envtest
185185+GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
186186+187187+## Tool Versions
188188+KUSTOMIZE_VERSION ?= v5.6.0
189189+CONTROLLER_TOOLS_VERSION ?= v0.18.0
190190+#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
191191+ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
192192+#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
193193+ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
194194+GOLANGCI_LINT_VERSION ?= v2.1.6
195195+196196+.PHONY: kustomize
197197+kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
198198+$(KUSTOMIZE): $(LOCALBIN)
199199+ $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
200200+201201+.PHONY: controller-gen
202202+controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
203203+$(CONTROLLER_GEN): $(LOCALBIN)
204204+ $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
205205+206206+.PHONY: setup-envtest
207207+setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
208208+ @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
209209+ @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \
210210+ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
211211+ exit 1; \
212212+ }
213213+214214+.PHONY: envtest
215215+envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
216216+$(ENVTEST): $(LOCALBIN)
217217+ $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
218218+219219+.PHONY: golangci-lint
220220+golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
221221+$(GOLANGCI_LINT): $(LOCALBIN)
222222+ $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
223223+224224+# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
225225+# $1 - target path with name of binary
226226+# $2 - package url which can be installed
227227+# $3 - specific version of package
228228+define go-install-tool
229229+@[ -f "$(1)-$(3)" ] || { \
230230+set -e; \
231231+package=$(2)@$(3) ;\
232232+echo "Downloading $${package}" ;\
233233+rm -f $(1) || true ;\
234234+GOBIN=$(LOCALBIN) go install $${package} ;\
235235+mv $(1) $(1)-$(3) ;\
236236+} ;\
237237+ln -sf $(1)-$(3) $(1)
238238+endef
+11
PROJECT
···11+# Code generated by tool. DO NOT EDIT.
22+# This file is used to track the info used to scaffold your project
33+# and allow the plugins properly work.
44+# More info: https://book.kubebuilder.io/reference/project-config.html
55+cliVersion: v4.7.1
66+domain: kgz.sh
77+layout:
88+- go.kubebuilder.io/v4
99+projectName: pist
1010+repo: github.com/kragniz/pist
1111+version: "3"
+135
README.md
···11+# pist
22+// TODO(user): Add simple overview of use/purpose
33+44+## Description
55+// TODO(user): An in-depth paragraph about your project and overview of use
66+77+## Getting Started
88+99+### Prerequisites
1010+- go version v1.24.0+
1111+- docker version 17.03+.
1212+- kubectl version v1.11.3+.
1313+- Access to a Kubernetes v1.11.3+ cluster.
1414+1515+### To Deploy on the cluster
1616+**Build and push your image to the location specified by `IMG`:**
1717+1818+```sh
1919+make docker-build docker-push IMG=<some-registry>/pist:tag
2020+```
2121+2222+**NOTE:** This image ought to be published in the personal registry you specified.
2323+And it is required to have access to pull the image from the working environment.
2424+Make sure you have the proper permission to the registry if the above commands don’t work.
2525+2626+**Install the CRDs into the cluster:**
2727+2828+```sh
2929+make install
3030+```
3131+3232+**Deploy the Manager to the cluster with the image specified by `IMG`:**
3333+3434+```sh
3535+make deploy IMG=<some-registry>/pist:tag
3636+```
3737+3838+> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
3939+privileges or be logged in as admin.
4040+4141+**Create instances of your solution**
4242+You can apply the samples (examples) from the config/sample:
4343+4444+```sh
4545+kubectl apply -k config/samples/
4646+```
4747+4848+>**NOTE**: Ensure that the samples has default values to test it out.
4949+5050+### To Uninstall
5151+**Delete the instances (CRs) from the cluster:**
5252+5353+```sh
5454+kubectl delete -k config/samples/
5555+```
5656+5757+**Delete the APIs(CRDs) from the cluster:**
5858+5959+```sh
6060+make uninstall
6161+```
6262+6363+**UnDeploy the controller from the cluster:**
6464+6565+```sh
6666+make undeploy
6767+```
6868+6969+## Project Distribution
7070+7171+Following the options to release and provide this solution to the users.
7272+7373+### By providing a bundle with all YAML files
7474+7575+1. Build the installer for the image built and published in the registry:
7676+7777+```sh
7878+make build-installer IMG=<some-registry>/pist:tag
7979+```
8080+8181+**NOTE:** The makefile target mentioned above generates an 'install.yaml'
8282+file in the dist directory. This file contains all the resources built
8383+with Kustomize, which are necessary to install this project without its
8484+dependencies.
8585+8686+2. Using the installer
8787+8888+Users can just run 'kubectl apply -f <URL for YAML BUNDLE>' to install
8989+the project, i.e.:
9090+9191+```sh
9292+kubectl apply -f https://raw.githubusercontent.com/<org>/pist/<tag or branch>/dist/install.yaml
9393+```
9494+9595+### By providing a Helm Chart
9696+9797+1. Build the chart using the optional helm plugin
9898+9999+```sh
100100+kubebuilder edit --plugins=helm/v1-alpha
101101+```
102102+103103+2. See that a chart was generated under 'dist/chart', and users
104104+can obtain this solution from there.
105105+106106+**NOTE:** If you change the project, you need to update the Helm Chart
107107+using the same command above to sync the latest changes. Furthermore,
108108+if you create webhooks, you need to use the above command with
109109+the '--force' flag and manually ensure that any custom configuration
110110+previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml'
111111+is manually re-applied afterwards.
112112+113113+## Contributing
114114+// TODO(user): Add detailed information on how you would like others to contribute to this project
115115+116116+**NOTE:** Run `make help` for more information on all potential `make` targets
117117+118118+More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
119119+120120+## License
121121+122122+Copyright 2026.
123123+124124+Licensed under the Apache License, Version 2.0 (the "License");
125125+you may not use this file except in compliance with the License.
126126+You may obtain a copy of the License at
127127+128128+ http://www.apache.org/licenses/LICENSE-2.0
129129+130130+Unless required by applicable law or agreed to in writing, software
131131+distributed under the License is distributed on an "AS IS" BASIS,
132132+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
133133+See the License for the specific language governing permissions and
134134+limitations under the License.
135135+
+233
cmd/main.go
···11+/*
22+Copyright 2026.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package main
1818+1919+import (
2020+ "crypto/tls"
2121+ "flag"
2222+ "os"
2323+ "path/filepath"
2424+2525+ // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2626+ // to ensure that exec-entrypoint and run can make use of them.
2727+ _ "k8s.io/client-go/plugin/pkg/client/auth"
2828+2929+ "k8s.io/apimachinery/pkg/runtime"
3030+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3131+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3232+ ctrl "sigs.k8s.io/controller-runtime"
3333+ "sigs.k8s.io/controller-runtime/pkg/certwatcher"
3434+ "sigs.k8s.io/controller-runtime/pkg/healthz"
3535+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
3636+ "sigs.k8s.io/controller-runtime/pkg/metrics/filters"
3737+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
3838+ "sigs.k8s.io/controller-runtime/pkg/webhook"
3939+ // +kubebuilder:scaffold:imports
4040+)
4141+4242+var (
4343+ scheme = runtime.NewScheme()
4444+ setupLog = ctrl.Log.WithName("setup")
4545+)
4646+4747+func init() {
4848+ utilruntime.Must(clientgoscheme.AddToScheme(scheme))
4949+5050+ // +kubebuilder:scaffold:scheme
5151+}
5252+5353+// nolint:gocyclo
5454+func main() {
5555+ var metricsAddr string
5656+ var metricsCertPath, metricsCertName, metricsCertKey string
5757+ var webhookCertPath, webhookCertName, webhookCertKey string
5858+ var enableLeaderElection bool
5959+ var probeAddr string
6060+ var secureMetrics bool
6161+ var enableHTTP2 bool
6262+ var tlsOpts []func(*tls.Config)
6363+ flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
6464+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
6565+ flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
6666+ flag.BoolVar(&enableLeaderElection, "leader-elect", false,
6767+ "Enable leader election for controller manager. "+
6868+ "Enabling this will ensure there is only one active controller manager.")
6969+ flag.BoolVar(&secureMetrics, "metrics-secure", true,
7070+ "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
7171+ flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
7272+ flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
7373+ flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
7474+ flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
7575+ "The directory that contains the metrics server certificate.")
7676+ flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
7777+ flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
7878+ flag.BoolVar(&enableHTTP2, "enable-http2", false,
7979+ "If set, HTTP/2 will be enabled for the metrics and webhook servers")
8080+ opts := zap.Options{
8181+ Development: true,
8282+ }
8383+ opts.BindFlags(flag.CommandLine)
8484+ flag.Parse()
8585+8686+ ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
8787+8888+ // if the enable-http2 flag is false (the default), http/2 should be disabled
8989+ // due to its vulnerabilities. More specifically, disabling http/2 will
9090+ // prevent from being vulnerable to the HTTP/2 Stream Cancellation and
9191+ // Rapid Reset CVEs. For more information see:
9292+ // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
9393+ // - https://github.com/advisories/GHSA-4374-p667-p6c8
9494+ disableHTTP2 := func(c *tls.Config) {
9595+ setupLog.Info("disabling http/2")
9696+ c.NextProtos = []string{"http/1.1"}
9797+ }
9898+9999+ if !enableHTTP2 {
100100+ tlsOpts = append(tlsOpts, disableHTTP2)
101101+ }
102102+103103+ // Create watchers for metrics and webhooks certificates
104104+ var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher
105105+106106+ // Initial webhook TLS options
107107+ webhookTLSOpts := tlsOpts
108108+109109+ if len(webhookCertPath) > 0 {
110110+ setupLog.Info("Initializing webhook certificate watcher using provided certificates",
111111+ "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
112112+113113+ var err error
114114+ webhookCertWatcher, err = certwatcher.New(
115115+ filepath.Join(webhookCertPath, webhookCertName),
116116+ filepath.Join(webhookCertPath, webhookCertKey),
117117+ )
118118+ if err != nil {
119119+ setupLog.Error(err, "Failed to initialize webhook certificate watcher")
120120+ os.Exit(1)
121121+ }
122122+123123+ webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) {
124124+ config.GetCertificate = webhookCertWatcher.GetCertificate
125125+ })
126126+ }
127127+128128+ webhookServer := webhook.NewServer(webhook.Options{
129129+ TLSOpts: webhookTLSOpts,
130130+ })
131131+132132+ // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
133133+ // More info:
134134+ // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server
135135+ // - https://book.kubebuilder.io/reference/metrics.html
136136+ metricsServerOptions := metricsserver.Options{
137137+ BindAddress: metricsAddr,
138138+ SecureServing: secureMetrics,
139139+ TLSOpts: tlsOpts,
140140+ }
141141+142142+ if secureMetrics {
143143+ // FilterProvider is used to protect the metrics endpoint with authn/authz.
144144+ // These configurations ensure that only authorized users and service accounts
145145+ // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
146146+ // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
147147+ metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
148148+ }
149149+150150+ // If the certificate is not specified, controller-runtime will automatically
151151+ // generate self-signed certificates for the metrics server. While convenient for development and testing,
152152+ // this setup is not recommended for production.
153153+ //
154154+ // TODO(user): If you enable certManager, uncomment the following lines:
155155+ // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
156156+ // managed by cert-manager for the metrics server.
157157+ // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
158158+ if len(metricsCertPath) > 0 {
159159+ setupLog.Info("Initializing metrics certificate watcher using provided certificates",
160160+ "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
161161+162162+ var err error
163163+ metricsCertWatcher, err = certwatcher.New(
164164+ filepath.Join(metricsCertPath, metricsCertName),
165165+ filepath.Join(metricsCertPath, metricsCertKey),
166166+ )
167167+ if err != nil {
168168+ setupLog.Error(err, "to initialize metrics certificate watcher", "error", err)
169169+ os.Exit(1)
170170+ }
171171+172172+ metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) {
173173+ config.GetCertificate = metricsCertWatcher.GetCertificate
174174+ })
175175+ }
176176+177177+ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
178178+ Scheme: scheme,
179179+ Metrics: metricsServerOptions,
180180+ WebhookServer: webhookServer,
181181+ HealthProbeBindAddress: probeAddr,
182182+ LeaderElection: enableLeaderElection,
183183+ LeaderElectionID: "28c041a5.kgz.sh",
184184+ // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
185185+ // when the Manager ends. This requires the binary to immediately end when the
186186+ // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
187187+ // speeds up voluntary leader transitions as the new leader don't have to wait
188188+ // LeaseDuration time first.
189189+ //
190190+ // In the default scaffold provided, the program ends immediately after
191191+ // the manager stops, so would be fine to enable this option. However,
192192+ // if you are doing or is intended to do any operation such as perform cleanups
193193+ // after the manager stops then its usage might be unsafe.
194194+ // LeaderElectionReleaseOnCancel: true,
195195+ })
196196+ if err != nil {
197197+ setupLog.Error(err, "unable to start manager")
198198+ os.Exit(1)
199199+ }
200200+201201+ // +kubebuilder:scaffold:builder
202202+203203+ if metricsCertWatcher != nil {
204204+ setupLog.Info("Adding metrics certificate watcher to manager")
205205+ if err := mgr.Add(metricsCertWatcher); err != nil {
206206+ setupLog.Error(err, "unable to add metrics certificate watcher to manager")
207207+ os.Exit(1)
208208+ }
209209+ }
210210+211211+ if webhookCertWatcher != nil {
212212+ setupLog.Info("Adding webhook certificate watcher to manager")
213213+ if err := mgr.Add(webhookCertWatcher); err != nil {
214214+ setupLog.Error(err, "unable to add webhook certificate watcher to manager")
215215+ os.Exit(1)
216216+ }
217217+ }
218218+219219+ if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
220220+ setupLog.Error(err, "unable to set up health check")
221221+ os.Exit(1)
222222+ }
223223+ if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
224224+ setupLog.Error(err, "unable to set up ready check")
225225+ os.Exit(1)
226226+ }
227227+228228+ setupLog.Info("starting manager")
229229+ if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
230230+ setupLog.Error(err, "problem running manager")
231231+ os.Exit(1)
232232+ }
233233+}
+30
config/default/cert_metrics_manager_patch.yaml
···11+# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.
22+33+# Add the volumeMount for the metrics-server certs
44+- op: add
55+ path: /spec/template/spec/containers/0/volumeMounts/-
66+ value:
77+ mountPath: /tmp/k8s-metrics-server/metrics-certs
88+ name: metrics-certs
99+ readOnly: true
1010+1111+# Add the --metrics-cert-path argument for the metrics server
1212+- op: add
1313+ path: /spec/template/spec/containers/0/args/-
1414+ value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs
1515+1616+# Add the metrics-server certs volume configuration
1717+- op: add
1818+ path: /spec/template/spec/volumes/-
1919+ value:
2020+ name: metrics-certs
2121+ secret:
2222+ secretName: metrics-server-cert
2323+ optional: false
2424+ items:
2525+ - key: ca.crt
2626+ path: ca.crt
2727+ - key: tls.crt
2828+ path: tls.crt
2929+ - key: tls.key
3030+ path: tls.key
+234
config/default/kustomization.yaml
···11+# Adds namespace to all resources.
22+namespace: pist-system
33+44+# Value of this field is prepended to the
55+# names of all resources, e.g. a deployment named
66+# "wordpress" becomes "alices-wordpress".
77+# Note that it should also match with the prefix (text before '-') of the namespace
88+# field above.
99+namePrefix: pist-
1010+1111+# Labels to add to all resources and selectors.
1212+#labels:
1313+#- includeSelectors: true
1414+# pairs:
1515+# someName: someValue
1616+1717+resources:
1818+#- ../crd
1919+- ../rbac
2020+- ../manager
2121+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
2222+# crd/kustomization.yaml
2323+#- ../webhook
2424+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
2525+#- ../certmanager
2626+# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
2727+#- ../prometheus
2828+# [METRICS] Expose the controller manager metrics service.
2929+- metrics_service.yaml
3030+# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
3131+# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
3232+# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
3333+# be able to communicate with the Webhook Server.
3434+#- ../network-policy
3535+3636+# Uncomment the patches line if you enable Metrics
3737+patches:
3838+# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
3939+# More info: https://book.kubebuilder.io/reference/metrics
4040+- path: manager_metrics_patch.yaml
4141+ target:
4242+ kind: Deployment
4343+4444+# Uncomment the patches line if you enable Metrics and CertManager
4545+# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
4646+# This patch will protect the metrics with certManager self-signed certs.
4747+#- path: cert_metrics_manager_patch.yaml
4848+# target:
4949+# kind: Deployment
5050+5151+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
5252+# crd/kustomization.yaml
5353+#- path: manager_webhook_patch.yaml
5454+# target:
5555+# kind: Deployment
5656+5757+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
5858+# Uncomment the following replacements to add the cert-manager CA injection annotations
5959+#replacements:
6060+# - source: # Uncomment the following block to enable certificates for metrics
6161+# kind: Service
6262+# version: v1
6363+# name: controller-manager-metrics-service
6464+# fieldPath: metadata.name
6565+# targets:
6666+# - select:
6767+# kind: Certificate
6868+# group: cert-manager.io
6969+# version: v1
7070+# name: metrics-certs
7171+# fieldPaths:
7272+# - spec.dnsNames.0
7373+# - spec.dnsNames.1
7474+# options:
7575+# delimiter: '.'
7676+# index: 0
7777+# create: true
7878+# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
7979+# kind: ServiceMonitor
8080+# group: monitoring.coreos.com
8181+# version: v1
8282+# name: controller-manager-metrics-monitor
8383+# fieldPaths:
8484+# - spec.endpoints.0.tlsConfig.serverName
8585+# options:
8686+# delimiter: '.'
8787+# index: 0
8888+# create: true
8989+9090+# - source:
9191+# kind: Service
9292+# version: v1
9393+# name: controller-manager-metrics-service
9494+# fieldPath: metadata.namespace
9595+# targets:
9696+# - select:
9797+# kind: Certificate
9898+# group: cert-manager.io
9999+# version: v1
100100+# name: metrics-certs
101101+# fieldPaths:
102102+# - spec.dnsNames.0
103103+# - spec.dnsNames.1
104104+# options:
105105+# delimiter: '.'
106106+# index: 1
107107+# create: true
108108+# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
109109+# kind: ServiceMonitor
110110+# group: monitoring.coreos.com
111111+# version: v1
112112+# name: controller-manager-metrics-monitor
113113+# fieldPaths:
114114+# - spec.endpoints.0.tlsConfig.serverName
115115+# options:
116116+# delimiter: '.'
117117+# index: 1
118118+# create: true
119119+120120+# - source: # Uncomment the following block if you have any webhook
121121+# kind: Service
122122+# version: v1
123123+# name: webhook-service
124124+# fieldPath: .metadata.name # Name of the service
125125+# targets:
126126+# - select:
127127+# kind: Certificate
128128+# group: cert-manager.io
129129+# version: v1
130130+# name: serving-cert
131131+# fieldPaths:
132132+# - .spec.dnsNames.0
133133+# - .spec.dnsNames.1
134134+# options:
135135+# delimiter: '.'
136136+# index: 0
137137+# create: true
138138+# - source:
139139+# kind: Service
140140+# version: v1
141141+# name: webhook-service
142142+# fieldPath: .metadata.namespace # Namespace of the service
143143+# targets:
144144+# - select:
145145+# kind: Certificate
146146+# group: cert-manager.io
147147+# version: v1
148148+# name: serving-cert
149149+# fieldPaths:
150150+# - .spec.dnsNames.0
151151+# - .spec.dnsNames.1
152152+# options:
153153+# delimiter: '.'
154154+# index: 1
155155+# create: true
156156+157157+# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
158158+# kind: Certificate
159159+# group: cert-manager.io
160160+# version: v1
161161+# name: serving-cert # This name should match the one in certificate.yaml
162162+# fieldPath: .metadata.namespace # Namespace of the certificate CR
163163+# targets:
164164+# - select:
165165+# kind: ValidatingWebhookConfiguration
166166+# fieldPaths:
167167+# - .metadata.annotations.[cert-manager.io/inject-ca-from]
168168+# options:
169169+# delimiter: '/'
170170+# index: 0
171171+# create: true
172172+# - source:
173173+# kind: Certificate
174174+# group: cert-manager.io
175175+# version: v1
176176+# name: serving-cert
177177+# fieldPath: .metadata.name
178178+# targets:
179179+# - select:
180180+# kind: ValidatingWebhookConfiguration
181181+# fieldPaths:
182182+# - .metadata.annotations.[cert-manager.io/inject-ca-from]
183183+# options:
184184+# delimiter: '/'
185185+# index: 1
186186+# create: true
187187+188188+# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
189189+# kind: Certificate
190190+# group: cert-manager.io
191191+# version: v1
192192+# name: serving-cert
193193+# fieldPath: .metadata.namespace # Namespace of the certificate CR
194194+# targets:
195195+# - select:
196196+# kind: MutatingWebhookConfiguration
197197+# fieldPaths:
198198+# - .metadata.annotations.[cert-manager.io/inject-ca-from]
199199+# options:
200200+# delimiter: '/'
201201+# index: 0
202202+# create: true
203203+# - source:
204204+# kind: Certificate
205205+# group: cert-manager.io
206206+# version: v1
207207+# name: serving-cert
208208+# fieldPath: .metadata.name
209209+# targets:
210210+# - select:
211211+# kind: MutatingWebhookConfiguration
212212+# fieldPaths:
213213+# - .metadata.annotations.[cert-manager.io/inject-ca-from]
214214+# options:
215215+# delimiter: '/'
216216+# index: 1
217217+# create: true
218218+219219+# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
220220+# kind: Certificate
221221+# group: cert-manager.io
222222+# version: v1
223223+# name: serving-cert
224224+# fieldPath: .metadata.namespace # Namespace of the certificate CR
225225+# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
226226+# +kubebuilder:scaffold:crdkustomizecainjectionns
227227+# - source:
228228+# kind: Certificate
229229+# group: cert-manager.io
230230+# version: v1
231231+# name: serving-cert
232232+# fieldPath: .metadata.name
233233+# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
234234+# +kubebuilder:scaffold:crdkustomizecainjectionname
+4
config/default/manager_metrics_patch.yaml
···11+# This patch adds the args to allow exposing the metrics endpoint using HTTPS
22+- op: add
33+ path: /spec/template/spec/containers/0/args/0
44+ value: --metrics-bind-address=:8443
···11+apiVersion: v1
22+kind: Namespace
33+metadata:
44+ labels:
55+ control-plane: controller-manager
66+ app.kubernetes.io/name: pist
77+ app.kubernetes.io/managed-by: kustomize
88+ name: system
99+---
1010+apiVersion: apps/v1
1111+kind: Deployment
1212+metadata:
1313+ name: controller-manager
1414+ namespace: system
1515+ labels:
1616+ control-plane: controller-manager
1717+ app.kubernetes.io/name: pist
1818+ app.kubernetes.io/managed-by: kustomize
1919+spec:
2020+ selector:
2121+ matchLabels:
2222+ control-plane: controller-manager
2323+ app.kubernetes.io/name: pist
2424+ replicas: 1
2525+ template:
2626+ metadata:
2727+ annotations:
2828+ kubectl.kubernetes.io/default-container: manager
2929+ labels:
3030+ control-plane: controller-manager
3131+ app.kubernetes.io/name: pist
3232+ spec:
3333+ # TODO(user): Uncomment the following code to configure the nodeAffinity expression
3434+ # according to the platforms which are supported by your solution.
3535+ # It is considered best practice to support multiple architectures. You can
3636+ # build your manager image using the makefile target docker-buildx.
3737+ # affinity:
3838+ # nodeAffinity:
3939+ # requiredDuringSchedulingIgnoredDuringExecution:
4040+ # nodeSelectorTerms:
4141+ # - matchExpressions:
4242+ # - key: kubernetes.io/arch
4343+ # operator: In
4444+ # values:
4545+ # - amd64
4646+ # - arm64
4747+ # - ppc64le
4848+ # - s390x
4949+ # - key: kubernetes.io/os
5050+ # operator: In
5151+ # values:
5252+ # - linux
5353+ securityContext:
5454+ # Projects are configured by default to adhere to the "restricted" Pod Security Standards.
5555+ # This ensures that deployments meet the highest security requirements for Kubernetes.
5656+ # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
5757+ runAsNonRoot: true
5858+ seccompProfile:
5959+ type: RuntimeDefault
6060+ containers:
6161+ - command:
6262+ - /manager
6363+ args:
6464+ - --leader-elect
6565+ - --health-probe-bind-address=:8081
6666+ image: controller:latest
6767+ name: manager
6868+ ports: []
6969+ securityContext:
7070+ readOnlyRootFilesystem: true
7171+ allowPrivilegeEscalation: false
7272+ capabilities:
7373+ drop:
7474+ - "ALL"
7575+ livenessProbe:
7676+ httpGet:
7777+ path: /healthz
7878+ port: 8081
7979+ initialDelaySeconds: 15
8080+ periodSeconds: 20
8181+ readinessProbe:
8282+ httpGet:
8383+ path: /readyz
8484+ port: 8081
8585+ initialDelaySeconds: 5
8686+ periodSeconds: 10
8787+ # TODO(user): Configure the resources accordingly based on the project requirements.
8888+ # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
8989+ resources:
9090+ limits:
9191+ cpu: 500m
9292+ memory: 128Mi
9393+ requests:
9494+ cpu: 10m
9595+ memory: 64Mi
9696+ volumeMounts: []
9797+ volumes: []
9898+ serviceAccountName: controller-manager
9999+ terminationGracePeriodSeconds: 10
+27
config/network-policy/allow-metrics-traffic.yaml
···11+# This NetworkPolicy allows ingress traffic
22+# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
33+# namespaces are able to gather data from the metrics endpoint.
44+apiVersion: networking.k8s.io/v1
55+kind: NetworkPolicy
66+metadata:
77+ labels:
88+ app.kubernetes.io/name: pist
99+ app.kubernetes.io/managed-by: kustomize
1010+ name: allow-metrics-traffic
1111+ namespace: system
1212+spec:
1313+ podSelector:
1414+ matchLabels:
1515+ control-plane: controller-manager
1616+ app.kubernetes.io/name: pist
1717+ policyTypes:
1818+ - Ingress
1919+ ingress:
2020+ # This allows ingress traffic from any namespace with the label metrics: enabled
2121+ - from:
2222+ - namespaceSelector:
2323+ matchLabels:
2424+ metrics: enabled # Only from namespaces with this label
2525+ ports:
2626+ - port: 8443
2727+ protocol: TCP
···11+resources:
22+- monitor.yaml
33+44+# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
55+# to securely reference certificates created and managed by cert-manager.
66+# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
77+# to mount the "metrics-server-cert" secret in the Manager Deployment.
88+#patches:
99+# - path: monitor_tls_patch.yaml
1010+# target:
1111+# kind: ServiceMonitor
+27
config/prometheus/monitor.yaml
···11+# Prometheus Monitor Service (Metrics)
22+apiVersion: monitoring.coreos.com/v1
33+kind: ServiceMonitor
44+metadata:
55+ labels:
66+ control-plane: controller-manager
77+ app.kubernetes.io/name: pist
88+ app.kubernetes.io/managed-by: kustomize
99+ name: controller-manager-metrics-monitor
1010+ namespace: system
1111+spec:
1212+ endpoints:
1313+ - path: /metrics
1414+ port: https # Ensure this is the name of the port that exposes HTTPS metrics
1515+ scheme: https
1616+ bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
1717+ tlsConfig:
1818+ # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
1919+ # certificate verification, exposing the system to potential man-in-the-middle attacks.
2020+ # For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
2121+ # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
2222+ # which securely references the certificate from the 'metrics-server-cert' secret.
2323+ insecureSkipVerify: true
2424+ selector:
2525+ matchLabels:
2626+ control-plane: controller-manager
2727+ app.kubernetes.io/name: pist
+19
config/prometheus/monitor_tls_patch.yaml
···11+# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
22+# using certificates managed by cert-manager
33+- op: replace
44+ path: /spec/endpoints/0/tlsConfig
55+ value:
66+ # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
77+ serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
88+ insecureSkipVerify: false
99+ ca:
1010+ secret:
1111+ name: metrics-server-cert
1212+ key: ca.crt
1313+ cert:
1414+ secret:
1515+ name: metrics-server-cert
1616+ key: tls.crt
1717+ keySecret:
1818+ name: metrics-server-cert
1919+ key: tls.key
+20
config/rbac/kustomization.yaml
···11+resources:
22+# All RBAC will be applied under this service account in
33+# the deployment namespace. You may comment out this resource
44+# if your manager will use a service account that exists at
55+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
66+# subjects if changing service account names.
77+- service_account.yaml
88+- role.yaml
99+- role_binding.yaml
1010+- leader_election_role.yaml
1111+- leader_election_role_binding.yaml
1212+# The following RBAC configurations are used to protect
1313+# the metrics endpoint with authn/authz. These configurations
1414+# ensure that only authorized users and service accounts
1515+# can access the metrics endpoint. Comment the following
1616+# permissions if you want to disable this protection.
1717+# More info: https://book.kubebuilder.io/reference/metrics.html
1818+- metrics_auth_role.yaml
1919+- metrics_auth_role_binding.yaml
2020+- metrics_reader_role.yaml
···11+/*
22+Copyright 2026.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
+89
test/e2e/e2e_suite_test.go
···11+/*
22+Copyright 2026.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package e2e
1818+1919+import (
2020+ "fmt"
2121+ "os"
2222+ "os/exec"
2323+ "testing"
2424+2525+ . "github.com/onsi/ginkgo/v2"
2626+ . "github.com/onsi/gomega"
2727+2828+ "github.com/kragniz/pist/test/utils"
2929+)
3030+3131+var (
3232+ // Optional Environment Variables:
3333+ // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup.
3434+ // These variables are useful if CertManager is already installed, avoiding
3535+ // re-installation and conflicts.
3636+ skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true"
3737+ // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster
3838+ isCertManagerAlreadyInstalled = false
3939+4040+ // projectImage is the name of the image which will be build and loaded
4141+ // with the code source changes to be tested.
4242+ projectImage = "example.com/pist:v0.0.1"
4343+)
4444+4545+// TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated,
4646+// temporary environment to validate project changes with the purpose of being used in CI jobs.
4747+// The default setup requires Kind, builds/loads the Manager Docker image locally, and installs
4848+// CertManager.
4949+func TestE2E(t *testing.T) {
5050+ RegisterFailHandler(Fail)
5151+ _, _ = fmt.Fprintf(GinkgoWriter, "Starting pist integration test suite\n")
5252+ RunSpecs(t, "e2e suite")
5353+}
5454+5555+var _ = BeforeSuite(func() {
5656+ By("building the manager(Operator) image")
5757+ cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage))
5858+ _, err := utils.Run(cmd)
5959+ ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image")
6060+6161+ // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is
6262+ // built and available before running the tests. Also, remove the following block.
6363+ By("loading the manager(Operator) image on Kind")
6464+ err = utils.LoadImageToKindClusterWithName(projectImage)
6565+ ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind")
6666+6767+ // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing.
6868+ // To prevent errors when tests run in environments with CertManager already installed,
6969+ // we check for its presence before execution.
7070+ // Setup CertManager before the suite if not skipped and if not already installed
7171+ if !skipCertManagerInstall {
7272+ By("checking if cert manager is installed already")
7373+ isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled()
7474+ if !isCertManagerAlreadyInstalled {
7575+ _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n")
7676+ Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
7777+ } else {
7878+ _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n")
7979+ }
8080+ }
8181+})
8282+8383+var _ = AfterSuite(func() {
8484+ // Teardown CertManager after the suite if not skipped and if it was not already installed
8585+ if !skipCertManagerInstall && !isCertManagerAlreadyInstalled {
8686+ _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n")
8787+ utils.UninstallCertManager()
8888+ }
8989+})
+330
test/e2e/e2e_test.go
···11+/*
22+Copyright 2026.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package e2e
1818+1919+import (
2020+ "encoding/json"
2121+ "fmt"
2222+ "os"
2323+ "os/exec"
2424+ "path/filepath"
2525+ "time"
2626+2727+ . "github.com/onsi/ginkgo/v2"
2828+ . "github.com/onsi/gomega"
2929+3030+ "github.com/kragniz/pist/test/utils"
3131+)
3232+3333+// namespace where the project is deployed in
3434+const namespace = "pist-system"
3535+3636+// serviceAccountName created for the project
3737+const serviceAccountName = "pist-controller-manager"
3838+3939+// metricsServiceName is the name of the metrics service of the project
4040+const metricsServiceName = "pist-controller-manager-metrics-service"
4141+4242+// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
4343+const metricsRoleBindingName = "pist-metrics-binding"
4444+4545+var _ = Describe("Manager", Ordered, func() {
4646+ var controllerPodName string
4747+4848+ // Before running the tests, set up the environment by creating the namespace,
4949+ // enforce the restricted security policy to the namespace, installing CRDs,
5050+ // and deploying the controller.
5151+ BeforeAll(func() {
5252+ By("creating manager namespace")
5353+ cmd := exec.Command("kubectl", "create", "ns", namespace)
5454+ _, err := utils.Run(cmd)
5555+ Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")
5656+5757+ By("labeling the namespace to enforce the restricted security policy")
5858+ cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
5959+ "pod-security.kubernetes.io/enforce=restricted")
6060+ _, err = utils.Run(cmd)
6161+ Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")
6262+6363+ By("installing CRDs")
6464+ cmd = exec.Command("make", "install")
6565+ _, err = utils.Run(cmd)
6666+ Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")
6767+6868+ By("deploying the controller-manager")
6969+ cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage))
7070+ _, err = utils.Run(cmd)
7171+ Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
7272+ })
7373+7474+ // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
7575+ // and deleting the namespace.
7676+ AfterAll(func() {
7777+ By("cleaning up the curl pod for metrics")
7878+ cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
7979+ _, _ = utils.Run(cmd)
8080+8181+ By("undeploying the controller-manager")
8282+ cmd = exec.Command("make", "undeploy")
8383+ _, _ = utils.Run(cmd)
8484+8585+ By("uninstalling CRDs")
8686+ cmd = exec.Command("make", "uninstall")
8787+ _, _ = utils.Run(cmd)
8888+8989+ By("removing manager namespace")
9090+ cmd = exec.Command("kubectl", "delete", "ns", namespace)
9191+ _, _ = utils.Run(cmd)
9292+ })
9393+9494+ // After each test, check for failures and collect logs, events,
9595+ // and pod descriptions for debugging.
9696+ AfterEach(func() {
9797+ specReport := CurrentSpecReport()
9898+ if specReport.Failed() {
9999+ By("Fetching controller manager pod logs")
100100+ cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
101101+ controllerLogs, err := utils.Run(cmd)
102102+ if err == nil {
103103+ _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
104104+ } else {
105105+ _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
106106+ }
107107+108108+ By("Fetching Kubernetes events")
109109+ cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
110110+ eventsOutput, err := utils.Run(cmd)
111111+ if err == nil {
112112+ _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
113113+ } else {
114114+ _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
115115+ }
116116+117117+ By("Fetching curl-metrics logs")
118118+ cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
119119+ metricsOutput, err := utils.Run(cmd)
120120+ if err == nil {
121121+ _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
122122+ } else {
123123+ _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
124124+ }
125125+126126+ By("Fetching controller manager pod description")
127127+ cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
128128+ podDescription, err := utils.Run(cmd)
129129+ if err == nil {
130130+ fmt.Println("Pod description:\n", podDescription)
131131+ } else {
132132+ fmt.Println("Failed to describe controller pod")
133133+ }
134134+ }
135135+ })
136136+137137+ SetDefaultEventuallyTimeout(2 * time.Minute)
138138+ SetDefaultEventuallyPollingInterval(time.Second)
139139+140140+ Context("Manager", func() {
141141+ It("should run successfully", func() {
142142+ By("validating that the controller-manager pod is running as expected")
143143+ verifyControllerUp := func(g Gomega) {
144144+ // Get the name of the controller-manager pod
145145+ cmd := exec.Command("kubectl", "get",
146146+ "pods", "-l", "control-plane=controller-manager",
147147+ "-o", "go-template={{ range .items }}"+
148148+ "{{ if not .metadata.deletionTimestamp }}"+
149149+ "{{ .metadata.name }}"+
150150+ "{{ \"\\n\" }}{{ end }}{{ end }}",
151151+ "-n", namespace,
152152+ )
153153+154154+ podOutput, err := utils.Run(cmd)
155155+ g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
156156+ podNames := utils.GetNonEmptyLines(podOutput)
157157+ g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
158158+ controllerPodName = podNames[0]
159159+ g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))
160160+161161+ // Validate the pod's status
162162+ cmd = exec.Command("kubectl", "get",
163163+ "pods", controllerPodName, "-o", "jsonpath={.status.phase}",
164164+ "-n", namespace,
165165+ )
166166+ output, err := utils.Run(cmd)
167167+ g.Expect(err).NotTo(HaveOccurred())
168168+ g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
169169+ }
170170+ Eventually(verifyControllerUp).Should(Succeed())
171171+ })
172172+173173+ It("should ensure the metrics endpoint is serving metrics", func() {
174174+ By("creating a ClusterRoleBinding for the service account to allow access to metrics")
175175+ cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
176176+ "--clusterrole=pist-metrics-reader",
177177+ fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
178178+ )
179179+ _, err := utils.Run(cmd)
180180+ Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")
181181+182182+ By("validating that the metrics service is available")
183183+ cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
184184+ _, err = utils.Run(cmd)
185185+ Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")
186186+187187+ By("getting the service account token")
188188+ token, err := serviceAccountToken()
189189+ Expect(err).NotTo(HaveOccurred())
190190+ Expect(token).NotTo(BeEmpty())
191191+192192+ By("waiting for the metrics endpoint to be ready")
193193+ verifyMetricsEndpointReady := func(g Gomega) {
194194+ cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace)
195195+ output, err := utils.Run(cmd)
196196+ g.Expect(err).NotTo(HaveOccurred())
197197+ g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready")
198198+ }
199199+ Eventually(verifyMetricsEndpointReady).Should(Succeed())
200200+201201+ By("verifying that the controller manager is serving the metrics server")
202202+ verifyMetricsServerStarted := func(g Gomega) {
203203+ cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
204204+ output, err := utils.Run(cmd)
205205+ g.Expect(err).NotTo(HaveOccurred())
206206+ g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"),
207207+ "Metrics server not yet started")
208208+ }
209209+ Eventually(verifyMetricsServerStarted).Should(Succeed())
210210+211211+ By("creating the curl-metrics pod to access the metrics endpoint")
212212+ cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
213213+ "--namespace", namespace,
214214+ "--image=curlimages/curl:latest",
215215+ "--overrides",
216216+ fmt.Sprintf(`{
217217+ "spec": {
218218+ "containers": [{
219219+ "name": "curl",
220220+ "image": "curlimages/curl:latest",
221221+ "command": ["/bin/sh", "-c"],
222222+ "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"],
223223+ "securityContext": {
224224+ "readOnlyRootFilesystem": true,
225225+ "allowPrivilegeEscalation": false,
226226+ "capabilities": {
227227+ "drop": ["ALL"]
228228+ },
229229+ "runAsNonRoot": true,
230230+ "runAsUser": 1000,
231231+ "seccompProfile": {
232232+ "type": "RuntimeDefault"
233233+ }
234234+ }
235235+ }],
236236+ "serviceAccountName": "%s"
237237+ }
238238+ }`, token, metricsServiceName, namespace, serviceAccountName))
239239+ _, err = utils.Run(cmd)
240240+ Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
241241+242242+ By("waiting for the curl-metrics pod to complete.")
243243+ verifyCurlUp := func(g Gomega) {
244244+ cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
245245+ "-o", "jsonpath={.status.phase}",
246246+ "-n", namespace)
247247+ output, err := utils.Run(cmd)
248248+ g.Expect(err).NotTo(HaveOccurred())
249249+ g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
250250+ }
251251+ Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())
252252+253253+ By("getting the metrics by checking curl-metrics logs")
254254+ metricsOutput := getMetricsOutput()
255255+ Expect(metricsOutput).To(ContainSubstring(
256256+ "controller_runtime_reconcile_total",
257257+ ))
258258+ })
259259+260260+ // +kubebuilder:scaffold:e2e-webhooks-checks
261261+262262+ // TODO: Customize the e2e test suite with scenarios specific to your project.
263263+ // Consider applying sample/CR(s) and check their status and/or verifying
264264+ // the reconciliation by using the metrics, i.e.:
265265+ // metricsOutput := getMetricsOutput()
266266+ // Expect(metricsOutput).To(ContainSubstring(
267267+ // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
268268+ // strings.ToLower(<Kind>),
269269+ // ))
270270+ })
271271+})
272272+273273+// serviceAccountToken returns a token for the specified service account in the given namespace.
274274+// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
275275+// and parsing the resulting token from the API response.
276276+func serviceAccountToken() (string, error) {
277277+ const tokenRequestRawString = `{
278278+ "apiVersion": "authentication.k8s.io/v1",
279279+ "kind": "TokenRequest"
280280+ }`
281281+282282+ // Temporary file to store the token request
283283+ secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
284284+ tokenRequestFile := filepath.Join("/tmp", secretName)
285285+ err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
286286+ if err != nil {
287287+ return "", err
288288+ }
289289+290290+ var out string
291291+ verifyTokenCreation := func(g Gomega) {
292292+ // Execute kubectl command to create the token
293293+ cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
294294+ "/api/v1/namespaces/%s/serviceaccounts/%s/token",
295295+ namespace,
296296+ serviceAccountName,
297297+ ), "-f", tokenRequestFile)
298298+299299+ output, err := cmd.CombinedOutput()
300300+ g.Expect(err).NotTo(HaveOccurred())
301301+302302+ // Parse the JSON output to extract the token
303303+ var token tokenRequest
304304+ err = json.Unmarshal(output, &token)
305305+ g.Expect(err).NotTo(HaveOccurred())
306306+307307+ out = token.Status.Token
308308+ }
309309+ Eventually(verifyTokenCreation).Should(Succeed())
310310+311311+ return out, err
312312+}
313313+314314+// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
315315+func getMetricsOutput() string {
316316+ By("getting the curl-metrics logs")
317317+ cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
318318+ metricsOutput, err := utils.Run(cmd)
319319+ Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
320320+ Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
321321+ return metricsOutput
322322+}
323323+324324+// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
325325+// containing only the token field that we need to extract.
326326+type tokenRequest struct {
327327+ Status struct {
328328+ Token string `json:"token"`
329329+ } `json:"status"`
330330+}
+267
test/utils/utils.go
···11+/*
22+Copyright 2026.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package utils
1818+1919+import (
2020+ "bufio"
2121+ "bytes"
2222+ "fmt"
2323+ "os"
2424+ "os/exec"
2525+ "strings"
2626+2727+ . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
2828+)
2929+3030+const (
3131+ prometheusOperatorVersion = "v0.77.1"
3232+ prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" +
3333+ "releases/download/%s/bundle.yaml"
3434+3535+ certmanagerVersion = "v1.16.3"
3636+ certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
3737+)
3838+3939+func warnError(err error) {
4040+ _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
4141+}
4242+4343+// Run executes the provided command within this context
4444+func Run(cmd *exec.Cmd) (string, error) {
4545+ dir, _ := GetProjectDir()
4646+ cmd.Dir = dir
4747+4848+ if err := os.Chdir(cmd.Dir); err != nil {
4949+ _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
5050+ }
5151+5252+ cmd.Env = append(os.Environ(), "GO111MODULE=on")
5353+ command := strings.Join(cmd.Args, " ")
5454+ _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
5555+ output, err := cmd.CombinedOutput()
5656+ if err != nil {
5757+ return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
5858+ }
5959+6060+ return string(output), nil
6161+}
6262+6363+// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics.
6464+func InstallPrometheusOperator() error {
6565+ url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
6666+ cmd := exec.Command("kubectl", "create", "-f", url)
6767+ _, err := Run(cmd)
6868+ return err
6969+}
7070+7171+// UninstallPrometheusOperator uninstalls the prometheus
7272+func UninstallPrometheusOperator() {
7373+ url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
7474+ cmd := exec.Command("kubectl", "delete", "-f", url)
7575+ if _, err := Run(cmd); err != nil {
7676+ warnError(err)
7777+ }
7878+}
7979+8080+// IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed
8181+// by verifying the existence of key CRDs related to Prometheus.
8282+func IsPrometheusCRDsInstalled() bool {
8383+ // List of common Prometheus CRDs
8484+ prometheusCRDs := []string{
8585+ "prometheuses.monitoring.coreos.com",
8686+ "prometheusrules.monitoring.coreos.com",
8787+ "prometheusagents.monitoring.coreos.com",
8888+ }
8989+9090+ cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name")
9191+ output, err := Run(cmd)
9292+ if err != nil {
9393+ return false
9494+ }
9595+ crdList := GetNonEmptyLines(output)
9696+ for _, crd := range prometheusCRDs {
9797+ for _, line := range crdList {
9898+ if strings.Contains(line, crd) {
9999+ return true
100100+ }
101101+ }
102102+ }
103103+104104+ return false
105105+}
106106+107107+// UninstallCertManager uninstalls the cert manager
108108+func UninstallCertManager() {
109109+ url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
110110+ cmd := exec.Command("kubectl", "delete", "-f", url)
111111+ if _, err := Run(cmd); err != nil {
112112+ warnError(err)
113113+ }
114114+115115+ // Delete leftover leases in kube-system (not cleaned by default)
116116+ kubeSystemLeases := []string{
117117+ "cert-manager-cainjector-leader-election",
118118+ "cert-manager-controller",
119119+ }
120120+ for _, lease := range kubeSystemLeases {
121121+ cmd = exec.Command("kubectl", "delete", "lease", lease,
122122+ "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
123123+ if _, err := Run(cmd); err != nil {
124124+ warnError(err)
125125+ }
126126+ }
127127+}
128128+129129+// InstallCertManager installs the cert manager bundle.
130130+func InstallCertManager() error {
131131+ url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
132132+ cmd := exec.Command("kubectl", "apply", "-f", url)
133133+ if _, err := Run(cmd); err != nil {
134134+ return err
135135+ }
136136+ // Wait for cert-manager-webhook to be ready, which can take time if cert-manager
137137+ // was re-installed after uninstalling on a cluster.
138138+ cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
139139+ "--for", "condition=Available",
140140+ "--namespace", "cert-manager",
141141+ "--timeout", "5m",
142142+ )
143143+144144+ _, err := Run(cmd)
145145+ return err
146146+}
147147+148148+// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
149149+// by verifying the existence of key CRDs related to Cert Manager.
150150+func IsCertManagerCRDsInstalled() bool {
151151+ // List of common Cert Manager CRDs
152152+ certManagerCRDs := []string{
153153+ "certificates.cert-manager.io",
154154+ "issuers.cert-manager.io",
155155+ "clusterissuers.cert-manager.io",
156156+ "certificaterequests.cert-manager.io",
157157+ "orders.acme.cert-manager.io",
158158+ "challenges.acme.cert-manager.io",
159159+ }
160160+161161+ // Execute the kubectl command to get all CRDs
162162+ cmd := exec.Command("kubectl", "get", "crds")
163163+ output, err := Run(cmd)
164164+ if err != nil {
165165+ return false
166166+ }
167167+168168+ // Check if any of the Cert Manager CRDs are present
169169+ crdList := GetNonEmptyLines(output)
170170+ for _, crd := range certManagerCRDs {
171171+ for _, line := range crdList {
172172+ if strings.Contains(line, crd) {
173173+ return true
174174+ }
175175+ }
176176+ }
177177+178178+ return false
179179+}
180180+181181+// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
182182+func LoadImageToKindClusterWithName(name string) error {
183183+ cluster := "kind"
184184+ if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
185185+ cluster = v
186186+ }
187187+ kindOptions := []string{"load", "docker-image", name, "--name", cluster}
188188+ cmd := exec.Command("kind", kindOptions...)
189189+ _, err := Run(cmd)
190190+ return err
191191+}
192192+193193+// GetNonEmptyLines converts given command output string into individual objects
194194+// according to line breakers, and ignores the empty elements in it.
195195+func GetNonEmptyLines(output string) []string {
196196+ var res []string
197197+ elements := strings.Split(output, "\n")
198198+ for _, element := range elements {
199199+ if element != "" {
200200+ res = append(res, element)
201201+ }
202202+ }
203203+204204+ return res
205205+}
206206+207207+// GetProjectDir will return the directory where the project is
208208+func GetProjectDir() (string, error) {
209209+ wd, err := os.Getwd()
210210+ if err != nil {
211211+ return wd, fmt.Errorf("failed to get current working directory: %w", err)
212212+ }
213213+ wd = strings.ReplaceAll(wd, "/test/e2e", "")
214214+ return wd, nil
215215+}
216216+217217+// UncommentCode searches for target in the file and remove the comment prefix
218218+// of the target content. The target content may span multiple lines.
219219+func UncommentCode(filename, target, prefix string) error {
220220+ // false positive
221221+ // nolint:gosec
222222+ content, err := os.ReadFile(filename)
223223+ if err != nil {
224224+ return fmt.Errorf("failed to read file %q: %w", filename, err)
225225+ }
226226+ strContent := string(content)
227227+228228+ idx := strings.Index(strContent, target)
229229+ if idx < 0 {
230230+ return fmt.Errorf("unable to find the code %q to be uncomment", target)
231231+ }
232232+233233+ out := new(bytes.Buffer)
234234+ _, err = out.Write(content[:idx])
235235+ if err != nil {
236236+ return fmt.Errorf("failed to write to output: %w", err)
237237+ }
238238+239239+ scanner := bufio.NewScanner(bytes.NewBufferString(target))
240240+ if !scanner.Scan() {
241241+ return nil
242242+ }
243243+ for {
244244+ if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
245245+ return fmt.Errorf("failed to write to output: %w", err)
246246+ }
247247+ // Avoid writing a newline in case the previous line was the last in target.
248248+ if !scanner.Scan() {
249249+ break
250250+ }
251251+ if _, err = out.WriteString("\n"); err != nil {
252252+ return fmt.Errorf("failed to write to output: %w", err)
253253+ }
254254+ }
255255+256256+ if _, err = out.Write(content[idx+len(target):]); err != nil {
257257+ return fmt.Errorf("failed to write to output: %w", err)
258258+ }
259259+260260+ // false positive
261261+ // nolint:gosec
262262+ if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
263263+ return fmt.Errorf("failed to write file %q: %w", filename, err)
264264+ }
265265+266266+ return nil
267267+}