diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e917e5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f637cd --- /dev/null +++ b/Makefile @@ -0,0 +1,289 @@ +# VERSION defines the project version for the bundle. +# Update this value when you upgrade the version of your project. +# To re-generate a bundle for another specific version without changing the standard setup, you can: +# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) +# - use environment variables to overwrite this value (e.g export VERSION=0.0.2) +VERSION ?= 0.0.1 + +# CHANNELS define the bundle channels used in the bundle. +# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") +# To re-generate a bundle for other specific channels without changing the standard setup, you can: +# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) +# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif + +# DEFAULT_CHANNEL defines the default channel used in the bundle. +# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") +# To re-generate a bundle for any other default channel without changing the default setup, you can: +# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) +# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. +# This variable is used to construct full image tags for bundle and catalog images. +# +# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both +# my.domain/runtimes-inventory-operator-bundle:$VERSION and my.domain/runtimes-inventory-operator-catalog:$VERSION. +IMAGE_TAG_BASE ?= my.domain/runtimes-inventory-operator + +# BUNDLE_IMG defines the image:tag used for the bundle. +# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) +BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) + +# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command +BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + +# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests +# You can enable this value if you would like to use SHA Based Digests +# To enable set flag to true +USE_IMAGE_DIGESTS ?= false +ifeq ($(USE_IMAGE_DIGESTS), true) + BUNDLE_GEN_FLAGS += --use-image-digests +endif + +# Set the Operator SDK version to use. By default, what is installed on the system is used. +# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. +OPERATOR_SDK_VERSION ?= v1.33.0 + +# Image URL to use all building/pushing image targets +IMG ?= controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.27.1 + +# 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 commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ + OPENSHIFT_API_MOD_VERSION=$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' github.com/openshift/api) \ + go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +# If you wish built 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: test ## 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 build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## 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 project-v3-builder + $(CONTAINER_TOOL) buildx use project-v3-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.0.1 +CONTROLLER_TOOLS_VERSION ?= v0.12.0 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + +.PHONY: operator-sdk +OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk +operator-sdk: ## Download operator-sdk locally if necessary. +ifeq (,$(wildcard $(OPERATOR_SDK))) +ifeq (, $(shell which operator-sdk 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPERATOR_SDK)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ + chmod +x $(OPERATOR_SDK) ;\ + } +else +OPERATOR_SDK = $(shell which operator-sdk) +endif +endif + +.PHONY: bundle +bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. + $(OPERATOR_SDK) generate kustomize manifests -q + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) + $(OPERATOR_SDK) bundle validate ./bundle + +.PHONY: bundle-build +bundle-build: ## Build the bundle image. + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + +.PHONY: bundle-push +bundle-push: ## Push the bundle image. + $(MAKE) docker-push IMG=$(BUNDLE_IMG) + +.PHONY: opm +OPM = ./bin/opm +opm: ## Download opm locally if necessary. +ifeq (,$(wildcard $(OPM))) +ifeq (,$(shell which opm 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPM)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ + chmod +x $(OPM) ;\ + } +else +OPM = $(shell which opm) +endif +endif + +# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). +# These images MUST exist in a registry and be pull-able. +BUNDLE_IMGS ?= $(BUNDLE_IMG) + +# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). +CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) + +# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. +ifneq ($(origin CATALOG_BASE_IMG), undefined) +FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) +endif + +# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. +# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: +# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator +.PHONY: catalog-build +catalog-build: opm ## Build a catalog image. + $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) + +# Push the catalog image. +.PHONY: catalog-push +catalog-push: ## Push a catalog image. + $(MAKE) docker-push IMG=$(CATALOG_IMG) diff --git a/README.md b/README.md new file mode 100644 index 0000000..63c8fe4 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# runtimes-inventory-operator +This project aims to provide a reusable component for Red Hat operators managing Java workloads. +This component allows these operators to more easily integrate their workloads into the Red Hat Insights +Runtimes Inventory. + +## Description +Containers running in OpenShift that support either the [Insights Java Client](https://github.com/RedHatInsights/insights-java-client) +or its corresponding [Java Agent](https://github.com/RedHatInsights/insights-java-agent), will attempt to send reports to Red Hat Insights. +Doing so requires authentication to associate the report with a particular Red Hat customer. On OpenShift, these containers will likely not have +the means to obtain this authentication information when sending their reports. + +This component allows operators for Red Hat products to create and manage an [APICast](https://github.com/3scale/APIcast) HTTP proxy, +configured with the necessary authentication information. +If the workload containers are configured to send their Insights reports to the proxy, they do not need to authenticate themselves. +The proxy is created using authentication information obtained from the OpenShift cluster that uniquely identifies it as belonging to a particular +customer. + +## Getting Started + +### Environment Variables +The following are required environment variables your operator must set: +- `RELATED_IMAGE_INSIGHTS_PROXY`: the container image to be used for the APICast proxy (e.g. `registry.redhat.io/3scale-amp2/apicast-gateway-rhel8:3scale2.14`) +- `INSIGHTS_BACKEND_DOMAIN`: the Red Hat Insights server host where reports will be forwarded (e.g. `console.redhat.com`) +- `INSIGHTS_ENABLED`: must be set to `true` in order for this component to run, this provides an opt-out mechanism for customers at the operator level + +Optionally set the following environment variable: +- `INSIGHTS_PROXY_DOMAIN`: only needed when testing against a staging Insights backend that requires a proxy to access + +### RBAC +Your operator will need to be run with the following permissions: +- Create, Get, List, Watch, Delete on Deployments, Services, Config Maps, Secrets in its own namespace +- Get, List, Watch on the OpenShift global pull secret: `pull-secret` in the `openshift-config` namespace +- Get, List, Watch on the cluster-scoped ClusterVersion resource, named `version` + +### UHC Auth Proxy +In order for Red Hat Insights to accept traffic from the proxy, the proxy must specify a User-Agent header +with an approved prefix. Ensure that your operator's name is added to the list of +[approved prefixes](https://github.com/RedHatInsights/uhc-auth-proxy/blob/02be85bd43fb083c2dbed8f24356d9c040b0d6b1/server/server.go#L46-L53). + +### Adding to your Manager +Inside your operator's main function, add this component by creating a new `InsightsIntegration` and +call its `Setup` method. You will need the following arguments: +- Your controller-runtime Manager +- The operator's name and namespace, which can be obtained from the Kubernetes downward API +- The UHC Auth Proxy approved User-Agent prefix, of the form `operator-name/x.y.z`, where x.y.z is your operator's version +- Your Manager's logger + +```go + insightsURL, err := insights.NewInsightsIntegration(mgr, + operatorName, operatorNamespace, userAgentPrefix, &setupLog).Setup() + if err != nil { + setupLog.Error(err, "failed to set up Insights integration") + } + setupLog.Info("Insights proxy set up", "url", insightsURL.String()) +``` + +This will add a new Insights Controller to your manager, which will be responsible for managing the proxy container. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..afded0a --- /dev/null +++ b/go.mod @@ -0,0 +1,75 @@ +module github.com/RedHatInsights/runtimes-inventory-operator + +go 1.20 + +require ( + github.com/go-logr/logr v1.2.4 + github.com/onsi/ginkgo/v2 v2.9.5 + github.com/onsi/gomega v1.27.7 + github.com/openshift/api v0.0.0-20240209153439-49a5add2e592 + k8s.io/api v0.27.2 + k8s.io/apimachinery v0.27.2 + k8s.io/client-go v0.27.2 + sigs.k8s.io/controller-runtime v0.15.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/zapr v1.2.4 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.6 // 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/matttproud/golang_protobuf_extensions v1.0.4 // 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.15.1 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.9.1 // indirect + gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.27.2 // indirect + k8s.io/component-base v0.27.2 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..faa6733 --- /dev/null +++ b/go.sum @@ -0,0 +1,287 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +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.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/openshift/api v0.0.0-20240209153439-49a5add2e592 h1:PcbtlsqIsD1bYu7ZFFCdafVvrskU8OxX5Wb8cHYQG7Y= +github.com/openshift/api v0.0.0-20240209153439-49a5add2e592/go.mod h1:yimSGmjsI+XF1mr+AKBs2//fSXIOhhetHGbMlBEfXbs= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +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.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= +gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo= +k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4= +k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo= +k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ= +k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg= +k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/client-go v0.27.2 h1:vDLSeuYvCHKeoQRhCXjxXO45nHVv2Ip4Fe0MfioMrhE= +k8s.io/client-go v0.27.2/go.mod h1:tY0gVmUsHrAmjzHX9zs7eCjxcBsf8IiNe7KQ52biTcQ= +k8s.io/component-base v0.27.2 h1:neju+7s/r5O4x4/txeUONNTS9r1HsPbyoPBAtHsDCpo= +k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= +sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..4335120 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright Red Hat. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/internal/common/common_utils.go b/internal/common/common_utils.go new file mode 100644 index 0000000..3d27703 --- /dev/null +++ b/internal/common/common_utils.go @@ -0,0 +1,72 @@ +// Copyright The Cryostat Authors. +// +// 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 common + +import ( + "os" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// OSUtils is an abstraction on functionality that interacts with the operating system +type OSUtils interface { + GetEnv(name string) string +} + +type DefaultOSUtils struct{} + +// GetEnv returns the value of the environment variable with the provided name. If no such +// variable exists, the empty string is returned. +func (o *DefaultOSUtils) GetEnv(name string) string { + return os.Getenv(name) +} + +// MergeLabelsAndAnnotations copies labels and annotations from a source +// to the destination ObjectMeta, overwriting any existing labels and +// annotations of the same key. +func MergeLabelsAndAnnotations(dest *metav1.ObjectMeta, srcLabels, srcAnnotations map[string]string) { + // Check and create labels/annotations map if absent + if dest.Labels == nil { + dest.Labels = map[string]string{} + } + if dest.Annotations == nil { + dest.Annotations = map[string]string{} + } + + // Merge labels and annotations, preferring those in the source + for k, v := range srcLabels { + dest.Labels[k] = v + } + for k, v := range srcAnnotations { + dest.Annotations[k] = v + } +} + +// SeccompProfile returns a SeccompProfile for the restricted +// Pod Security Standard that, on OpenShift, is backwards-compatible +// with OpenShift < 4.11. +// TODO Remove once OpenShift < 4.11 support is dropped +func SeccompProfile(openshift bool) *corev1.SeccompProfile { + // For backward-compatibility with OpenShift < 4.11, + // leave the seccompProfile empty. In OpenShift >= 4.11, + // the restricted-v2 SCC will populate it for us. + if openshift { + return nil + } + return &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + } +} diff --git a/internal/common/constants.go b/internal/common/constants.go new file mode 100644 index 0000000..1c0f485 --- /dev/null +++ b/internal/common/constants.go @@ -0,0 +1,28 @@ +// Copyright The Cryostat Authors. +// +// 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 common + +const ( + InsightsConfigMapName = "insights-proxy" + ProxyDeploymentName = InsightsConfigMapName + ProxyServiceName = ProxyDeploymentName + ProxyServicePort = 8080 + ProxySecretName = "apicastconf" + EnvInsightsBackendDomain = "INSIGHTS_BACKEND_DOMAIN" + EnvInsightsProxyDomain = "INSIGHTS_PROXY_DOMAIN" + EnvInsightsEnabled = "INSIGHTS_ENABLED" + // Environment variable to override the Insights proxy image + EnvInsightsProxyImageTag = "RELATED_IMAGE_INSIGHTS_PROXY" +) diff --git a/internal/controllers/apicast.go b/internal/controllers/apicast.go new file mode 100644 index 0000000..464d75c --- /dev/null +++ b/internal/controllers/apicast.go @@ -0,0 +1,104 @@ +// Copyright The Cryostat Authors. +// +// 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 controllers + +import ( + "bytes" + "text/template" +) + +type apiCastConfigParams struct { + FrontendDomains string + BackendInsightsDomain string + HeaderValue string + UserAgent string + ProxyDomain string +} + +var apiCastConfigTemplate = template.Must(template.New("").Parse(`{ + "services": [ + { + "id": "1", + "backend_version": "1", + "proxy": { + "hosts": [{{ .FrontendDomains }}], + "api_backend": "https://{{ .BackendInsightsDomain }}:443/", + "backend": { "endpoint": "http://127.0.0.1:8081", "host": "backend" }, + "policy_chain": [ + { + "name": "default_credentials", + "version": "builtin", + "configuration": { + "auth_type": "user_key", + "user_key": "dummy_key" + } + }, + {{- if .ProxyDomain }} + { + "name": "apicast.policy.http_proxy", + "configuration": { + "https_proxy": "http://{{ .ProxyDomain }}/", + "http_proxy": "http://{{ .ProxyDomain }}/" + } + }, + {{- end }} + { + "name": "headers", + "version": "builtin", + "configuration": { + "request": [ + { + "op": "set", + "header": "Authorization", + "value_type": "plain", + "value": "Bearer {{ .HeaderValue }}" + }, + { + "op": "set", + "header": "User-Agent", + "value_type": "plain", + "value": "{{ .UserAgent }}" + } + ] + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "proxy_rules": [ + { + "http_method": "POST", + "pattern": "/", + "metric_system_name": "hits", + "delta": 1, + "parameters": [], + "querystring_parameters": {} + } + ] + } + } + ] +}`)) + +func getAPICastConfig(params *apiCastConfigParams) (*string, error) { + buf := &bytes.Buffer{} + err := apiCastConfigTemplate.Execute(buf, params) + if err != nil { + return nil, err + } + result := buf.String() + return &result, nil +} diff --git a/internal/controllers/insights.go b/internal/controllers/insights.go new file mode 100644 index 0000000..6ccb619 --- /dev/null +++ b/internal/controllers/insights.go @@ -0,0 +1,376 @@ +// Copyright The Cryostat Authors. +// +// 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 controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/RedHatInsights/runtimes-inventory-operator/internal/common" + configv1 "github.com/openshift/api/config/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func (r *InsightsReconciler) reconcileInsights(ctx context.Context) error { + err := r.reconcilePullSecret(ctx) + if err != nil { + return err + } + err = r.reconcileProxyDeployment(ctx) + if err != nil { + return err + } + return r.reconcileProxyService(ctx) +} + +func (r *InsightsReconciler) reconcilePullSecret(ctx context.Context) error { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.ProxySecretName, + Namespace: r.Namespace, + }, + } + owner := &corev1.ConfigMap{} + err := r.Client.Get(ctx, types.NamespacedName{Name: common.InsightsConfigMapName, + Namespace: r.Namespace}, owner) + if err != nil { + return err + } + + token, err := r.getTokenFromPullSecret(ctx) + if err != nil { + return err + } + + userAgent, err := r.getUserAgentString(ctx) + if err != nil { + return err + } + + params := &apiCastConfigParams{ + FrontendDomains: fmt.Sprintf("\"%s\",\"%s.%s.svc.cluster.local\"", common.ProxyServiceName, common.ProxyServiceName, r.Namespace), + BackendInsightsDomain: r.backendDomain, + ProxyDomain: r.proxyDomain, + HeaderValue: *token, + UserAgent: *userAgent, + } + config, err := getAPICastConfig(params) + if err != nil { + return err + } + + return r.createOrUpdateProxySecret(ctx, secret, owner, *config) +} + +func (r *InsightsReconciler) reconcileProxyDeployment(ctx context.Context) error { + deploy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.ProxyDeploymentName, + Namespace: r.Namespace, + }, + } + owner := &corev1.ConfigMap{} + err := r.Client.Get(ctx, types.NamespacedName{Name: common.InsightsConfigMapName, + Namespace: r.Namespace}, owner) + if err != nil { + return err + } + + return r.createOrUpdateProxyDeployment(ctx, deploy, owner) +} + +func (r *InsightsReconciler) reconcileProxyService(ctx context.Context) error { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.ProxyServiceName, + Namespace: r.Namespace, + }, + } + owner := &corev1.ConfigMap{} + err := r.Client.Get(ctx, types.NamespacedName{Name: common.InsightsConfigMapName, + Namespace: r.Namespace}, owner) + if err != nil { + return err + } + + return r.createOrUpdateProxyService(ctx, svc, owner) +} + +func (r *InsightsReconciler) getTokenFromPullSecret(ctx context.Context) (*string, error) { + // Get the global pull secret + pullSecret := &corev1.Secret{} + err := r.Client.Get(ctx, types.NamespacedName{Namespace: "openshift-config", Name: "pull-secret"}, pullSecret) + if err != nil { + return nil, err + } + + // Look for the .dockerconfigjson key within it + dockerConfigRaw, pres := pullSecret.Data[corev1.DockerConfigJsonKey] + if !pres { + return nil, fmt.Errorf("no %s key present in pull secret", corev1.DockerConfigJsonKey) + } + + // Unmarshal the .dockerconfigjson into a struct + dockerConfig := struct { + Auths map[string]struct { + Auth string `json:"auth"` + } `json:"auths"` + }{} + err = json.Unmarshal(dockerConfigRaw, &dockerConfig) + if err != nil { + return nil, err + } + + // Look for the "cloud.openshift.com" auth + openshiftAuth, pres := dockerConfig.Auths["cloud.openshift.com"] + if !pres { + return nil, errors.New("no \"cloud.openshift.com\" auth within pull secret") + } + + token := strings.TrimSpace(openshiftAuth.Auth) + if strings.Contains(token, "\n") || strings.Contains(token, "\r") { + return nil, fmt.Errorf("invalid cloud.openshift.com token") + } + return &token, nil +} + +func (r *InsightsReconciler) getUserAgentString(ctx context.Context) (*string, error) { + cv := &configv1.ClusterVersion{} + err := r.Client.Get(ctx, types.NamespacedName{Name: "version"}, cv) + if err != nil { + return nil, err + } + + userAgent := fmt.Sprintf("%s cluster/%s", r.UserAgentPrefix, cv.Spec.ClusterID) + return &userAgent, nil +} + +func (r *InsightsReconciler) createOrUpdateProxySecret(ctx context.Context, secret *corev1.Secret, owner metav1.Object, + config string) error { + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, secret, func() error { + // Set the config map as controller + if err := controllerutil.SetControllerReference(owner, secret, r.Scheme); err != nil { + return err + } + // Add the APICast config.json + if secret.StringData == nil { + secret.StringData = map[string]string{} + } + secret.StringData["config.json"] = config + return nil + }) + if err != nil { + return err + } + r.Log.Info(fmt.Sprintf("Secret %s", op), "name", secret.Name, "namespace", secret.Namespace) + return nil +} + +func (r *InsightsReconciler) createOrUpdateProxyDeployment(ctx context.Context, deploy *appsv1.Deployment, owner metav1.Object) error { + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error { + labels := map[string]string{"app": common.ProxyDeploymentName} + annotations := map[string]string{} + common.MergeLabelsAndAnnotations(&deploy.ObjectMeta, labels, annotations) + // Set the config map as controller + if err := controllerutil.SetControllerReference(owner, deploy, r.Scheme); err != nil { + return err + } + // Immutable, only updated when the deployment is created + if deploy.CreationTimestamp.IsZero() { + // Selector is immutable, avoid modifying if possible + deploy.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": common.ProxyDeploymentName, + }, + } + } + + // Update pod template spec + r.createOrUpdateProxyPodSpec(deploy) + // Update pod template metadata + common.MergeLabelsAndAnnotations(&deploy.Spec.Template.ObjectMeta, labels, annotations) + return nil + }) + if err != nil { + return err + } + r.Log.Info(fmt.Sprintf("Deployment %s", op), "name", deploy.Name, "namespace", deploy.Namespace) + return nil +} + +func (r *InsightsReconciler) createOrUpdateProxyService(ctx context.Context, svc *corev1.Service, owner metav1.Object) error { + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, svc, func() error { + // Update labels and annotations + labels := map[string]string{"app": common.ProxyDeploymentName} + annotations := map[string]string{} + common.MergeLabelsAndAnnotations(&svc.ObjectMeta, labels, annotations) + + // Set the config map as controller + if err := controllerutil.SetControllerReference(owner, svc, r.Scheme); err != nil { + return err + } + // Update the service type + svc.Spec.Type = corev1.ServiceTypeClusterIP + svc.Spec.Selector = map[string]string{ + "app": common.ProxyDeploymentName, + } + svc.Spec.Ports = []corev1.ServicePort{ + { + Name: "proxy", + Port: common.ProxyServicePort, + TargetPort: intstr.FromString("proxy"), + }, + { + Name: "management", + Port: 8090, + TargetPort: intstr.FromString("management"), + }, + } + return nil + }) + if err != nil { + return err + } + r.Log.Info(fmt.Sprintf("Service %s", op), "name", svc.Name, "namespace", svc.Namespace) + return nil +} + +const ( + defaultProxyCPURequest = "50m" + defaultProxyCPULimit = "200m" + defaultProxyMemRequest = "64Mi" + defaultProxyMemLimit = "128Mi" + // ALL capability to drop for restricted pod security. See: + // https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + capabilityAll corev1.Capability = "ALL" +) + +func (r *InsightsReconciler) createOrUpdateProxyPodSpec(deploy *appsv1.Deployment) { + privEscalation := false + nonRoot := true + readOnlyMode := int32(0440) + + podSpec := &deploy.Spec.Template.Spec + // Create the container if it doesn't exist + var container *corev1.Container + if deploy.CreationTimestamp.IsZero() { + podSpec.Containers = []corev1.Container{{}} + } + container = &podSpec.Containers[0] + + // Set fields that are hard-coded by operator + container.Name = common.ProxyDeploymentName + container.Image = r.proxyImageTag + container.Env = []corev1.EnvVar{ + { + Name: "THREESCALE_CONFIG_FILE", + Value: "/tmp/gateway-configuration-volume/config.json", + }, + } + container.VolumeMounts = []corev1.VolumeMount{ + { + Name: "gateway-configuration-volume", + MountPath: "/tmp/gateway-configuration-volume", + ReadOnly: true, + }, + } + container.Ports = []corev1.ContainerPort{ + { + Name: "proxy", + ContainerPort: common.ProxyServicePort, + }, + { + Name: "management", + ContainerPort: 8090, + }, + { + Name: "metrics", + ContainerPort: 9421, + }, + } + container.SecurityContext = &corev1.SecurityContext{ + AllowPrivilegeEscalation: &privEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{capabilityAll}, + }, + } + container.LivenessProbe = &corev1.Probe{ + InitialDelaySeconds: 10, + TimeoutSeconds: 5, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/status/live", + Port: intstr.FromInt(8090), + }, + }, + } + container.ReadinessProbe = &corev1.Probe{ + InitialDelaySeconds: 15, + PeriodSeconds: 30, + TimeoutSeconds: 5, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/status/ready", + Port: intstr.FromInt(8090), + }, + }, + } + + // Set resource requirements only on creation, this allows + // the user to modify them if they wish + if deploy.CreationTimestamp.IsZero() { + container.Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(defaultProxyCPURequest), + corev1.ResourceMemory: resource.MustParse(defaultProxyMemRequest), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(defaultProxyCPULimit), + corev1.ResourceMemory: resource.MustParse(defaultProxyMemLimit), + }, + } + } + + podSpec.Volumes = []corev1.Volume{ // TODO detect change and redeploy + { + Name: "gateway-configuration-volume", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: common.ProxySecretName, + Items: []corev1.KeyToPath{ + { + Key: "config.json", + Path: "config.json", + Mode: &readOnlyMode, + }, + }, + }, + }, + }, + } + podSpec.SecurityContext = &corev1.PodSecurityContext{ + RunAsNonRoot: &nonRoot, + SeccompProfile: common.SeccompProfile(true), + } +} diff --git a/internal/controllers/insights_controller.go b/internal/controllers/insights_controller.go new file mode 100644 index 0000000..e12d4a1 --- /dev/null +++ b/internal/controllers/insights_controller.go @@ -0,0 +1,129 @@ +// Copyright The Cryostat Authors. +// +// 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 controllers + +import ( + "context" + "errors" + + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/RedHatInsights/runtimes-inventory-operator/internal/common" + "github.com/go-logr/logr" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// InsightsReconciler reconciles the Insights proxy for Cryostat agents +type InsightsReconciler struct { + *InsightsReconcilerConfig + backendDomain string + proxyDomain string + proxyImageTag string +} + +// InsightsReconcilerConfig contains configuration to create an InsightsReconciler +type InsightsReconcilerConfig struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Namespace string + UserAgentPrefix string + common.OSUtils +} + +// NewInsightsReconciler creates an InsightsReconciler using the provided configuration +func NewInsightsReconciler(config *InsightsReconcilerConfig) (*InsightsReconciler, error) { + backendDomain := config.GetEnv(common.EnvInsightsBackendDomain) + if len(backendDomain) == 0 { + return nil, errors.New("no backend domain provided for Insights") + } + imageTag := config.GetEnv(common.EnvInsightsProxyImageTag) + if len(imageTag) == 0 { + return nil, errors.New("no proxy image tag provided for Insights") + } + proxyDomain := config.GetEnv(common.EnvInsightsProxyDomain) + + return &InsightsReconciler{ + InsightsReconcilerConfig: config, + backendDomain: backendDomain, + proxyDomain: proxyDomain, + proxyImageTag: imageTag, + }, nil +} + +// +kubebuilder:rbac:groups=apps,resources=deployments;deployments/finalizers,verbs=* +// +kubebuilder:rbac:groups="",resources=services;secrets;configmaps;configmaps/finalizers,verbs=* +// +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch + +// Reconcile processes the Insights proxy deployment and configures it accordingly +func (r *InsightsReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { + reqLogger := r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconciling Insights Proxy") + + // Reconcile all Insights support + err := r.reconcileInsights(ctx) + if err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *InsightsReconciler) SetupWithManager(mgr ctrl.Manager) error { + c := ctrl.NewControllerManagedBy(mgr). + Named("insights"). + // Filter controller to watch only specific objects we care about + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.isPullSecretOrProxyConfig)). + Watches(&appsv1.Deployment{}, + handler.EnqueueRequestsFromMapFunc(r.isProxyDeployment)). + Watches(&corev1.Service{}, + handler.EnqueueRequestsFromMapFunc(r.isProxyService)) + return c.Complete(r) +} + +func (r *InsightsReconciler) isPullSecretOrProxyConfig(ctx context.Context, secret client.Object) []reconcile.Request { + if !(secret.GetNamespace() == "openshift-config" && secret.GetName() == "pull-secret") && + !(secret.GetNamespace() == r.Namespace && secret.GetName() == common.ProxySecretName) { + return nil + } + return r.proxyDeploymentRequest() +} + +func (r *InsightsReconciler) isProxyDeployment(ctx context.Context, deploy client.Object) []reconcile.Request { + if deploy.GetNamespace() != r.Namespace || deploy.GetName() != common.ProxyDeploymentName { + return nil + } + return r.proxyDeploymentRequest() +} + +func (r *InsightsReconciler) isProxyService(ctx context.Context, svc client.Object) []reconcile.Request { + if svc.GetNamespace() != r.Namespace || svc.GetName() != common.ProxyServiceName { + return nil + } + return r.proxyDeploymentRequest() +} + +func (r *InsightsReconciler) proxyDeploymentRequest() []reconcile.Request { + req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: r.Namespace, Name: common.ProxyDeploymentName}} + return []reconcile.Request{req} +} diff --git a/internal/controllers/insights_controller_test.go b/internal/controllers/insights_controller_test.go new file mode 100644 index 0000000..46d43ae --- /dev/null +++ b/internal/controllers/insights_controller_test.go @@ -0,0 +1,295 @@ +// Copyright The Cryostat Authors. +// +// 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 controllers_test + +import ( + "context" + "strconv" + + "github.com/RedHatInsights/runtimes-inventory-operator/internal/controllers" + "github.com/RedHatInsights/runtimes-inventory-operator/internal/controllers/test" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type insightsTestInput struct { + client ctrlclient.Client + controller *controllers.InsightsReconciler + objs []ctrlclient.Object + opNamespace string + *test.TestUtilsConfig + *test.InsightsTestResources +} + +var _ = Describe("InsightsController", func() { + var t *insightsTestInput + + count := 0 + namespaceWithSuffix := func(name string) string { + return name + "-" + strconv.Itoa(count) + } + + Describe("reconciling a request", func() { + BeforeEach(func() { + t = &insightsTestInput{ + TestUtilsConfig: &test.TestUtilsConfig{ + EnvInsightsEnabled: &[]bool{true}[0], + EnvInsightsBackendDomain: &[]string{"insights.example.com"}[0], + EnvInsightsProxyImageTag: &[]string{"example.com/proxy:latest"}[0], + }, + InsightsTestResources: &test.InsightsTestResources{ + Namespace: namespaceWithSuffix("controller-test"), + UserAgentPrefix: "test-operator/0.0.0", + }, + } + t.objs = []ctrlclient.Object{ + t.NewNamespace(), + t.NewGlobalPullSecret(), + t.NewClusterVersion(), + t.NewOperatorDeployment(), + t.NewProxyConfigMap(), + } + }) + + JustBeforeEach(func() { + s := scheme.Scheme + logger := zap.New() + logf.SetLogger(logger) + + t.client = k8sClient + for _, obj := range t.objs { + err := t.client.Create(context.Background(), obj) + Expect(err).ToNot(HaveOccurred()) + } + + config := &controllers.InsightsReconcilerConfig{ + Client: t.client, + Scheme: s, + Log: logger, + Namespace: t.Namespace, + UserAgentPrefix: t.UserAgentPrefix, + OSUtils: test.NewTestOSUtils(t.TestUtilsConfig), + } + controller, err := controllers.NewInsightsReconciler(config) + Expect(err).ToNot(HaveOccurred()) + t.controller = controller + }) + + JustAfterEach(func() { + for _, obj := range t.objs { + err := ctrlclient.IgnoreNotFound(t.client.Delete(context.Background(), obj)) + Expect(err).ToNot(HaveOccurred()) + } + }) + + AfterEach(func() { + count++ + }) + + Context("successfully creates required resources", func() { + Context("with defaults", func() { + JustBeforeEach(func() { + result, err := t.reconcile() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(reconcile.Result{})) + }) + It("should create the APICast config secret", func() { + expected := t.NewInsightsProxySecret() + actual := &corev1.Secret{} + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).ToNot(HaveOccurred()) + + Expect(actual.Labels).To(Equal(expected.Labels)) + Expect(actual.Annotations).To(Equal(expected.Annotations)) + Expect(metav1.IsControlledBy(actual, t.getProxyConfigMap())).To(BeTrue()) + Expect(actual.Data).To(HaveLen(1)) + Expect(actual.Data).To(HaveKey("config.json")) + Expect(actual.Data["config.json"]).To(MatchJSON(expected.StringData["config.json"])) + }) + It("should create the proxy deployment", func() { + expected := t.NewInsightsProxyDeployment() + actual := &appsv1.Deployment{} + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).ToNot(HaveOccurred()) + + t.checkProxyDeployment(actual, expected) + }) + It("should create the proxy service", func() { + expected := t.NewInsightsProxyService() + actual := &corev1.Service{} + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).ToNot(HaveOccurred()) + + Expect(actual.Labels).To(Equal(expected.Labels)) + Expect(actual.Annotations).To(Equal(expected.Annotations)) + Expect(metav1.IsControlledBy(actual, t.getProxyConfigMap())).To(BeTrue()) + + Expect(actual.Spec.Selector).To(Equal(expected.Spec.Selector)) + Expect(actual.Spec.Type).To(Equal(expected.Spec.Type)) + Expect(actual.Spec.Ports).To(ConsistOf(expected.Spec.Ports)) + }) + }) + Context("with a proxy domain", func() { + BeforeEach(func() { + t.EnvInsightsProxyDomain = &[]string{"proxy.example.com"}[0] + }) + JustBeforeEach(func() { + result, err := t.reconcile() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(reconcile.Result{})) + }) + It("should create the APICast config secret", func() { + expected := t.NewInsightsProxySecretWithProxyDomain() + actual := &corev1.Secret{} + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).ToNot(HaveOccurred()) + + Expect(actual.Labels).To(Equal(expected.Labels)) + Expect(actual.Annotations).To(Equal(expected.Annotations)) + Expect(metav1.IsControlledBy(actual, t.getProxyConfigMap())).To(BeTrue()) + Expect(actual.Data).To(HaveLen(1)) + Expect(actual.Data).To(HaveKey("config.json")) + Expect(actual.Data["config.json"]).To(MatchJSON(expected.StringData["config.json"])) + }) + }) + }) + Context("updating the deployment", func() { + BeforeEach(func() { + t.objs = append(t.objs, + t.NewInsightsProxyDeployment(), + t.NewInsightsProxySecret(), + t.NewInsightsProxyService(), + ) + }) + Context("with resource requirements", func() { + var resources *corev1.ResourceRequirements + + BeforeEach(func() { + resources = &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + } + }) + JustBeforeEach(func() { + // Fetch the deployment + deploy := t.getProxyDeployment() + + // Change the resource requirements + deploy.Spec.Template.Spec.Containers[0].Resources = *resources + + // Update the deployment + err := t.client.Update(context.Background(), deploy) + Expect(err).ToNot(HaveOccurred()) + + // Reconcile again + result, err := t.reconcile() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(reconcile.Result{})) + }) + It("should leave the custom resource requirements", func() { + // Fetch the deployment again + actual := t.getProxyDeployment() + + // Check only resource requirements differ from defaults + t.Resources = resources + expected := t.NewInsightsProxyDeployment() + t.checkProxyDeployment(actual, expected) + }) + }) + }) + }) +}) + +func (t *insightsTestInput) reconcile() (reconcile.Result, error) { + req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "insights-proxy", Namespace: t.Namespace}} + return t.controller.Reconcile(context.Background(), req) +} + +func (t *insightsTestInput) getProxyDeployment() *appsv1.Deployment { + deploy := t.NewInsightsProxyDeployment() + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: deploy.Name, + Namespace: deploy.Namespace, + }, deploy) + Expect(err).ToNot(HaveOccurred()) + return deploy +} + +func (t *insightsTestInput) checkProxyDeployment(actual, expected *appsv1.Deployment) { + Expect(actual.Labels).To(Equal(expected.Labels)) + Expect(actual.Annotations).To(Equal(expected.Annotations)) + Expect(metav1.IsControlledBy(actual, t.getProxyConfigMap())).To(BeTrue()) + Expect(actual.Spec.Selector).To(Equal(expected.Spec.Selector)) + + expectedTemplate := expected.Spec.Template + actualTemplate := actual.Spec.Template + Expect(actualTemplate.Labels).To(Equal(expectedTemplate.Labels)) + Expect(actualTemplate.Annotations).To(Equal(expectedTemplate.Annotations)) + Expect(actualTemplate.Spec.SecurityContext).To(Equal(expectedTemplate.Spec.SecurityContext)) + Expect(actualTemplate.Spec.Volumes).To(Equal(expectedTemplate.Spec.Volumes)) + + Expect(actualTemplate.Spec.Containers).To(HaveLen(1)) + expectedContainer := expectedTemplate.Spec.Containers[0] + actualContainer := actualTemplate.Spec.Containers[0] + Expect(actualContainer.Ports).To(ConsistOf(expectedContainer.Ports)) + Expect(actualContainer.Env).To(ConsistOf(expectedContainer.Env)) + Expect(actualContainer.EnvFrom).To(ConsistOf(expectedContainer.EnvFrom)) + Expect(actualContainer.VolumeMounts).To(ConsistOf(expectedContainer.VolumeMounts)) + Expect(actualContainer.LivenessProbe).To(Equal(expectedContainer.LivenessProbe)) + Expect(actualContainer.StartupProbe).To(Equal(expectedContainer.StartupProbe)) + Expect(actualContainer.SecurityContext).To(Equal(expectedContainer.SecurityContext)) + + test.ExpectResourceRequirements(&actualContainer.Resources, &expectedContainer.Resources) +} + +func (t *insightsTestInput) getProxyConfigMap() *corev1.ConfigMap { + cm := &corev1.ConfigMap{} + expected := t.NewProxyConfigMap() + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, cm) + Expect(err).ToNot(HaveOccurred()) + return cm +} diff --git a/internal/controllers/insights_controller_unit_test.go b/internal/controllers/insights_controller_unit_test.go new file mode 100644 index 0000000..194570e --- /dev/null +++ b/internal/controllers/insights_controller_unit_test.go @@ -0,0 +1,145 @@ +// Copyright The Cryostat Authors. +// +// 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 controllers + +import ( + "context" + + "github.com/RedHatInsights/runtimes-inventory-operator/internal/controllers/test" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type insightsUnitTestInput struct { + client ctrlclient.Client + controller *InsightsReconciler + objs []ctrlclient.Object + *test.TestUtilsConfig + *test.InsightsTestResources +} + +var _ = Describe("InsightsController", func() { + var t *insightsUnitTestInput + + Describe("configuring watches", func() { + + BeforeEach(func() { + t = &insightsUnitTestInput{ + TestUtilsConfig: &test.TestUtilsConfig{ + EnvInsightsEnabled: &[]bool{true}[0], + EnvInsightsBackendDomain: &[]string{"insights.example.com"}[0], + EnvInsightsProxyImageTag: &[]string{"example.com/proxy:latest"}[0], + }, + InsightsTestResources: &test.InsightsTestResources{ + Namespace: "test", + }, + } + t.objs = []ctrlclient.Object{ + t.NewNamespace(), + t.NewGlobalPullSecret(), + t.NewOperatorDeployment(), + } + }) + + JustBeforeEach(func() { + s := scheme.Scheme + logger := zap.New() + logf.SetLogger(logger) + + t.client = fake.NewClientBuilder().WithScheme(s).WithObjects(t.objs...).Build() + + config := &InsightsReconcilerConfig{ + Client: t.client, + Scheme: s, + Log: logger, + Namespace: t.Namespace, + OSUtils: test.NewTestOSUtils(t.TestUtilsConfig), + } + controller, err := NewInsightsReconciler(config) + Expect(err).ToNot(HaveOccurred()) + t.controller = controller + }) + + Context("for secrets", func() { + It("should reconcile global pull secret", func() { + result := t.controller.isPullSecretOrProxyConfig(context.Background(), t.NewGlobalPullSecret()) + Expect(result).To(ConsistOf(t.deploymentReconcileRequest())) + }) + It("should reconcile APICast secret", func() { + result := t.controller.isPullSecretOrProxyConfig(context.Background(), t.NewInsightsProxySecret()) + Expect(result).To(ConsistOf(t.deploymentReconcileRequest())) + }) + It("should not reconcile a secret in another namespace", func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.NewGlobalPullSecret().Name, + Namespace: "other", + }, + } + result := t.controller.isPullSecretOrProxyConfig(context.Background(), secret) + Expect(result).To(BeEmpty()) + }) + }) + + Context("for deployments", func() { + It("should reconcile proxy deployment", func() { + result := t.controller.isProxyDeployment(context.Background(), t.NewInsightsProxyDeployment()) + Expect(result).To(ConsistOf(t.deploymentReconcileRequest())) + }) + It("should not reconcile a deployment in another namespace", func() { + deploy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.NewInsightsProxyDeployment().Name, + Namespace: "other", + }, + } + result := t.controller.isProxyDeployment(context.Background(), deploy) + Expect(result).To(BeEmpty()) + }) + }) + + Context("for services", func() { + It("should reconcile proxy service", func() { + result := t.controller.isProxyService(context.Background(), t.NewInsightsProxyService()) + Expect(result).To(ConsistOf(t.deploymentReconcileRequest())) + }) + It("should not reconcile a service in another namespace", func() { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.NewInsightsProxyService().Name, + Namespace: "other", + }, + } + result := t.controller.isProxyService(context.Background(), svc) + Expect(result).To(BeEmpty()) + }) + }) + }) +}) + +func (t *insightsUnitTestInput) deploymentReconcileRequest() reconcile.Request { + return reconcile.Request{NamespacedName: types.NamespacedName{Name: "insights-proxy", Namespace: t.Namespace}} +} diff --git a/internal/controllers/insights_suite_test.go b/internal/controllers/insights_suite_test.go new file mode 100644 index 0000000..d963090 --- /dev/null +++ b/internal/controllers/insights_suite_test.go @@ -0,0 +1,104 @@ +// Copyright The Cryostat Authors. +// +// 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 controllers_test + +import ( + "context" + "fmt" + "go/build" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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" + + configv1 "github.com/openshift/api/config/v1" + corev1 "k8s.io/api/core/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestInsights(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Insights Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + + openshiftModVersion := os.Getenv("OPENSHIFT_API_MOD_VERSION") + Expect(openshiftModVersion).ToNot(BeEmpty(), "OPENSHIFT_API_MOD_VERSION environment variable must be set") + openshiftPrefix := []string{build.Default.GOPATH, "pkg", "mod", "github.com", "openshift", + "api@" + openshiftModVersion} + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + // No CRDs defined + //filepath.Join("..", "..", "..", "config", "crd", "bases"), + filepath.Join(append(openshiftPrefix, "config", "v1")...), + }, + ErrorIfCRDPathMissing: true, + } + fmt.Println(testEnv.CRDDirectoryPaths) + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = configv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // Create openshift-config namespace + err = k8sClient.Create(context.Background(), newOpenShiftConfigNamespace()) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +func newOpenShiftConfigNamespace() *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-config", + }, + } +} diff --git a/internal/controllers/test/expect.go b/internal/controllers/test/expect.go new file mode 100644 index 0000000..82dfada --- /dev/null +++ b/internal/controllers/test/expect.go @@ -0,0 +1,60 @@ +// Copyright The Cryostat Authors. +// +// 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 test + +import ( + "fmt" + + "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" +) + +func ExpectResourceRequirements(containerResource, expectedResource *corev1.ResourceRequirements) { + // Containers must have resource requests + gomega.Expect(containerResource.Requests).ToNot(gomega.BeNil()) + + requestCpu, requestCpuFound := containerResource.Requests[corev1.ResourceCPU] + expectedRequestCpu := expectedResource.Requests[corev1.ResourceCPU] + gomega.Expect(requestCpuFound).To(gomega.BeTrue()) + fmt.Printf("%+v\n%+v\n", containerResource.Requests, expectedResource.Requests) + gomega.Expect(requestCpu.Equal(expectedRequestCpu)).To(gomega.BeTrue()) + + requestMemory, requestMemoryFound := containerResource.Requests[corev1.ResourceMemory] + expectedRequestMemory := expectedResource.Requests[corev1.ResourceMemory] + gomega.Expect(requestMemoryFound).To(gomega.BeTrue()) + gomega.Expect(requestMemory.Equal(expectedRequestMemory)).To(gomega.BeTrue()) + + if expectedResource.Limits == nil { + gomega.Expect(containerResource.Limits).To(gomega.BeNil()) + } else { + gomega.Expect(containerResource.Limits).ToNot(gomega.BeNil()) + + limitCpu, limitCpuFound := containerResource.Limits[corev1.ResourceCPU] + expectedLimitCpu, expectedLimitCpuFound := expectedResource.Limits[corev1.ResourceCPU] + + gomega.Expect(limitCpuFound).To(gomega.Equal(expectedLimitCpuFound)) + if expectedLimitCpuFound { + gomega.Expect(limitCpu.Equal(expectedLimitCpu)).To(gomega.BeTrue()) + } + + limitMemory, limitMemoryFound := containerResource.Limits[corev1.ResourceMemory] + expectedlimitMemory, expectedLimitMemoryFound := expectedResource.Limits[corev1.ResourceMemory] + + gomega.Expect(limitMemoryFound).To(gomega.Equal(expectedLimitMemoryFound)) + if expectedLimitCpuFound { + gomega.Expect(limitMemory.Equal(expectedlimitMemory)).To(gomega.BeTrue()) + } + } +} diff --git a/internal/controllers/test/manager.go b/internal/controllers/test/manager.go new file mode 100644 index 0000000..c041a1a --- /dev/null +++ b/internal/controllers/test/manager.go @@ -0,0 +1,75 @@ +// Copyright The Cryostat Authors. +// +// 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 test + +import ( + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type FakeManager struct { + ctrl.Manager + client client.Client + scheme *runtime.Scheme + logger *logr.Logger +} + +var _ ctrl.Manager = &FakeManager{} + +func NewFakeManager(client client.Client, scheme *runtime.Scheme, logger *logr.Logger) *FakeManager { + return &FakeManager{ + client: client, + scheme: scheme, + logger: logger, + } +} + +func (m *FakeManager) GetCache() cache.Cache { + return nil +} + +func (m *FakeManager) GetClient() client.Client { + return m.client +} + +func (m *FakeManager) GetScheme() *runtime.Scheme { + return m.scheme +} + +func (m *FakeManager) GetAPIReader() client.Reader { + // May need to change if not using a fake client + return m.client +} + +func (m *FakeManager) GetControllerOptions() config.Controller { + return config.Controller{} +} + +func (m *FakeManager) GetLogger() logr.Logger { + return *m.logger +} + +func (m *FakeManager) SetFields(interface{}) error { + return nil +} + +func (m *FakeManager) Add(manager.Runnable) error { + return nil +} diff --git a/internal/controllers/test/resources.go b/internal/controllers/test/resources.go new file mode 100644 index 0000000..960a179 --- /dev/null +++ b/internal/controllers/test/resources.go @@ -0,0 +1,412 @@ +// Copyright The Cryostat Authors. +// +// 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 test + +import ( + "fmt" + + configv1 "github.com/openshift/api/config/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +type InsightsTestResources struct { + Namespace string + UserAgentPrefix string + Resources *corev1.ResourceRequirements +} + +func (r *InsightsTestResources) NewNamespace() *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.Namespace, + }, + } +} + +func (r *InsightsTestResources) NewGlobalPullSecret() *corev1.Secret { + config := `{"auths":{"example.com":{"auth":"hello"},"cloud.openshift.com":{"auth":"world"}}}` + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pull-secret", + Namespace: "openshift-config", + }, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte(config), + }, + } +} + +func (r *InsightsTestResources) NewOperatorDeployment() *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-controller-manager", + Namespace: r.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "control-plane": "controller-manager", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "control-plane": "controller-manager", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "manager", + Image: "example.com/operator:latest", + }, + }, + }, + }, + }, + } +} + +func (r *InsightsTestResources) NewProxyConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "insights-proxy", + Namespace: r.Namespace, + }, + } +} + +func (r *InsightsTestResources) NewInsightsProxySecret() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "apicastconf", + Namespace: r.Namespace, + }, + StringData: map[string]string{ + "config.json": fmt.Sprintf(`{ + "services": [ + { + "id": "1", + "backend_version": "1", + "proxy": { + "hosts": ["insights-proxy","insights-proxy.%s.svc.cluster.local"], + "api_backend": "https://insights.example.com:443/", + "backend": { "endpoint": "http://127.0.0.1:8081", "host": "backend" }, + "policy_chain": [ + { + "name": "default_credentials", + "version": "builtin", + "configuration": { + "auth_type": "user_key", + "user_key": "dummy_key" + } + }, + { + "name": "headers", + "version": "builtin", + "configuration": { + "request": [ + { + "op": "set", + "header": "Authorization", + "value_type": "plain", + "value": "Bearer world" + }, + { + "op": "set", + "header": "User-Agent", + "value_type": "plain", + "value": "%s cluster/abcde" + } + ] + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "proxy_rules": [ + { + "http_method": "POST", + "pattern": "/", + "metric_system_name": "hits", + "delta": 1, + "parameters": [], + "querystring_parameters": {} + } + ] + } + } + ] + }`, r.Namespace, r.UserAgentPrefix), + }, + } +} + +func (r *InsightsTestResources) NewInsightsProxySecretWithProxyDomain() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "apicastconf", + Namespace: r.Namespace, + }, + StringData: map[string]string{ + "config.json": fmt.Sprintf(`{ + "services": [ + { + "id": "1", + "backend_version": "1", + "proxy": { + "hosts": ["insights-proxy","insights-proxy.%s.svc.cluster.local"], + "api_backend": "https://insights.example.com:443/", + "backend": { "endpoint": "http://127.0.0.1:8081", "host": "backend" }, + "policy_chain": [ + { + "name": "default_credentials", + "version": "builtin", + "configuration": { + "auth_type": "user_key", + "user_key": "dummy_key" + } + }, + { + "name": "apicast.policy.http_proxy", + "configuration": { + "https_proxy": "http://proxy.example.com/", + "http_proxy": "http://proxy.example.com/" + } + }, + { + "name": "headers", + "version": "builtin", + "configuration": { + "request": [ + { + "op": "set", + "header": "Authorization", + "value_type": "plain", + "value": "Bearer world" + }, + { + "op": "set", + "header": "User-Agent", + "value_type": "plain", + "value": "%s cluster/abcde" + } + ] + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "proxy_rules": [ + { + "http_method": "POST", + "pattern": "/", + "metric_system_name": "hits", + "delta": 1, + "parameters": [], + "querystring_parameters": {} + } + ] + } + } + ] + }`, r.Namespace, r.UserAgentPrefix), + }, + } +} + +func (r *InsightsTestResources) NewInsightsProxyDeployment() *appsv1.Deployment { + var resources *corev1.ResourceRequirements + if r.Resources != nil { + resources = r.Resources + } else { + resources = &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + } + } + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "insights-proxy", + Namespace: r.Namespace, + Labels: map[string]string{ + "app": "insights-proxy", + }, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "insights-proxy", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "insights-proxy", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "insights-proxy", + Image: "example.com/proxy:latest", + Env: []corev1.EnvVar{ + { + Name: "THREESCALE_CONFIG_FILE", + Value: "/tmp/gateway-configuration-volume/config.json", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "gateway-configuration-volume", + MountPath: "/tmp/gateway-configuration-volume", + ReadOnly: true, + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "proxy", + Protocol: corev1.ProtocolTCP, + ContainerPort: 8080, + }, + { + Name: "management", + Protocol: corev1.ProtocolTCP, + ContainerPort: 8090, + }, + { + Name: "metrics", + Protocol: corev1.ProtocolTCP, + ContainerPort: 9421, + }, + }, + Resources: *resources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: 10, + TimeoutSeconds: 5, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/status/live", + Port: intstr.FromInt(8090), + Scheme: corev1.URISchemeHTTP, + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + InitialDelaySeconds: 15, + PeriodSeconds: 30, + TimeoutSeconds: 5, + SuccessThreshold: 1, + FailureThreshold: 3, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/status/ready", + Port: intstr.FromInt(8090), + Scheme: corev1.URISchemeHTTP, + }, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "gateway-configuration-volume", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "apicastconf", + Items: []corev1.KeyToPath{ + { + Key: "config.json", + Path: "config.json", + Mode: &[]int32{0440}[0], + }, + }, + DefaultMode: &[]int32{0644}[0], + }, + }, + }, + }, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + }, + }, + }, + }, + } +} + +func (r *InsightsTestResources) NewInsightsProxyService() *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "insights-proxy", + Namespace: r.Namespace, + Labels: map[string]string{ + "app": "insights-proxy", + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{ + "app": "insights-proxy", + }, + Ports: []corev1.ServicePort{ + { + Name: "proxy", + Protocol: corev1.ProtocolTCP, + Port: 8080, + TargetPort: intstr.FromString("proxy"), + }, + { + Name: "management", + Protocol: corev1.ProtocolTCP, + Port: 8090, + TargetPort: intstr.FromString("management"), + }, + }, + }, + } +} + +func (r *InsightsTestResources) NewClusterVersion() *configv1.ClusterVersion { + return &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "version", + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: "abcde", + }, + } +} diff --git a/internal/controllers/test/utils.go b/internal/controllers/test/utils.go new file mode 100644 index 0000000..85942b1 --- /dev/null +++ b/internal/controllers/test/utils.go @@ -0,0 +1,62 @@ +// Copyright The Cryostat Authors. +// +// 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 test + +import ( + "strconv" +) + +// TestUtilsConfig groups parameters used to create a test OSUtils +type TestUtilsConfig struct { + EnvInsightsEnabled *bool + EnvInsightsProxyImageTag *string + EnvInsightsBackendDomain *string + EnvInsightsProxyDomain *string +} + +type testOSUtils struct { + envs map[string]string +} + +func NewTestOSUtils(config *TestUtilsConfig) *testOSUtils { + envs := map[string]string{} + if config.EnvInsightsEnabled != nil { + envs["INSIGHTS_ENABLED"] = strconv.FormatBool(*config.EnvInsightsEnabled) + } + if config.EnvInsightsProxyImageTag != nil { + envs["RELATED_IMAGE_INSIGHTS_PROXY"] = *config.EnvInsightsProxyImageTag + } + if config.EnvInsightsBackendDomain != nil { + envs["INSIGHTS_BACKEND_DOMAIN"] = *config.EnvInsightsBackendDomain + } + if config.EnvInsightsProxyDomain != nil { + envs["INSIGHTS_PROXY_DOMAIN"] = *config.EnvInsightsProxyDomain + } + return &testOSUtils{envs: envs} +} + +func (o *testOSUtils) GetFileContents(path string) ([]byte, error) { + // Unused + return nil, nil +} + +func (o *testOSUtils) GetEnv(name string) string { + return o.envs[name] +} + +func (o *testOSUtils) GenPasswd(length int) string { + // Unused + return "" +} diff --git a/pkg/insights/setup.go b/pkg/insights/setup.go new file mode 100644 index 0000000..2629d8d --- /dev/null +++ b/pkg/insights/setup.go @@ -0,0 +1,183 @@ +// Copyright The Cryostat Authors. +// +// 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 insights + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/RedHatInsights/runtimes-inventory-operator/internal/common" + "github.com/RedHatInsights/runtimes-inventory-operator/internal/controllers" + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// InsightsIntegration allows your operator to manage a proxy +// for sending Red Hat Insights reports from Java-based workloads +// to the Runtimes Inventory service. +type InsightsIntegration struct { + Manager ctrl.Manager + Log *logr.Logger + opName string + opNamespace string + userAgentPrefix string + common.OSUtils +} + +// NewInsightsIntegration creates a new InsightsIntegration using +// your operator's Manager and logger. +// Provide the operator's name and namespace, +// which can be discovered using the Kubernetes downward API. +// The User Agent prefix must be an approved UHC Auth Proxy prefix. +func NewInsightsIntegration(mgr ctrl.Manager, operatorName string, operatorNamespace string, userAgentPrefix string, log *logr.Logger) *InsightsIntegration { + return &InsightsIntegration{ + Manager: mgr, + Log: log, + opName: operatorName, + opNamespace: operatorNamespace, + userAgentPrefix: userAgentPrefix, + OSUtils: &common.DefaultOSUtils{}, + } +} + +// Setup adds a controller to your manager, which creates and +// manages the HTTP proxy container that workloads may use +// to send reports to Red Hat Insights. +func (i *InsightsIntegration) Setup() (*url.URL, error) { + var proxyUrl *url.URL + // This will happen when running the operator locally + if len(i.opNamespace) == 0 { // TODO return error instead? + i.Log.Info("Operator namespace not detected") + return nil, nil + } + if len(i.opName) == 0 { + i.Log.Info("Operator name not detected") + return nil, nil + } + if len(i.userAgentPrefix) == 0 { + i.Log.Info("User Agent prefix not detected") + return nil, nil + } + + ctx := context.Background() + if i.isInsightsEnabled() { + err := i.createInsightsController() + if err != nil { + i.Log.Error(err, "unable to add controller to manager", "controller", "Insights") + return nil, err + } + // Create a Config Map to be used as a parent of all Insights Proxy related objects + err = i.createConfigMap(ctx) + if err != nil { + i.Log.Error(err, "failed to create config map for Insights") + return nil, err + } + proxyUrl = i.getProxyURL() + } else { + // Delete any previously created Config Map (and its children) + err := i.deleteConfigMap(ctx) + if err != nil { + i.Log.Error(err, "failed to delete config map for Insights") + return nil, err + } + + } + return proxyUrl, nil +} + +func (i *InsightsIntegration) isInsightsEnabled() bool { + return strings.ToLower(i.GetEnv(common.EnvInsightsEnabled)) == "true" +} + +func (i *InsightsIntegration) createInsightsController() error { + config := &controllers.InsightsReconcilerConfig{ + Client: i.Manager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Insights"), + Scheme: i.Manager.GetScheme(), + Namespace: i.opNamespace, + UserAgentPrefix: i.userAgentPrefix, + OSUtils: i.OSUtils, + } + controller, err := controllers.NewInsightsReconciler(config) + if err != nil { + return err + } + if err := controller.SetupWithManager(i.Manager); err != nil { + return err + } + return nil +} + +func (i *InsightsIntegration) createConfigMap(ctx context.Context) error { + // The config map should be owned by the operator deployment to ensure it and its descendants are garbage collected + owner := &appsv1.Deployment{} + // Use the APIReader instead of the cache, since the cache may not be synced yet + err := i.Manager.GetAPIReader().Get(ctx, types.NamespacedName{ + Name: i.opName, Namespace: i.opNamespace}, owner) + if err != nil { + return err + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.InsightsConfigMapName, + Namespace: i.opNamespace, + }, + } + err = controllerutil.SetControllerReference(owner, cm, i.Manager.GetScheme()) + if err != nil { + return err + } + + err = i.Manager.GetClient().Create(ctx, cm, &client.CreateOptions{}) + if err == nil { + i.Log.Info("Config Map for Insights created", "name", cm.Name, "namespace", cm.Namespace) + } + // This may already exist if the pod restarted + return client.IgnoreAlreadyExists(err) +} + +func (i *InsightsIntegration) deleteConfigMap(ctx context.Context) error { + // Children will be garbage collected + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.InsightsConfigMapName, + Namespace: i.opNamespace, + }, + } + + err := i.Manager.GetClient().Delete(ctx, cm, &client.DeleteOptions{}) + if err == nil { + i.Log.Info("Config Map for Insights deleted", "name", cm.Name, "namespace", cm.Namespace) + } + // This may not exist if no config map was previously created + return client.IgnoreNotFound(err) +} + +func (i *InsightsIntegration) getProxyURL() *url.URL { + return &url.URL{ + Scheme: "http", // TODO add https support + Host: fmt.Sprintf("%s.%s.svc.cluster.local:%d", common.ProxyServiceName, i.opNamespace, + common.ProxyServicePort), + } +} diff --git a/pkg/insights/setup_suite_test.go b/pkg/insights/setup_suite_test.go new file mode 100644 index 0000000..e342cba --- /dev/null +++ b/pkg/insights/setup_suite_test.go @@ -0,0 +1,77 @@ +// Copyright The Cryostat Authors. +// +// 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 insights_test + +import ( + "fmt" + "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" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestInsights(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Insights Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "config", "crd", "bases"), + }, + // No CRDs defined + //ErrorIfCRDPathMissing: true, + ErrorIfCRDPathMissing: false, + } + fmt.Println(testEnv.CRDDirectoryPaths) + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/pkg/insights/setup_test.go b/pkg/insights/setup_test.go new file mode 100644 index 0000000..8729651 --- /dev/null +++ b/pkg/insights/setup_test.go @@ -0,0 +1,196 @@ +// Copyright The Cryostat Authors. +// +// 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 insights_test + +import ( + "context" + "fmt" + "strconv" + + "github.com/RedHatInsights/runtimes-inventory-operator/internal/controllers/test" + "github.com/RedHatInsights/runtimes-inventory-operator/pkg/insights" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +type setupTestInput struct { + client ctrlclient.Client + objs []ctrlclient.Object + opNamespace string + integration *insights.InsightsIntegration + *test.TestUtilsConfig + *test.InsightsTestResources +} + +var _ = Describe("InsightsIntegration", func() { + var t *setupTestInput + + count := 0 + namespaceWithSuffix := func(name string) string { + return name + "-" + strconv.Itoa(count) + } + + Describe("setting up", func() { + BeforeEach(func() { + t = &setupTestInput{ + TestUtilsConfig: &test.TestUtilsConfig{ + EnvInsightsEnabled: &[]bool{true}[0], + EnvInsightsBackendDomain: &[]string{"insights.example.com"}[0], + EnvInsightsProxyImageTag: &[]string{"example.com/proxy:latest"}[0], + }, + InsightsTestResources: &test.InsightsTestResources{ + Namespace: namespaceWithSuffix("setup-test"), + UserAgentPrefix: "test-operator/0.0.0", + }, + } + t.objs = []ctrlclient.Object{ + t.NewNamespace(), + t.NewOperatorDeployment(), + } + t.opNamespace = t.Namespace + }) + + JustBeforeEach(func() { + s := scheme.Scheme + logger := zap.New() + logf.SetLogger(logger) + + t.client = k8sClient + for _, obj := range t.objs { + err := t.client.Create(context.Background(), obj) + Expect(err).ToNot(HaveOccurred()) + } + + manager := test.NewFakeManager(t.client, s, &logger) + deploy := t.NewOperatorDeployment() + t.integration = insights.NewInsightsIntegration(manager, deploy.Name, t.opNamespace, t.UserAgentPrefix, &logger) + t.integration.OSUtils = test.NewTestOSUtils(t.TestUtilsConfig) + }) + + JustAfterEach(func() { + for _, obj := range t.objs { + err := ctrlclient.IgnoreNotFound(t.client.Delete(context.Background(), obj)) + Expect(err).ToNot(HaveOccurred()) + } + }) + + AfterEach(func() { + count++ + }) + + Context("with defaults", func() { + It("should return proxy URL", func() { + result, err := t.integration.Setup() + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + Expect(result.String()).To(Equal(fmt.Sprintf("http://insights-proxy.%s.svc.cluster.local:8080", t.Namespace))) + }) + + It("should create config map", func() { + _, err := t.integration.Setup() + Expect(err).ToNot(HaveOccurred()) + + expected := t.NewProxyConfigMap() + actual := &corev1.ConfigMap{} + err = t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).ToNot(HaveOccurred()) + + Expect(actual.Labels).To(Equal(expected.Labels)) + Expect(actual.Annotations).To(Equal(expected.Annotations)) + Expect(metav1.IsControlledBy(actual, t.getOperatorDeployment())).To(BeTrue()) + Expect(actual.Data).To(BeEmpty()) + }) + }) + + Context("with Insights disabled", func() { + BeforeEach(func() { + t.EnvInsightsEnabled = &[]bool{false}[0] + t.objs = append(t.objs, + t.NewProxyConfigMap(), + ) + }) + + It("should return nil", func() { + result, err := t.integration.Setup() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeNil()) + }) + + It("should delete config map", func() { + _, err := t.integration.Setup() + Expect(err).ToNot(HaveOccurred()) + + expected := t.NewProxyConfigMap() + actual := &corev1.ConfigMap{} + err = t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).To(HaveOccurred()) + Expect(kerrors.IsNotFound(err)).To(BeTrue(), err.Error()) + }) + }) + + Context("when run out-of-cluster", func() { + BeforeEach(func() { + t.opNamespace = "" + }) + + It("should return nil", func() { + result, err := t.integration.Setup() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeNil()) + }) + + It("should not create config map", func() { + _, err := t.integration.Setup() + Expect(err).ToNot(HaveOccurred()) + + expected := t.NewProxyConfigMap() + actual := &corev1.ConfigMap{} + err = t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + Expect(err).To(HaveOccurred()) + Expect(kerrors.IsNotFound(err)).To(BeTrue(), err.Error()) + }) + }) + }) +}) + +func (t *setupTestInput) getOperatorDeployment() *appsv1.Deployment { + deploy := &appsv1.Deployment{} + expected := t.NewOperatorDeployment() + err := t.client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, deploy) + Expect(err).ToNot(HaveOccurred()) + return deploy +}