diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..63e5c32 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +# Build the manager binary +FROM golang:1.22.1-alpine as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# Copy the go source +COPY main.go main.go +COPY api api/ +COPY controllers controllers/ + +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +RUN GO111MODULE=on go get github.com/mikefarah/yq/v3 +RUN apk add curl + +ARG TAG_default_tag=from_dockerfile + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.buildVersion=${TAG_default_tag}'" -a -o manager main.go + + +ENV VERSION_SERVICEBINDING=0.4.0 +ENV VERSION_KPACK=0.13.4 +ENV VERSION_CERT_MANAGER=1.14.6 +ENV VERSION_GATEWAY_API=1.1.0 +ENV VERSION_TWUNI=2.2.3 +ENV VERSION_KORIFI=0.12.0 + + +WORKDIR /workspace/module-data/servicebinding +RUN curl -O https://github.com/servicebinding/runtime/releases/download/v$VERSION_SERVICEBINDING/servicebinding-runtime-v$VERSION_SERVICEBINDING.yaml +RUN curl -O https://github.com/servicebinding/runtime/releases/download/v$VERSION_SERVICEBINDING/servicebinding-workloadresourcemappings-v$VERSION_SERVICEBINDING.yaml + +WORKDIR /workspace/module-data/kpack +RUN curl -O https://github.com/buildpacks-community/kpack/releases/download/v$VERSION_KPACK/release-$VERSION_KPACK.yaml + +WORKDIR /workspace/module-data/cert-manager +RUN curl -O https://github.com/cert-manager/cert-manager/releases/download/v$VERSION_CERT_MANAGER/cert-manager.yaml + +WORKDIR /workspace/module-data/gateway-api +RUN curl -O https://github.com/kubernetes-sigs/gateway-api/releases/download/v$VERSION_GATEWAY_API/experimental-install.yaml + +WORKDIR /workspace/module-data/twuni-helm +RUN curl -L -O https://github.com/twuni/docker-registry.helm/archive/refs/tags/v$VERSION_TWUNI.tar.gz + +#Some day we are going to use the OSS Korifi project +#WORKDIR /workspace/module-data/korifi +#RUN curl -L -O https://github.com/cloudfoundry/korifi/releases/download/v$VERSION_KORIFI/korifi-$VERSION_KORIFI.tgz + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +COPY --from=builder --chown=65532:65532 /workspace/module-data module-data/ +COPY --chown=65532:65532 module-data module-data/ +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4ea6b0a --- /dev/null +++ b/Makefile @@ -0,0 +1,241 @@ +# Image URL to use all building/pushing image targets +#IMG ?= controller:latest +VERSION ?= 0.0.0 +#IMG ?= trinity.common.repositories.cloud.sap/kyma-module/cfapi-controller-$(VERSION) +REGISTRY = ghcr.io +IMG ?= kyma-project/cfapi/cfapi-controller-$(VERSION) + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.24.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 + +# Credentials used for authenticating into the module registry +# see `kyma alpha mod create --help for more info` + +# This will change the flags of the `kyma alpha module create` command in case we spot credentials +# Otherwise we will assume http-based local registries without authentication (e.g. for k3d) +ifneq (,$(PROW_JOB_ID)) +GCP_ACCESS_TOKEN=$(shell gcloud auth application-default print-access-token) +MODULE_CREATION_FLAGS=--registry $(MODULE_REGISTRY) --module-archive-version-overwrite -c oauth2accesstoken:$(GCP_ACCESS_TOKEN) +else ifeq (,$(MODULE_CREDENTIALS)) +# when built locally we should not include security content. +MODULE_CREATION_FLAGS=--registry $(MODULE_REGISTRY) --module-archive-version-overwrite --insecure --sec-scanners-config=sec-scanners-config-local.yaml +else +MODULE_CREATION_FLAGS=--registry $(MODULE_REGISTRY) --module-archive-version-overwrite -c $(MODULE_CREDENTIALS) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# 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) 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: test +test: manifests generate fmt vet envtest ## Run tests. + ACK_GINKGO_DEPRECATIONS=1.16.5 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: generate fmt vet lint ## Build manager binary. + go build -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + docker build -t ${REGISTRY}/${IMG} --build-arg TARGETARCH=amd64 . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. +ifneq (,$(GCR_DOCKER_PASSWORD)) + docker login $(IMG_REGISTRY) -u oauth2accesstoken --password $(GCR_DOCKER_PASSWORD) +endif + docker push ${REGISTRY}/${IMG} + +##@ Release +.PHONY: release +release: manifests kustomize + rm -rf release-$(VERSION) + mkdir -p release-$(VERSION) + cp default-cr.yaml release-$(VERSION)/cfapi-default-cr.yaml + $(KUSTOMIZE) build config/crd > release-$(VERSION)/cfapi-crd.yaml + pushd config/manager && $(KUSTOMIZE) edit set image controller=${REGISRRY}/${IMG} && popd + $(KUSTOMIZE) build config/default > release-$(VERSION)/cfapi-manager.yaml + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: provision +provision: kyma + ${KYMA} provision --ci k3d + kubectl create namespace cfapi-system + +.PHONY: install-istio +install-istio: system-namespace + kubectl label namespace cfapi-system istio-injection=enabled --overwrite + kubectl apply -f https://github.com/kyma-project/istio/releases/latest/download/istio-manager.yaml + kubectl apply -f module-data/istio/istio-default-cr.yaml + +.PHONY: install-istio-experimental +install-istio-experimental: system-namespace + kubectl label namespace cfapi-system istio-injection=enabled --overwrite + kubectl apply -f https://github.com/kyma-project/istio/releases/latest/download/istio-manager-experimental.yaml + kubectl apply -f module-data/istio/istio-default-cr-experimental.yaml + + +.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: system-namespace +system-namespace: + kubectl create namespace cfapi-system --dry-run=client -o yaml | kubectl apply -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: deploy-cr +deploy-cr: manifests + kubectl apply -f default-cr.yaml + +.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 - + +##@ Tools + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +########## Kustomize ########### +KUSTOMIZE ?= $(LOCALBIN)/kustomize +KUSTOMIZE_VERSION ?= v5.3.0 +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download & Build kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) + +########## controller-gen ########### +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +CONTROLLER_TOOLS_VERSION ?= v0.14.0 +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download & Build controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +########## envtest ########### +ENVTEST ?= $(LOCALBIN)/setup-envtest +.PHONY: envtest +envtest: $(ENVTEST) ## Download & Build envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + +##@ Checks + +########## static code checks ########### +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +GOLANG_CI_LINT = $(LOCALBIN)/golangci-lint +GOLANG_CI_LINT_VERSION ?= v1.56.2 +.PHONY: lint +lint: ## Download & Build & Run golangci-lint against code. + GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANG_CI_LINT_VERSION) + $(LOCALBIN)/golangci-lint run + +.PHONY: configure-git-origin +configure-git-origin: + @git remote | grep '^origin$$' -q || \ + git remote add origin https://github.com/kyma-project/template-operator + +.PHONY: build-manifests +build-manifests: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/default > template-operator.yaml + +DEFAULT_CR ?= $(shell pwd)/config/samples/default-sample-cr.yaml +.PHONY: build-module +build-module: kyma build-manifests configure-git-origin ## Build the Module and push it to a registry defined in MODULE_REGISTRY + ################################################################# + ## Building module with: + # - image: ${IMG} + # - channel: ${MODULE_CHANNEL} + # - name: kyma-project.io/module/$(MODULE_NAME) + # - version: $(MODULE_VERSION) + echo "running alpha create" + @$(KYMA) alpha create module --path . --output=module-template.yaml --module-config-file=module-config.yaml $(MODULE_CREATION_FLAGS) + +########## Kyma CLI ########### +KYMA_STABILITY ?= unstable + +# $(call os_error, os-type, os-architecture) +define os_error +$(error Error: unsuported platform OS_TYPE:$1, OS_ARCH:$2; to mitigate this problem set variable KYMA with absolute path to kyma-cli binary compatible with your operating system and architecture) +endef + +KYMA_FILE_NAME ?= $(shell ./scripts/local/get_kyma_file_name.sh) +KYMA ?= $(LOCALBIN)/kyma-$(KYMA_STABILITY) + +.PHONY: kyma +kyma: $(LOCALBIN) $(KYMA) ## Download kyma CLI locally if necessary. +$(KYMA): + ################################################################# + $(if $(KYMA_FILE_NAME),,$(call os_error, ${OS_TYPE}, ${OS_ARCH})) + ## Downloading Kyma CLI: https://storage.googleapis.com/kyma-cli-$(KYMA_STABILITY)/$(KYMA_FILE_NAME) + test -f $@ || curl -s -Lo $(KYMA) https://storage.googleapis.com/kyma-cli-$(KYMA_STABILITY)/$(KYMA_FILE_NAME) + chmod 0100 $(KYMA) + ${KYMA} version -c diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..45575e6 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,10 @@ +# RELEASE version 0.0.4 + +# Prerequisites +* UAA set as OIDC provider +* A dockerregistry secret with name cfapi-system-registry with credentials to artifactory project trinity + +# In this release +* API servicebinding.io installed +* CR OpenIDConnect installed in case CRD is found +* Istio experimental Gateway API supported diff --git a/api/.DS_Store b/api/.DS_Store new file mode 100644 index 0000000..fa265e4 Binary files /dev/null and b/api/.DS_Store differ diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..1b84819 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,22 @@ +module github.com/kyma-project/template-operator/api + +go 1.22.1 + +require k8s.io/apimachinery v0.28.3 + +require ( + github.com/go-logr/logr v1.2.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..7aef9d6 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,82 @@ +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/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/api/v1alpha1/cfapi_types.go b/api/v1alpha1/cfapi_types.go new file mode 100644 index 0000000..d218e26 --- /dev/null +++ b/api/v1alpha1/cfapi_types.go @@ -0,0 +1,114 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the component v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=operator.kyma-project.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + CFAPIKind Kind = "CFAPI" + Version Kind = "v1alpha1" +) + +type Kind string + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "operator.kyma-project.io", Version: "v1alpha1"} + + ConditionTypeInstallation = "Installation" + ConditionReasonReady = "Ready" +) + +type CFAPIStatus struct { + Status `json:",inline"` + + // Conditions contain a set of conditionals to determine the State of Status. + // If all Conditions are met, State is expected to be in StateReady. + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // URL contains the URL that should be used by the cf CLI in order + // to consume the CF API. + URL string `json:"url,omitempty"` +} + +func (s *CFAPIStatus) WithState(state State) *CFAPIStatus { + s.State = state + return s +} + +func (s *CFAPIStatus) WithURL(url string) *CFAPIStatus { + s.URL = url + return s +} + +func (s *CFAPIStatus) WithInstallConditionStatus(status metav1.ConditionStatus, objGeneration int64) *CFAPIStatus { + if s.Conditions == nil { + s.Conditions = make([]metav1.Condition, 0, 1) + } + + condition := meta.FindStatusCondition(s.Conditions, ConditionTypeInstallation) + + if condition == nil { + condition = &metav1.Condition{ + Type: ConditionTypeInstallation, + Reason: ConditionReasonReady, + Message: "installation is ready and resources can be used", + } + } + + condition.Status = status + condition.ObservedGeneration = objGeneration + meta.SetStatusCondition(&s.Conditions, *condition) + return s +} + +type CFAPISpec struct { + RootNamespace string `json:"rootNamespace,omitempty"` + AppImagePullSecret string `json:"appImagePullSecret,omitempty"` + UAA string `json:"uaa,omitempty"` + CFAdmins []string `json:"cfadmins,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="State",type=string,JSONPath=".status.state" +//+kubebuilder:printcolumn:name="URL",type=string,JSONPath=".status.url" + +// CFAPI is the Schema for the samples API. +type CFAPI struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CFAPISpec `json:"spec,omitempty"` + Status CFAPIStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// CFAPIList contains a list of CFAPI. +type CFAPIList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CFAPI `json:"items"` +} diff --git a/api/v1alpha1/managed_types.go b/api/v1alpha1/managed_types.go new file mode 100644 index 0000000..b884364 --- /dev/null +++ b/api/v1alpha1/managed_types.go @@ -0,0 +1,40 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the component v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=operator.kyma-project.io +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true + +type Managed struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` +} + +// +kubebuilder:object:root=true + +// ManagedList contains a list of Managed. +type ManagedList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Managed `json:"items"` +} diff --git a/api/v1alpha1/status.go b/api/v1alpha1/status.go new file mode 100644 index 0000000..b23d056 --- /dev/null +++ b/api/v1alpha1/status.go @@ -0,0 +1,37 @@ +package v1alpha1 + +type State string + +// Valid Module CR States. +const ( + // StateReady signifies Module CR is Ready and has been installed successfully. + StateReady State = "Ready" + + // StateProcessing signifies Module CR is reconciling and is in the process of installation. + // Processing can also signal that the Installation previously encountered an error and is now recovering. + StateProcessing State = "Processing" + + // StateError signifies an error for Module CR. This signifies that the Installation + // process encountered an error. + // Contrary to Processing, it can be expected that this state should change on the next retry. + StateError State = "Error" + + // StateDeleting signifies Module CR is being deleted. This is the state that is used + // when a deletionTimestamp was detected and Finalizers are picked up. + StateDeleting State = "Deleting" + + // StateWarning signifies specified resource has been deployed, but cannot be used due to misconfiguration, + // usually it means that user interaction is required. + StateWarning State = "Warning" +) + +// +k8s:deepcopy-gen=true + +// Status defines the observed state of Module CR. +type Status struct { + // State signifies current state of Module CR. + // Value can be one of ("Ready", "Processing", "Error", "Deleting"). + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=Processing;Deleting;Ready;Error;Warning;"" + State State `json:"state"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..717a484 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,195 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFAPI) DeepCopyInto(out *CFAPI) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFAPI. +func (in *CFAPI) DeepCopy() *CFAPI { + if in == nil { + return nil + } + out := new(CFAPI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CFAPI) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFAPIList) DeepCopyInto(out *CFAPIList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CFAPI, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFAPIList. +func (in *CFAPIList) DeepCopy() *CFAPIList { + if in == nil { + return nil + } + out := new(CFAPIList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CFAPIList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFAPISpec) DeepCopyInto(out *CFAPISpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFAPISpec. +func (in *CFAPISpec) DeepCopy() *CFAPISpec { + if in == nil { + return nil + } + out := new(CFAPISpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFAPIStatus) DeepCopyInto(out *CFAPIStatus) { + *out = *in + out.Status = in.Status + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFAPIStatus. +func (in *CFAPIStatus) DeepCopy() *CFAPIStatus { + if in == nil { + return nil + } + out := new(CFAPIStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Managed) DeepCopyInto(out *Managed) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Managed. +func (in *Managed) DeepCopy() *Managed { + if in == nil { + return nil + } + out := new(Managed) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Managed) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedList) DeepCopyInto(out *ManagedList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Managed, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedList. +func (in *ManagedList) DeepCopy() *ManagedList { + if in == nil { + return nil + } + out := new(ManagedList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/config/.DS_Store b/config/.DS_Store new file mode 100644 index 0000000..69bbda7 Binary files /dev/null and b/config/.DS_Store differ diff --git a/config/crd/.DS_Store b/config/crd/.DS_Store new file mode 100644 index 0000000..ae5c94e Binary files /dev/null and b/config/crd/.DS_Store differ diff --git a/config/crd/bases/operator.kyma-project.io_cfapis.yaml b/config/crd/bases/operator.kyma-project.io_cfapis.yaml new file mode 100644 index 0000000..ad89468 --- /dev/null +++ b/config/crd/bases/operator.kyma-project.io_cfapis.yaml @@ -0,0 +1,153 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: cfapis.operator.kyma-project.io +spec: + group: operator.kyma-project.io + names: + kind: CFAPI + listKind: CFAPIList + plural: cfapis + singular: cfapi + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.url + name: URL + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: CFAPI is the Schema for the samples API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + appImagePullSecret: + type: string + korifiNamespace: + type: string + rootNamespace: + type: string + type: object + status: + properties: + conditions: + description: |- + Conditions contain a set of conditionals to determine the State of Status. + If all Conditions are met, State is expected to be in StateReady. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + state: + description: |- + State signifies current state of Module CR. + Value can be one of ("Ready", "Processing", "Error", "Deleting"). + enum: + - Processing + - Deleting + - Ready + - Error + - Warning + - "" + type: string + url: + description: |- + URL contains the URL that should be used by the cf CLI in order + to consume the CF API. + type: string + required: + - state + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/operator.kyma-project.io_manageds.yaml b/config/crd/bases/operator.kyma-project.io_manageds.yaml new file mode 100644 index 0000000..fab51d2 --- /dev/null +++ b/config/crd/bases/operator.kyma-project.io_manageds.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: manageds.operator.kyma-project.io +spec: + group: operator.kyma-project.io + names: + kind: Managed + listKind: ManagedList + plural: manageds + singular: managed + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + type: object + served: true + storage: true diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 0000000..7d5de21 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/operator.kyma-project.io_cfapis.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_kedas.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_kedas.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml \ No newline at end of file diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..ec5c150 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_samples.yaml b/config/crd/patches/cainjection_in_samples.yaml new file mode 100644 index 0000000..70ab5d1 --- /dev/null +++ b/config/crd/patches/cainjection_in_samples.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: samples.operator.kyma-project.io diff --git a/config/crd/patches/webhook_in_samples.yaml b/config/crd/patches/webhook_in_samples.yaml new file mode 100644 index 0000000..80b95c7 --- /dev/null +++ b/config/crd/patches/webhook_in_samples.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: samples.operator.kyma-project.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 0000000..c0a9371 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,24 @@ +# Adds namespace to all resources. +namespace: cfapi-system +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: cfapi- + +# Labels to add to all resources and selectors. +labels: + app.kubernetes.io/component: cfapi-manager.kyma-project.io + +resources: +- ../crd +- ../rbac +- ../manager + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +labels: +- includeSelectors: true + pairs: + app.kubernetes.io/component: cfapi-manager.kyma-project.io diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 0000000..8cc4c38 --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,8 @@ +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: trinity.common.repositories.cloud.sap/kyma-module/cfapi-controller + newTag: 0.0.0 \ No newline at end of file diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 0000000..6c7080e --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: manager + namespace: cfapi-system + labels: + control-plane: manager +spec: + selector: + matchLabels: + control-plane: manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: manager + spec: + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + image: controller:latest + imagePullPolicy: Always + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + memory: 64Mi + cpu: 100m + limits: + memory: 512Mi + serviceAccountName: manager + terminationGracePeriodSeconds: 10 diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 0000000..0bee984 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,9 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml \ No newline at end of file diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..c4a6855 --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,238 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - "*" + verbs: + - "*" +- apiGroups: + - apps + resources: + - deployments + - replicasets + - statefulsets + - statefulsets/finalizers + verbs: + - create + - update + - delete + - patch + - list + - watch + - get +- apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - create + - delete + - deletecollection + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + - podsecuritypolicies + verbs: + - get + - create + - update + - patch + - delete + - deletecollection + - use +- apiGroups: + - storage.k8s.io + resources: + - storageclasses + verbs: + - get + - list + - watch +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - rbac.authorization.k8s.io + resources: + - "*" + verbs: + - "*" +- apiGroups: + - scheduling.k8s.io + resources: + - priorityclasses + verbs: + - get + - watch + - list + - patch + - update + - create + - delete +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + - validatingwebhookconfigurations + verbs: + - get + - watch + - list + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - create + - update + - delete + - patch + - watch +- apiGroups: + - metrics.k8s.io + resources: + - pods + verbs: + - get + - list + - watch +- apiGroups: + - cert.gardener.cloud + resources: + - certificates + verbs: + - get + - watch + - list + - create + - update + - patch + - delete +- apiGroups: + - dns.gardener.cloud + resources: + - dnsentries + verbs: + - get + - list + - create + - update + - patch + - watch +- apiGroups: + - operator.kyma-project.io + resources: + - cfapis + - cfapis/finalizers + - cfapis/status + verbs: + - "*" +- apiGroups: + - networking.istio.io + resources: + - gateways + - envoyfilters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways + - referencegrants + - tlsroutes + - httproutes + - httproutes/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kpack.io + resources: + - "*" + verbs: + - "*" +- apiGroups: + - korifi.cloudfoundry.org + resources: + - "*" + verbs: + - "*" +- apiGroups: + - servicebinding.io + resources: + - servicebindings + verbs: + - get + - list + - create + - patch + - update + - delete + - watch +- apiGroups: + - services.cloud.sap.com + resources: + - servicebindings + - serviceinstances + verbs: + - get + - list + - create + - patch + - update + - delete + - watch +- apiGroups: + - cert-manager.io + - acme.cert-manager.io + resources: + - "*" + verbs: + - "*" +- apiGroups: + - apiregistration.k8s.io + resources: + - apiservices + verbs: + - "*" \ No newline at end of file diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 0000000..8765945 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: manager + namespace: cfapi-system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 0000000..db71156 --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,7 @@ +imagePullSecrets: +- name: cfapi-system-registry +apiVersion: v1 +kind: ServiceAccount +metadata: + name: manager + namespace: cfapi-system \ No newline at end of file diff --git a/controllers/cfapi_auth.go b/controllers/cfapi_auth.go new file mode 100644 index 0000000..de7c8d1 --- /dev/null +++ b/controllers/cfapi_auth.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "context" + "strings" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func (r *CFAPIReconciler) getUserClusterAdmins(ctx context.Context) (error, []rbacv1.Subject) { + subjects := []rbacv1.Subject{} + crblist := &rbacv1.ClusterRoleBindingList{} + err := r.Client.List(ctx, crblist, client.MatchingLabels{"app": "kyma"}) + if err != nil { + return err, subjects + } + for _, crb := range crblist.Items { + if crb.RoleRef.Name == "cluster-admin" { + for _, subject := range crb.Subjects { + if subject.Kind == "User" { + subjects = append(subjects, subject) + } + } + } + } + return nil, subjects +} + +func toSubjectList(users []string) []rbacv1.Subject { + if users == nil { + return nil + } + var subjects = make([]rbacv1.Subject, len(users)) + for i, user := range users { + subjects[i] = rbacv1.Subject{ + Kind: "User", + Name: user, + } + } + return subjects +} + +func (r *CFAPIReconciler) assignCfAdministrators(ctx context.Context, subjects []rbacv1.Subject, cfNs string) error { + logger := log.FromContext(ctx) + var err error + _subjects := subjects + + if len(subjects) == 0 { + logger.Info("No CF administrators specified, will set kyma cluster admins as CF administrators") + err, _subjects = r.getUserClusterAdmins(ctx) + if err != nil { + logger.Error(err, "Failed to list users having clusterrole/cluster-admin") + return nil + } + if len(_subjects) == 0 { + logger.Info("No users with kyma cluster-admin role found, no CF administrators set") + return nil + } + } + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cfapi-admins-binding", + Namespace: cfNs, + Annotations: map[string]string{ + "cloudfoundry.org/propagate-cf-role": "true", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "korifi-controllers-admin", + }, + Subjects: _subjects, + } + + userNames := make([]string, len(_subjects)) + for i, subject := range _subjects { + userNames[i] = subject.Name + } + logger.Info("Bind role/korifi-controllers-admin to cluser-admin users " + strings.Join(userNames, ",")) + + return r.createIfMissing(ctx, rb) +} diff --git a/controllers/cfapi_controller_rendered_resources.go b/controllers/cfapi_controller_rendered_resources.go new file mode 100644 index 0000000..0fc5e34 --- /dev/null +++ b/controllers/cfapi_controller_rendered_resources.go @@ -0,0 +1,953 @@ +/* +Copyright 2022. + +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" + "context" + "encoding/json" + "errors" + "fmt" + "text/template" + "time" + + "helm.sh/helm/v3/pkg/chart/loader" + + "sigs.k8s.io/controller-runtime/pkg/scheme" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.tools.sap/unified-runtime/cfapi-kyma-module/api/v1alpha1" + + "sigs.k8s.io/controller-runtime/pkg/controller" +) + +const ( + defaultUaaUrl = "https://uaa.cf.eu10.hana.ondemand.com" +) + +// CFAPIReconciler reconciles a Sample object. +type CFAPIReconciler struct { + client.Client + Scheme *runtime.Scheme + *rest.Config + // EventRecorder for creating k8s events + record.EventRecorder + FinalState v1alpha1.State + FinalDeletionState v1alpha1.State +} + +type ManifestResources struct { + Items []*unstructured.Unstructured + Blobs [][]byte +} + +type DockerRegistryConfig struct { + Auths map[string]DockerRegistryAuth `json:"auths"` +} + +type DockerRegistryAuth struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type ContainerRegistry struct { + Server string + User string + Pass string +} + +var ( + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: v1alpha1.GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +var ( + DefaultTwuniUser = "user" + DefaultTwuniPass = "pass" +) + +func init() { //nolint:gochecknoinits + SchemeBuilder.Register(&v1alpha1.CFAPI{}, &v1alpha1.CFAPIList{}) +} + +// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=cfapi,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=cfapi/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=cfapi/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch;get;list;watch +// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;patch;delete +// +kubebuilder:rbac:groups="apps",resources=deployments,verbs=create;patch;delete + +// SetupWithManager sets up the controller with the Manager. +func (r *CFAPIReconciler) SetupWithManager(mgr ctrl.Manager, rateLimiter RateLimiter) error { + r.Config = mgr.GetConfig() + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.CFAPI{}). + WithOptions(controller.Options{ + RateLimiter: TemplateRateLimiter( + rateLimiter.BaseDelay, + rateLimiter.FailureMaxDelay, + rateLimiter.Frequency, + rateLimiter.Burst, + ), + }). + Complete(r) +} + +// Reconcile is the entry point from the controller-runtime framework. +// It performs a reconciliation based on the passed ctrl.Request object. +func (r *CFAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + objectInstance := v1alpha1.CFAPI{} + + if err := r.Client.Get(ctx, req.NamespacedName, &objectInstance); err != nil { + // we'll ignore not-found errors, since they can't be fixed by an immediate + // requeue (we'll need to wait for a new notification), and we can get them + // on deleted requests. + logger.Info(req.NamespacedName.String() + " got deleted!") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // check if deletionTimestamp is set, retry until it gets deleted + status := getStatusFromSample(&objectInstance) + + // set state to FinalDeletionState (default is Deleting) if not set for an object with deletion timestamp + if !objectInstance.GetDeletionTimestamp().IsZero() && status.State != r.FinalDeletionState { + return ctrl.Result{}, r.setStatusForObjectInstance(ctx, &objectInstance, status.WithState(r.FinalDeletionState)) + } + + if objectInstance.GetDeletionTimestamp().IsZero() { + // add finalizer if not present + if controllerutil.AddFinalizer(&objectInstance, finalizer) { + return ctrl.Result{}, r.ssa(ctx, &objectInstance) + } + } + + switch status.State { + case "": + return ctrl.Result{}, r.HandleInitialState(ctx, &objectInstance) + case v1alpha1.StateProcessing: + return ctrl.Result{Requeue: true}, r.HandleProcessingState(ctx, &objectInstance) + case v1alpha1.StateDeleting: + return ctrl.Result{Requeue: true}, r.HandleDeletingState(ctx, &objectInstance) + case v1alpha1.StateError: + return ctrl.Result{Requeue: true}, r.HandleErrorState(ctx, &objectInstance) + case v1alpha1.StateReady, v1alpha1.StateWarning: + return ctrl.Result{RequeueAfter: requeueInterval}, r.HandleReadyState(ctx, &objectInstance) + } + + return ctrl.Result{}, nil +} + +// HandleInitialState bootstraps state handling for the reconciled resource. +func (r *CFAPIReconciler) HandleInitialState(ctx context.Context, objectInstance *v1alpha1.CFAPI) error { + status := getStatusFromSample(objectInstance) + + return r.setStatusForObjectInstance(ctx, objectInstance, status. + WithState(v1alpha1.StateProcessing). + WithInstallConditionStatus(metav1.ConditionUnknown, objectInstance.GetGeneration())) +} + +// HandleProcessingState processes the reconciled resource by processing the underlying resources. +// Based on the processing either a success or failure state is set on the reconciled resource. +func (r *CFAPIReconciler) HandleProcessingState(ctx context.Context, objectInstance *v1alpha1.CFAPI) error { + status := getStatusFromSample(objectInstance) + + url, err := r.processResources(ctx, objectInstance) + + if err != nil { + // stay in Processing state if FinalDeletionState is set to Processing + if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateProcessing { + return nil + } + + r.EventRecorder.Event(objectInstance, "Warning", "ResourcesInstall", err.Error()) + return r.setStatusForObjectInstance(ctx, objectInstance, status. + WithState(v1alpha1.StateError). + WithInstallConditionStatus(metav1.ConditionFalse, objectInstance.GetGeneration())) + } + // set eventual state to Ready - if no errors were found + return r.setStatusForObjectInstance(ctx, objectInstance, status. + WithState(r.FinalState). + WithURL(url). + WithInstallConditionStatus(metav1.ConditionTrue, objectInstance.GetGeneration())) +} + +// HandleErrorState handles error recovery for the reconciled resource. +func (r *CFAPIReconciler) HandleErrorState(ctx context.Context, objectInstance *v1alpha1.CFAPI) error { + status := getStatusFromSample(objectInstance) + url, err := r.processResources(ctx, objectInstance) + + if err != nil { + return err + } + + // stay in Error state if FinalDeletionState is set to Error + if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateError { + return nil + } + // set eventual state to Ready - if no errors were found + return r.setStatusForObjectInstance(ctx, objectInstance, status. + WithState(r.FinalState). + WithURL(url). + WithInstallConditionStatus(metav1.ConditionTrue, objectInstance.GetGeneration())) +} + +// HandleDeletingState processed the deletion on the reconciled resource. +// Once the deletion if processed the relevant finalizers (if applied) are removed. +func (r *CFAPIReconciler) HandleDeletingState(ctx context.Context, objectInstance *v1alpha1.CFAPI) error { + r.EventRecorder.Event(objectInstance, "Normal", "Deleting", "resource deleting") + logger := log.FromContext(ctx) + + status := getStatusFromSample(objectInstance) + + // TODO + resourceObjs, err := getResourcesFromLocalPath("", logger) + + if err != nil && controllerutil.RemoveFinalizer(objectInstance, finalizer) { + // if error is encountered simply remove the finalizer and delete the reconciled resource + return r.Client.Update(ctx, objectInstance) + } + r.EventRecorder.Event(objectInstance, "Normal", "ResourcesDelete", "deleting resources") + + // the resources to be installed are unstructured, + // so please make sure the types are available on the target cluster + for _, obj := range resourceObjs.Items { + if err = r.Client.Delete(ctx, obj); err != nil && !errors2.IsNotFound(err) { + // stay in Deleting state if FinalDeletionState is set to Deleting + if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateDeleting { + return nil + } + + logger.Error(err, "error during uninstallation of resources") + r.EventRecorder.Event(objectInstance, "Warning", "ResourcesDelete", "deleting resources error") + return r.setStatusForObjectInstance(ctx, objectInstance, status. + WithState(v1alpha1.StateError). + WithInstallConditionStatus(metav1.ConditionFalse, objectInstance.GetGeneration())) + } + } + + // if resources are ready to be deleted, remove finalizer + if controllerutil.RemoveFinalizer(objectInstance, finalizer) { + return r.Client.Update(ctx, objectInstance) + } + return nil +} + +// HandleReadyState checks for the consistency of reconciled resource, by verifying the underlying resources. +func (r *CFAPIReconciler) HandleReadyState(ctx context.Context, objectInstance *v1alpha1.CFAPI) error { + status := getStatusFromSample(objectInstance) + if _, err := r.processResources(ctx, objectInstance); err != nil { + // stay in Ready/Warning state if FinalDeletionState is set to Ready/Warning + if !objectInstance.GetDeletionTimestamp().IsZero() && + (r.FinalDeletionState == v1alpha1.StateReady || r.FinalDeletionState == v1alpha1.StateWarning) { + return nil + } + + r.EventRecorder.Event(objectInstance, "Warning", "ResourcesInstall", err.Error()) + return r.setStatusForObjectInstance(ctx, objectInstance, status. + WithState(v1alpha1.StateError). + WithInstallConditionStatus(metav1.ConditionFalse, objectInstance.GetGeneration())) + } + return nil +} + +func (r *CFAPIReconciler) setStatusForObjectInstance(ctx context.Context, objectInstance *v1alpha1.CFAPI, + status *v1alpha1.CFAPIStatus, +) error { + objectInstance.Status = *status + + if err := r.ssaStatus(ctx, objectInstance); err != nil { + r.EventRecorder.Event(objectInstance, "Warning", "ErrorUpdatingStatus", + fmt.Sprintf("updating state to %v", string(status.State))) + return fmt.Errorf("error while updating status %s to: %w", status.State, err) + } + + r.EventRecorder.Event(objectInstance, "Normal", "StatusUpdated", fmt.Sprintf("updating state to %v", string(status.State))) + return nil +} + +func (r *CFAPIReconciler) processResources(ctx context.Context, cfAPI *v1alpha1.CFAPI) (string, error) { + logger := log.FromContext(ctx) + + r.EventRecorder.Event(cfAPI, "Normal", "ResourcesInstall", "installing resources") + + // get wildcard domain + wildCardDomain, err := r.getWildcardDomain() + if err != nil { + logger.Error(err, "error getting wildcard domain") + return "", err + } + + cfDomain := wildCardDomain[2:] + appsDomain := "apps." + cfDomain + korifiApiDomain := "cfapi." + cfDomain + twuniDomain := "cr." + cfDomain + + logger.Info("wildcard domain retrieved : " + wildCardDomain) + logger.Info("cf domain calculated : " + cfDomain) + logger.Info("apps domain calculated : " + appsDomain) + logger.Info("korifi api domain calculated : " + korifiApiDomain) + + containerRegistry, err := r.getAppContainerRegistry(ctx, cfAPI, twuniDomain) + if err != nil { + logger.Error(err, "error getting app container registry") + return "", err + } + + // create oidc config CR if supported + logger.Info("Starting OIDC CR creation ...") + err = r.createOIDCConfig(ctx, cfAPI) + if err != nil { + logger.Error(err, "error creating OIDC CR") + return "", err + } + logger.Info("OIDC CR creation completed") + + // install gateway api + logger.Info("Installing gateway API") + err = r.installGatewayAPI(ctx) + if err != nil { + logger.Error(err, "error installing gateway api") + return "", err + } + logger.Info("Gateway API installed") + + // install cert manager + err = r.installOneGlob(ctx, "./module-data/cert-manager/cert-manager.yaml") + if err != nil { + logger.Error(err, "error installing cert manager") + return "", err + } + + // create needed namespaces + logger.Info("Start creating namespaces ...") + err = r.createNamespaces(ctx, containerRegistry) + if err != nil { + logger.Error(err, "error creating namespaces") + return "", err + } + logger.Info("namespaces created") + + // install twuni + logger.Info("Start installing twuni ...") + err = r.installTwuni(ctx, cfAPI, cfDomain, twuniDomain) + if err != nil { + logger.Error(err, "error installing twuni") + return "", err + } + logger.Info("twuni installed") + + // generate ingress certificates + logger.Info("Start generating ingress certificates ...") + err = r.generateIngressCertificates(ctx, cfDomain, appsDomain, korifiApiDomain) + if err != nil { + logger.Error(err, "problem generating ingress certificates") + return "", err + } + logger.Info("ingress certificates generated") + + err = r.installOneGlob(ctx, "./module-data/kpack/release-*.yaml") + if err != nil { + logger.Error(err, "error installing kpack") + return "", err + } + + err = r.installOneGlob(ctx, "./module-data/servicebinding/servicebinding-runtime-v*.yaml") + if err != nil { + logger.Error(err, "error installing servicebindig runtime") + return "", err + } + err = r.installOneGlob(ctx, "./module-data/servicebinding/servicebinding-workloadresourcemappings-v*.yaml") + if err != nil { + logger.Error(err, "error installing servicebindig workloadresourcemappings") + return "", err + } + + // create buildkit secret + logger.Info("Start creating buildkit secret ...") + err = r.createBuildkitSecret(ctx, containerRegistry) + if err != nil { + logger.Error(err, "error creating buildkit secret") + return "", err + } + logger.Info("buildkit secret created") + + logger.Info("Sync cfapi-system-secret to NS korifi") + err = r.syncSecret(ctx, + types.NamespacedName{Namespace: "cfapi-system", Name: "cfapi-system-registry"}, + types.NamespacedName{Namespace: "korifi", Name: "cfapi-system-registry"}) + + if err != nil { + logger.Error(err, "error sync secret cfapi-system-registry") + return "", err + } + // deploy korifi + logger.Info("Start deploying korifi ...") + var uaaUrl = cfAPI.Spec.UAA + if uaaUrl == "" { + uaaUrl = defaultUaaUrl + } + err = r.deployKorifi(ctx, appsDomain, korifiApiDomain, cfDomain, containerRegistry.Server, uaaUrl) + if err != nil { + logger.Error(err, "error during deployment of Korifi") + return "", err + } + logger.Info("korifi deployed") + + // create dns entries + logger.Info("Start creating dns entries ...") + err = r.createDNSEntries(ctx, korifiApiDomain, appsDomain) + if err != nil { + logger.Error(err, "error creating dns entries") + return "", err + } + logger.Info("dns entries created") + + // create twuni dns entries + logger.Info("Start creating twuni dns entries ...") + err = r.createTwuniDNSEntry(ctx, cfAPI, twuniDomain) + if err != nil { + logger.Error(err, "error creating twuni dns entries") + return "", err + } + logger.Info("twuni dns entries created") + + var subjects = toSubjectList(cfAPI.Spec.CFAdmins) + err = r.assignCfAdministrators(ctx, subjects, cfAPI.Spec.RootNamespace) + if err != nil { + logger.Error(err, "Failed to assing CF administrators") + return "", err + } + + logger.Info("CFAPI reconciled successfully") + + return "https://" + korifiApiDomain, nil +} + +func (r *CFAPIReconciler) createOIDCConfig(ctx context.Context, cfAPI *v1alpha1.CFAPI) error { + logger := log.FromContext(ctx) + + if r.crdExists(ctx, "OpenIDConnect") { + logger.Info("OIDC CR exists, create CR") + + vals := struct { + UAA string + }{ + UAA: cfAPI.Spec.UAA, + } + + t1 := template.New("oidcUAA") + + t2, err := t1.ParseFiles("./module-data/oidc/oidc-uaa-experimental.tmpl") + + if err != nil { + logger.Error(err, "error during parsing of oidc template") + return err + } + + buf := &bytes.Buffer{} + + err = t2.ExecuteTemplate(buf, "oidcUAA", vals) + + if err != nil { + logger.Error(err, "error during execution of oidc template") + return err + } + + s := buf.String() + + resourceObjs, err := parseManifestStringToObjects(s) + + if err != nil { + logger.Error(err, "error during parsing of twuni tls route") + return nil + } + + for _, obj := range resourceObjs.Items { + if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of twuni tls route") + return err + } + } + } else { + logger.Info("OIDC CR does not exist, skip creating CR") + } + + return nil +} + +func (r *CFAPIReconciler) getAppContainerRegistry(ctx context.Context, cfAPI *v1alpha1.CFAPI, + twuniDomain string) (ContainerRegistry, error) { + logger := log.FromContext(ctx) + + if cfAPI.Spec.AppImagePullSecret != "" { + logger.Info("App Container Img Reg Secret is set, using it") + // extract container registry from secret + secret := corev1.Secret{} + err := r.Client.Get(context.Background(), client.ObjectKey{ + Namespace: "korifi", + Name: cfAPI.Spec.AppImagePullSecret, + }, &secret) + + if err != nil { + logger.Error(err, "error getting app container registry secret") + return ContainerRegistry{}, err + } + + return ContainerRegistry{ + Server: string(secret.Data["server"]), + User: string(secret.Data["username"]), + Pass: string(secret.Data["password"]), + }, nil + } + + logger.Info("App Container Img Reg Secret is not set, using twuni") + return ContainerRegistry{ + Server: twuniDomain, + User: DefaultTwuniUser, + Pass: DefaultTwuniPass, + }, nil +} + +func (r *CFAPIReconciler) createDNSEntries(ctx context.Context, korifiAPI, appsDomain string) error { + logger := log.FromContext(ctx) + + // get ingress hostname + ingress := corev1.Service{} + err := r.Client.Get(context.Background(), client.ObjectKey{ + Namespace: "korifi-gateway", + Name: "korifi-istio", + }, &ingress) + + if err != nil { + logger.Error(err, "error getting ingress hostname") + return err + } + + hostname := ingress.Status.LoadBalancer.Ingress[0].Hostname + + // create dns entries + vals := struct { + KorifiAPI string + IngressHost string + AppsDomain string + }{ + KorifiAPI: korifiAPI, + IngressHost: hostname, + AppsDomain: appsDomain, + } + + t1 := template.New("dnsEntries") + t2, err := t1.ParseFiles("./module-data/dns-entries/dns-entries.tmpl") + if err != nil { + logger.Error(err, "error during parsing of dns entries template") + return err + } + + buf := &bytes.Buffer{} + + err = t2.ExecuteTemplate(buf, "dnsEntries", vals) + if err != nil { + logger.Error(err, "error during execution of dns entries template") + return err + } + + s := buf.String() + + resourceObjs, err := parseManifestStringToObjects(s) + + if err != nil { + logger.Error(err, "error during parsing of dns entries") + return nil + } + + for _, obj := range resourceObjs.Items { + if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of dns entries") + return err + } + } + + return nil +} + +func (r *CFAPIReconciler) createBuildkitSecret(ctx context.Context, appContainerRegistry ContainerRegistry) error { + logger := log.FromContext(ctx) + + secretExists := r.secretExists("cfapi-system", "buildkit") + + if secretExists { + logger.Info("buildkit secret already exists, patching it") + + err := r.patchDockerSecret(ctx, "buildkit", "cfapi-system", appContainerRegistry.Server, + appContainerRegistry.User, appContainerRegistry.Pass) + + if err != nil { + logger.Error(err, "error patching buildkit secret") + return err + } + } else { + err := r.createDockerSecret(ctx, "buildkit", "cfapi-system", appContainerRegistry.Server, + appContainerRegistry.User, appContainerRegistry.Pass) + + if err != nil { + logger.Error(err, "error creating buildkit secret") + return err + } + } + + return nil +} + +func (r *CFAPIReconciler) createDockerSecret(ctx context.Context, name, namespace, server, username, password string) error { + logger := log.FromContext(ctx) + + conf := DockerRegistryConfig{ + Auths: map[string]DockerRegistryAuth{}, + } + + conf.Auths[server] = DockerRegistryAuth{ + Username: username, + Password: password, + } + + secretData, err := json.Marshal(conf) + + if err != nil { + logger.Error(err, "error marshalling docker registry config") + return err + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: "kubernetes.io/dockerconfigjson", + StringData: map[string]string{".dockerconfigjson": string(secretData)}, + } + + err = r.Client.Create(context.Background(), &secret) + + if err != nil { + logger.Error(err, "error creating "+name+" secret in ns "+namespace) + return err + } + + return nil +} + +func (r *CFAPIReconciler) generateIngressCertificates(ctx context.Context, cfDomain, appsDomain, korifiApiDomain string) error { + logger := log.FromContext(ctx) + + vals := struct { + CFDomain string + AppsDomain string + KorifiAPIDomain string + }{ + CFDomain: cfDomain, + AppsDomain: appsDomain, + KorifiAPIDomain: korifiApiDomain, + } + + t1 := template.New("ingressCerts") + t2, err := t1.ParseFiles("./module-data/ingress-certificates/ingress-certificates.tmpl") + + if err != nil { + logger.Error(err, "error during parsing of ingress certificates template") + return err + } + + buf := &bytes.Buffer{} + + err = t2.ExecuteTemplate(buf, "ingressCerts", vals) + + if err != nil { + logger.Error(err, "error during execution of ingress certificates template") + return err + } + + s := buf.String() + + resourceObjs, err := parseManifestStringToObjects(s) + + if err != nil { + logger.Error(err, "error during parsing of ingress certificates") + return nil + } + + for _, obj := range resourceObjs.Items { + if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of cert manager resources") + return err + } + } + + // wait for respective secrets to be created + err = r.waitForSecret("korifi", "korifi-api-ingress-cert") + if err != nil { + logger.Error(err, "error waiting for secret korifi-api-ingress-cert") + return err + } + + err = r.waitForSecret("korifi", "korifi-workloads-ingress-cert") + if err != nil { + logger.Error(err, "error waiting for secret korifi-workloads-ingress-cert") + return err + } + + return nil +} + +func (r *CFAPIReconciler) waitForSecret(namespace, name string) error { + logger := log.FromContext(context.Background()) + + start := time.Now() + + for { + secretExists := r.secretExists(namespace, name) + + if secretExists { + logger.Info("secret " + name + " found") + break + } + + logger.Info("secret " + name + " not found, retrying...") + time.Sleep(1 * time.Minute) + + now := time.Now() + + diff := now.Sub(start) + + if diff.Minutes() > 15 { + return errors.New("timeout waiting for secret " + name) + } + } + + return nil +} + +func (r *CFAPIReconciler) getWildcardDomain() (string, error) { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "networking.istio.io", + Kind: "Gateway", + Version: "v1beta1", + }) + + err := r.Client.Get(context.Background(), client.ObjectKey{ + Namespace: "kyma-system", + Name: "kyma-gateway", + }, u) + + if err != nil { + return "", err + } + + uc := u.UnstructuredContent() + + servers, found, err := unstructured.NestedSlice(uc, "spec", "servers") + + if err != nil { + return "", err + } + + if !found { + return "", errors.New("wildcard domain field not found") + } + + s := servers[0].(map[string]interface{})["hosts"].([]interface{})[0].(string) + + return s, nil +} + +func (r *CFAPIReconciler) createNamespaces(ctx context.Context, appContainerRegistry ContainerRegistry) error { + logger := log.FromContext(ctx) + + err := r.installOneGlob(ctx, "./module-data/namespaces/namespaces.yaml") + if err != nil { + logger.Error(err, "error creating namespaces") + return err + } + + // create container image pull secrets in namespaces + namespaces := []string{"korifi", "cf"} + for _, ns := range namespaces { + + secretExists := r.secretExists(ns, "cfapi-app-registry") + + if secretExists { + logger.Info("image pull secret already exists in ns " + ns + ", patching it") + + err = r.patchDockerSecret(ctx, "cfapi-app-registry", ns, appContainerRegistry.Server, + appContainerRegistry.User, appContainerRegistry.Pass) + + if err != nil { + logger.Error(err, "error patching image pull secret") + return err + } + } else { + err = r.createDockerSecret(ctx, "cfapi-app-registry", ns, appContainerRegistry.Server, + appContainerRegistry.User, appContainerRegistry.Pass) + + if err != nil { + logger.Error(err, "error creating image pull secret") + return err + } + } + } + + return nil +} + +func (r *CFAPIReconciler) installGatewayAPI(ctx context.Context) error { + logger := log.FromContext(ctx) + + err := r.installOneGlob(ctx, "./module-data/gateway-api/experimental-install.yaml") + if err != nil { + logger.Error(err, "error installing gateway API") + return err + } + + deploy := &appsv1.Deployment{} + err = r.Client.Get(context.Background(), client.ObjectKey{ + Namespace: "istio-system", + Name: "istiod", + }, deploy) + + if err != nil { + logger.Error(err, "error getting istiod deployment") + return err + } + + envVarFound := false + + for _, env := range deploy.Spec.Template.Spec.Containers[0].Env { + if env.Name == "PILOT_ENABLE_ALPHA_GATEWAY_API" { + envVarFound = true + } + } + + if !envVarFound { + deploy.Spec.Template.Spec.Containers[0].Env = append(deploy.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: "PILOT_ENABLE_ALPHA_GATEWAY_API", + Value: "true", + }) + + if err = r.ssa(ctx, deploy); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during patching istiod deployment") + return err + } + } + + err = r.installOneGlob(ctx, "./module-data/envoy-filter/empty-envoy-filter.yaml") + if err != nil { + logger.Error(err, "error installing envoy filter") + return err + } + + return nil +} + +func getStatusFromSample(objectInstance *v1alpha1.CFAPI) v1alpha1.CFAPIStatus { + return objectInstance.Status +} + +// Korifi HELM chart deployment +func (r *CFAPIReconciler) deployKorifi(ctx context.Context, appsDomain, korifiAPIDomain, cfDomain, crDomain, uaaURL string) error { + logger := log.FromContext(ctx) + + helmfile, err := findOneGlob("./module-data/korifi/korifi-helm-*.tar.gz") + if err != nil { + logger.Error(err, "Failed to find korifi helm chart under dir module-data/korifi") + return err + } + chart, err := loader.Load(helmfile) + if err != nil { + logger.Error(err, "error loading korifi helm chart") + return err + } + + values, err := loadOneYaml("./module-data/korifi/values-*.yaml") + if err != nil { + logger.Error(err, "Failed to load korifi helm chart release values") + return err + } + + values_cfapi, err := loadOneYaml("./module-data/korifi/values.yaml") + if err != nil { + logger.Error(err, "Failed to load CFAPI values for korifi helm chart") + return err + } + + DeepUpdate(values, values_cfapi) + + values_dynamic := map[string]interface{}{ + "api": map[string]interface{}{ + "apiServer": map[string]interface{}{ + "url": korifiAPIDomain, + }, + "logcache": map[string]interface{}{ + "url": "logcache." + cfDomain, + }, + "uaaURL": uaaURL, + }, + "kpackImageBuilder": map[string]interface{}{ + "builderRepository": crDomain + "/trinity/kpack-builder", + }, + "containerRepositoryPrefix": crDomain + "/", + "defaultAppDomainName": appsDomain, + "cfDomain": cfDomain, + } + + DeepUpdate(values, values_dynamic) + + if releaseExists("korifi", "korifi") { + // update + logger.Info("korifi release found, upgrading it") + + err = updateRelease(chart, "korifi", "korifi", values, logger) + } else { + // install + logger.Info("korifi release not found, installing it") + + err = installRelease(chart, "korifi", "korifi", values, logger) + } + + return err +} diff --git a/controllers/cfapi_controller_rendered_resources_test.go b/controllers/cfapi_controller_rendered_resources_test.go new file mode 100644 index 0000000..f04dafe --- /dev/null +++ b/controllers/cfapi_controller_rendered_resources_test.go @@ -0,0 +1,162 @@ +package controllers_test + +import ( + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/client-go/kubernetes" + + "github.tools.sap/unified-runtime/cfapi-kyma-module/api/v1alpha1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + podNs = "redis" + podName = "busybox-pod" +) + +var _ = Describe("Sample CR is created with the correct resource path", Ordered, func() { + sampleCR := createSampleCR("valid-sample", "./test/busybox/manifest") + sampleCRKey := client.ObjectKeyFromObject(sampleCR) + + It("should create SampleCR and resources", func() { + Expect(k8sClient.Create(ctx, sampleCR)).To(Succeed()) + + Eventually(getCRStatus(sampleCRKey)). + WithTimeout(30 * time.Second). + WithPolling(500 * time.Millisecond). + Should(Equal(CRStatus{State: v1alpha1.StateReady, InstallConditionStatus: metav1.ConditionTrue, Err: nil})) + + Eventually(getPod(podNs, podName)). + WithTimeout(30 * time.Second). + WithPolling(500 * time.Millisecond). + Should(BeTrue()) + }) + + It("should set state to Warning when deleted after setting FinalDeletionState", func() { + reconciler.FinalDeletionState = v1alpha1.StateWarning + Expect(k8sClient.Delete(ctx, sampleCR)).To(Succeed()) + + Eventually(getCRStatus(sampleCRKey)). + WithTimeout(30 * time.Second). + WithPolling(500 * time.Millisecond). + Should(Equal(CRStatus{ + State: v1alpha1.StateWarning, + InstallConditionStatus: metav1.ConditionTrue, Err: nil, + })) + Consistently(getCRStatus(sampleCRKey)). + WithTimeout(5 * time.Second). + WithPolling(100 * time.Millisecond). + Should(Equal(CRStatus{ + State: v1alpha1.StateWarning, + InstallConditionStatus: metav1.ConditionTrue, Err: nil, + })) + }) + + It("should delete when FinalDeletionState set to Deleting", func() { + reconciler.FinalDeletionState = v1alpha1.StateDeleting + Eventually(checkDeleted(sampleCRKey)). + WithTimeout(30 * time.Second). + WithPolling(500 * time.Millisecond). + Should(BeTrue()) + }) +}) + +var _ = Describe("Sample CR is created with an incorrect resource path", Ordered, func() { + sampleCR := createSampleCR("invalid-sample", "./invalid/path") + sampleCRKey := client.ObjectKeyFromObject(sampleCR) + + It("should create SampleCR in Error state", func() { + Expect(k8sClient.Create(ctx, sampleCR)).To(Succeed()) + Eventually(getCRStatus(sampleCRKey)). + WithTimeout(30 * time.Second). + WithPolling(500 * time.Millisecond). + Should(Equal(CRStatus{State: v1alpha1.StateError, InstallConditionStatus: metav1.ConditionFalse, Err: nil})) + + Expect(k8sClient.Delete(ctx, sampleCR)).To(Succeed()) + }) +}) + +func createSampleCR(sampleName, path string) *v1alpha1.CFAPI { + return &v1alpha1.CFAPI{ + TypeMeta: metav1.TypeMeta{ + Kind: string(v1alpha1.CFAPIKind), + APIVersion: v1alpha1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: sampleName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.CFAPISpec{RootNamespace: "cf"}, + } +} + +func getPod(namespace, podName string) func(g Gomega) bool { + return func(g Gomega) bool { + clientSet, err := kubernetes.NewForConfig(reconciler.Config) + g.Expect(err).ToNot(HaveOccurred()) + + pod, err := clientSet.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return false + } + + // Because there are no controllers monitoring built-in resources, state of objects do not get updated. + // Thus, 'Ready'-State of pod needs to be set manually. + pod.Status.Conditions = append(pod.Status.Conditions, v1.PodCondition{ + Type: v1.PodReady, + Status: v1.ConditionTrue, + }) + + _, err = clientSet.CoreV1().Pods(namespace).UpdateStatus(ctx, pod, metav1.UpdateOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + return true + } +} + +type CRStatus struct { + State v1alpha1.State + InstallConditionStatus metav1.ConditionStatus + Err error +} + +func getCRStatus(sampleObjKey client.ObjectKey) func(g Gomega) CRStatus { + return func(g Gomega) CRStatus { + sampleCR := &v1alpha1.CFAPI{} + err := k8sClient.Get(ctx, sampleObjKey, sampleCR) + if err != nil { + return CRStatus{State: v1alpha1.StateError, Err: err} + } + g.Expect(err).NotTo(HaveOccurred()) + condition := meta.FindStatusCondition(sampleCR.Status.Conditions, v1alpha1.ConditionTypeInstallation) + g.Expect(condition).ShouldNot(BeNil()) + return CRStatus{ + State: sampleCR.Status.State, + InstallConditionStatus: condition.Status, + Err: nil, + } + } +} + +func checkDeleted(sampleObjKey client.ObjectKey) func(g Gomega) bool { + return func(g Gomega) bool { + clientSet, err := kubernetes.NewForConfig(reconciler.Config) + g.Expect(err).ToNot(HaveOccurred()) + + // check if Pod resource is deleted + _, err = clientSet.CoreV1().Pods(podNs).Get(ctx, podName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + sampleCR := v1alpha1.CFAPI{} + // check if reconciled resource is also deleted + err = k8sClient.Get(ctx, sampleObjKey, &sampleCR) + return errors.IsNotFound(err) + } + return false + } +} diff --git a/controllers/common_utils.go b/controllers/common_utils.go new file mode 100644 index 0000000..811e22d --- /dev/null +++ b/controllers/common_utils.go @@ -0,0 +1,181 @@ +package controllers + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-logr/logr" + "golang.org/x/time/rate" + errors2 "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/sets" + yamlUtil "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/ratelimiter" + "sigs.k8s.io/yaml" +) + +type RateLimiter struct { + Burst int + Frequency int + BaseDelay time.Duration + FailureMaxDelay time.Duration +} + +const ( + requeueInterval = time.Hour * 1 + finalizer = "sample.kyma-project.io/finalizer" + debugLogLevel = 2 + fieldOwner = "sample.kyma-project.io/owner" +) + +func (r *CFAPIReconciler) installOneGlob(ctx context.Context, pattern string) error { + logger := log.FromContext(ctx) + logger.Info("Installing", "glob", pattern) + resources, err := loadOneGlob(pattern, logger) + + if err != nil { + return err + } + + for _, obj := range resources.Items { + if err := r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of resources") + return err + } + } + return nil +} + +func loadOneGlob(pattern string, logger logr.Logger) (*ManifestResources, error) { + filename, err := findOneGlob(pattern) + if err != nil { + return nil, err + } + return loadFile(filename, logger) +} + +func loadFile(file string, logger logr.Logger) (*ManifestResources, error) { + fileBytes, err := os.ReadFile(file) + if err != nil { + return nil, err + } + return parseManifestStringToObjects(string(fileBytes)) +} + +func findOneGlob(pattern string) (string, error) { + matches, err := filepath.Glob(pattern) + if err != nil { + return "", err + } + if len(matches) > 1 { + return "", fmt.Errorf("Ambiguous file glob %s, found more than one file", pattern) + } + if len(matches) == 0 { + return "", fmt.Errorf("No file glob found %s", pattern) + } + return matches[0], nil +} + +func loadOneYaml(pattern string) (map[string]any, error) { + file, err := findOneGlob(pattern) + if err != nil { + return nil, err + } + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + content := make(map[string]any, 50) + err = yaml.Unmarshal(data, &content) + return content, err +} + +// parseManifestStringToObjects parses the string of resources into a list of unstructured resources. +func parseManifestStringToObjects(manifest string) (*ManifestResources, error) { + objects := &ManifestResources{} + reader := yamlUtil.NewYAMLReader(bufio.NewReader(strings.NewReader(manifest))) + for { + rawBytes, err := reader.Read() + if err != nil { + if errors.Is(err, io.EOF) { + return objects, nil + } + + return nil, fmt.Errorf("invalid YAML doc: %w", err) + } + + rawBytes = bytes.TrimSpace(rawBytes) + unstructuredObj := unstructured.Unstructured{} + if err := yaml.Unmarshal(rawBytes, &unstructuredObj); err != nil { + objects.Blobs = append(objects.Blobs, append(bytes.TrimPrefix(rawBytes, []byte("---\n")), '\n')) + } + + if len(rawBytes) == 0 || bytes.Equal(rawBytes, []byte("null")) || len(unstructuredObj.Object) == 0 { + continue + } + + objects.Items = append(objects.Items, &unstructuredObj) + } +} + +// TemplateRateLimiter implements a rate limiter for a client-go.workqueue. It has +// both an overall (token bucket) and per-item (exponential) rate limiting. +func TemplateRateLimiter(failureBaseDelay time.Duration, failureMaxDelay time.Duration, + frequency int, burst int, +) ratelimiter.RateLimiter { + return workqueue.NewMaxOfRateLimiter( + workqueue.NewItemExponentialFailureRateLimiter(failureBaseDelay, failureMaxDelay), + &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(frequency), burst)}) +} + +// getResourcesFromLocalPath returns resources from the dirPath in unstructured format. +// Only one file in .yaml or .yml format should be present in the target directory. +func getResourcesFromLocalPath(dirPath string, logger logr.Logger) (*ManifestResources, error) { + dirEntries := make([]fs.DirEntry, 0) + err := filepath.WalkDir(dirPath, func(path string, info fs.DirEntry, err error) error { + // initial error + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + dirEntries, err = os.ReadDir(dirPath) + return err + }) + if err != nil { + return nil, err + } + + childCount := len(dirEntries) + if childCount == 0 { + logger.V(debugLogLevel).Info(fmt.Sprintf("no yaml file found at file path %s", dirPath)) + return nil, nil + } else if childCount > 1 { + logger.V(debugLogLevel).Info(fmt.Sprintf("more than one yaml file found at file path %s", dirPath)) + return nil, nil + } + file := dirEntries[0] + allowedExtns := sets.NewString(".yaml", ".yml") + if !allowedExtns.Has(filepath.Ext(file.Name())) { + return nil, nil + } + + fileBytes, err := os.ReadFile(filepath.Join(dirPath, file.Name())) + if err != nil { + return nil, fmt.Errorf("yaml file could not be read %s in dir %s: %w", file.Name(), dirPath, err) + } + return parseManifestStringToObjects(string(fileBytes)) +} diff --git a/controllers/helm_utils.go b/controllers/helm_utils.go new file mode 100644 index 0000000..91cbb6e --- /dev/null +++ b/controllers/helm_utils.go @@ -0,0 +1,116 @@ +package controllers + +import ( + golog "log" + "reflect" + "time" + + "github.com/go-logr/logr" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" +) + +func releaseExists(namespace, name string) bool { + settings := cli.New() + actionConfig := new(action.Configuration) + err := actionConfig.Init(settings.RESTClientGetter(), namespace, + "secret", golog.Printf) + + if err != nil { + return false + } + + histClient := action.NewHistory(actionConfig) + histClient.Max = 1 + versions, err := histClient.Run(name) + + return !(err == driver.ErrReleaseNotFound || isReleaseUninstalled(versions)) +} + +func installRelease(chart *chart.Chart, namespace, name string, values map[string]interface{}, logger logr.Logger) error { + + settings := cli.New() + actionConfig := new(action.Configuration) + err := actionConfig.Init(settings.RESTClientGetter(), namespace, + "secret", golog.Printf) + + if err != nil { + logger.Error(err, "error during init of helm action config") + return err + } + + installClient := action.NewInstall(actionConfig) + + installClient.ReleaseName = name + installClient.Namespace = namespace + installClient.Timeout = 5 * time.Minute + installClient.Wait = true + + _, err = installClient.Run(chart, values) + + if err != nil { + logger.Error(err, "error during install of korifi helm chart") + return err + } + + return nil +} + +func updateRelease(chart *chart.Chart, namespace, name string, values map[string]interface{}, logger logr.Logger) error { + + settings := cli.New() + actionConfig := new(action.Configuration) + err := actionConfig.Init(settings.RESTClientGetter(), namespace, + "secret", golog.Printf) + + if err != nil { + logger.Error(err, "error during init of helm action config") + return err + } + + upgradeClient := action.NewUpgrade(actionConfig) + + upgradeClient.Namespace = namespace + + upgradeClient.Install = true + upgradeClient.Wait = true + upgradeClient.Timeout = 5 * time.Minute + + _, err = upgradeClient.Run(name, chart, values) + + if err != nil { + logger.Error(err, "error during deployment of korifi helm chart") + return err + } + + return nil +} + +func isReleaseUninstalled(versions []*release.Release) bool { + return len(versions) > 0 && versions[len(versions)-1].Info.Status == release.StatusUninstalled +} + +/* +This will update map m1 with the values of map m2 doing deep update. +The purpose is to prepare HELM values from different YML sources +*/ +func DeepUpdate(m1, m2 map[string]any) { + for k, vn := range m2 { + vo, found := m1[k] + updated := false + if found && (vo != nil) { + ko := reflect.TypeOf(vo).Kind() + kn := reflect.TypeOf(vn).Kind() + if ko == reflect.Map && kn == reflect.Map { + DeepUpdate(vo.(map[string]any), vn.(map[string]any)) + updated = true + } + } + if !updated { + m1[k] = vn + } + } +} diff --git a/controllers/install_twuni.go b/controllers/install_twuni.go new file mode 100644 index 0000000..3d76092 --- /dev/null +++ b/controllers/install_twuni.go @@ -0,0 +1,278 @@ +package controllers + +import ( + "bytes" + "context" + "text/template" + + "github.tools.sap/unified-runtime/cfapi-kyma-module/api/v1alpha1" + "helm.sh/helm/v3/pkg/chart/loader" + corev1 "k8s.io/api/core/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func (r *CFAPIReconciler) installTwuni(ctx context.Context, cfAPI *v1alpha1.CFAPI, cfDomain, twuniDomain string) error { + logger := log.FromContext(ctx) + + if cfAPI.Spec.AppImagePullSecret != "" { + logger.Info("App Container Img Reg Secret is set, skipping twuni installation") + return nil + } + + // create twuni certificate + logger.Info("Start installing twuni ...") + err := r.createTwuniCertificate(ctx, cfDomain, twuniDomain) + if err != nil { + logger.Error(err, "error creating twuni certificate") + return err + } + + // deploy twuni helm + err = r.deployTwuniHelm(ctx) + if err != nil { + logger.Error(err, "error deploying twuni helm") + return err + } + + // create reference grant + err = r.createTwuniReferenceGrant(ctx) + if err != nil { + logger.Error(err, "error creating twuni reference grant") + return err + } + + // create tlsroute + err = r.createTwuniTLSRoute(ctx, twuniDomain) + if err != nil { + logger.Error(err, "error creating twuni tls route") + return err + } + + logger.Info("Finished installing twuni ...") + + return nil +} + +func (r *CFAPIReconciler) createTwuniTLSRoute(ctx context.Context, twuniDomain string) error { + logger := log.FromContext(ctx) + + vals := struct { + TwuniDomain string + }{ + TwuniDomain: twuniDomain, + } + + t1 := template.New("twuniTLSRoute") + + t2, err := t1.ParseFiles("./module-data/twuni-tlsroute/tlsroute.tmpl") + + if err != nil { + logger.Error(err, "error during parsing of twuni tls route template") + return err + } + + buf := &bytes.Buffer{} + + err = t2.ExecuteTemplate(buf, "twuniTLSRoute", vals) + + if err != nil { + logger.Error(err, "error during execution of twuni tls route template") + return err + } + + s := buf.String() + + resourceObjs, err := parseManifestStringToObjects(s) + + if err != nil { + logger.Error(err, "error during parsing of twuni tls route") + return nil + } + + for _, obj := range resourceObjs.Items { + if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of twuni tls route") + return err + } + } + + return nil +} + +func (r *CFAPIReconciler) createTwuniReferenceGrant(ctx context.Context) error { + logger := log.FromContext(ctx) + + err := r.installOneGlob(ctx, "./module-data/twuni-referencegrant/referencegrant.yaml") + if err != nil { + logger.Error(err, "error installing twuni reference grant resources") + return err + } + + return nil +} + +func (r *CFAPIReconciler) createTwuniDNSEntry(ctx context.Context, cfAPI *v1alpha1.CFAPI, twuniDomain string) error { + logger := log.FromContext(ctx) + + if cfAPI.Spec.AppImagePullSecret != "" { + logger.Info("App Container Img Reg Secret is set, skipping twuni installation") + return nil + } + + // get ingress hostname + ingress := corev1.Service{} + err := r.Client.Get(context.Background(), client.ObjectKey{ + Namespace: "korifi-gateway", + Name: "korifi-istio", + }, &ingress) + + if err != nil { + logger.Error(err, "error getting ingress hostname") + return err + } + + hostname := ingress.Status.LoadBalancer.Ingress[0].Hostname + + // create dns entries + vals := struct { + TwuniDomain string + IngressHost string + }{ + TwuniDomain: twuniDomain, + IngressHost: hostname, + } + + t1 := template.New("twuniDNSEntry") + + t2, err := t1.ParseFiles("./module-data/twuni-dns-entry/dnsentry.tmpl") + if err != nil { + logger.Error(err, "error during parsing of twuni dns entries template") + return err + } + + buf := &bytes.Buffer{} + + err = t2.ExecuteTemplate(buf, "twuniDNSEntry", vals) + if err != nil { + logger.Error(err, "error during execution of twuni dns entries template") + return err + } + + s := buf.String() + + resourceObjs, err := parseManifestStringToObjects(s) + + if err != nil { + logger.Error(err, "error during parsing of twuni dns entries") + return nil + } + + for _, obj := range resourceObjs.Items { + if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of twuni dns entries") + return err + } + } + + return nil +} + +func (r *CFAPIReconciler) deployTwuniHelm(ctx context.Context) error { + logger := log.FromContext(ctx) + + filename, err := findOneGlob("./module-data/twuni-helm/*.tar.gz") + if err != nil { + return err + } + + chart, err := loader.Load(filename) + if err != nil { + logger.Error(err, "error during loading twuni helm chart") + return err + } + + inputValues := map[string]interface{}{ + "persistence": map[string]interface{}{ + "enabled": true, + "deleteEnabled": true, + }, + "service": map[string]interface{}{ + "port": 30050, + }, + "secrets": map[string]interface{}{ + "htpasswd": "user:$2y$05$MKUm/h9dwwWoCOht5enn3uyih.awSPDILY.kovYyT3J8KSw5lmwIe", + }, + "tlsSecretName": "docker-registry-ingress-cert", + } + + if releaseExists("cfapi-system", "localregistry") { + // update + logger.Info("twuni release found, upgrading it") + + err = updateRelease(chart, "cfapi-system", "localregistry", inputValues, logger) + } else { + // install + logger.Info("twuni release not found, installing it") + + err = installRelease(chart, "cfapi-system", "localregistry", inputValues, logger) + } + + return err +} + +func (r *CFAPIReconciler) createTwuniCertificate(ctx context.Context, cfDomain, twuniDomain string) error { + logger := log.FromContext(ctx) + + vals := struct { + CFDomain string + TwuniDomain string + }{ + CFDomain: cfDomain, + TwuniDomain: twuniDomain, + } + + t1 := template.New("twuniCert") + + t2, err := t1.ParseFiles("./module-data/twuni-certificate/certificate.tmpl") + + if err != nil { + logger.Error(err, "error during parsing of twuni certificate template") + return err + } + + buf := &bytes.Buffer{} + + err = t2.ExecuteTemplate(buf, "twuniCert", vals) + + if err != nil { + logger.Error(err, "error during execution of twuni certificate template") + return err + } + + s := buf.String() + + resourceObjs, err := parseManifestStringToObjects(s) + + if err != nil { + logger.Error(err, "error during parsing of ingress certificates") + return nil + } + + for _, obj := range resourceObjs.Items { + if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) { + logger.Error(err, "error during installation of cert manager resources") + return err + } + } + + // wait for respective secrets to be created + err = r.waitForSecret("cfapi-system", "docker-registry-ingress-cert") + if err != nil { + logger.Error(err, "error waiting for secret docker-registry-ingress-cert") + return err + } + + return nil +} diff --git a/controllers/k8s_utils.go b/controllers/k8s_utils.go new file mode 100644 index 0000000..20ded1d --- /dev/null +++ b/controllers/k8s_utils.go @@ -0,0 +1,131 @@ +package controllers + +import ( + "context" + "encoding/json" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func (r *CFAPIReconciler) crdExists(ctx context.Context, kind string) bool { + logger := log.FromContext(ctx) + + _ = v1.AddToScheme(r.Scheme) + + crds := &v1.CustomResourceDefinitionList{} + err := r.Client.List(ctx, crds) + + if err != nil { + logger.Error(err, "error listing CRDs") + return false + } + + for _, i := range crds.Items { + if i.Spec.Names.Kind == kind { + return true + } + } + + return false +} + +func (r *CFAPIReconciler) secretExists(namespace, name string) bool { + secret := corev1.Secret{} + + err := r.Client.Get(context.Background(), client.ObjectKey{ + Namespace: namespace, + Name: name, + }, &secret) + + return err == nil +} + +func (r *CFAPIReconciler) patchDockerSecret(ctx context.Context, name, namespace, server, username, password string) error { + logger := log.FromContext(ctx) + + conf := DockerRegistryConfig{ + Auths: map[string]DockerRegistryAuth{}, + } + + conf.Auths[server] = DockerRegistryAuth{ + Username: username, + Password: password, + } + + secretData, err := json.Marshal(conf) + + if err != nil { + logger.Error(err, "error marshalling docker registry config") + return err + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: "kubernetes.io/dockerconfigjson", + StringData: map[string]string{".dockerconfigjson": string(secretData)}, + } + + err = r.Client.Patch(context.Background(), &secret, client.MergeFrom(&corev1.Secret{})) + + if err != nil { + logger.Error(err, "error patching "+name+" secret in ns "+namespace) + return err + } + + return nil +} + +// ssaStatus patches status using SSA on the passed object. +func (r *CFAPIReconciler) ssaStatus(ctx context.Context, obj client.Object) error { + obj.SetManagedFields(nil) + obj.SetResourceVersion("") + return r.Client.Status().Patch(ctx, obj, client.Apply, + &client.SubResourcePatchOptions{PatchOptions: client.PatchOptions{FieldManager: fieldOwner}}) +} + +// ssa patches the object using SSA. +func (r *CFAPIReconciler) ssa(ctx context.Context, obj client.Object) error { + obj.SetManagedFields(nil) + obj.SetResourceVersion("") + + return r.Client.Patch(ctx, obj, client.Apply, client.ForceOwnership, client.FieldOwner(fieldOwner)) +} + +func (r *CFAPIReconciler) syncSecret(ctx context.Context, source, target types.NamespacedName) error { + var sourceSecret = &corev1.Secret{} + var err = r.Client.Get(ctx, source, sourceSecret) + if err != nil { + return err + + } + targetSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: target.Name, + Namespace: target.Namespace, + }, + Type: "kubernetes.io/dockerconfigjson", + Data: sourceSecret.Data, + } + if r.secretExists(target.Namespace, target.Name) { + return r.Client.Patch(ctx, &targetSecret, client.MergeFrom(&corev1.Secret{})) + } else { + return r.Client.Create(context.Background(), &targetSecret) + } +} + +func (r *CFAPIReconciler) createIfMissing(ctx context.Context, object client.Object) error { + err := r.Client.Create(ctx, object) + if !errors.IsAlreadyExists(err) { + return err + } + return nil +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go new file mode 100644 index 0000000..049b279 --- /dev/null +++ b/controllers/suite_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2022. + +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" + "path/filepath" + "testing" + "time" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.tools.sap/unified-runtime/cfapi-kyma-module/controllers" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + "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" + + operatorkymaprojectiov1alpha1 "github.tools.sap/unified-runtime/cfapi-kyma-module/api/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + k8sClient client.Client //nolint:gochecknoglobals + k8sManager manager.Manager //nolint:gochecknoglobals + testEnv *envtest.Environment //nolint:gochecknoglobals + ctx context.Context //nolint:gochecknoglobals + cancel context.CancelFunc //nolint:gochecknoglobals + reconciler *controllers.CFAPIReconciler //nolint:gochecknoglobals +) + +const ( + testChartPath = "./test/busybox" + rateLimiterBurstDefault = 200 + rateLimiterFrequencyDefault = 30 + failureBaseDelayDefault = 1 * time.Second + failureMaxDelayDefault = 1000 * time.Second +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + ctx, cancel = context.WithCancel(context.Background()) + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + rateLimiter := controllers.RateLimiter{ + Burst: rateLimiterBurstDefault, + Frequency: rateLimiterFrequencyDefault, + BaseDelay: failureBaseDelayDefault, + FailureMaxDelay: failureMaxDelayDefault, + } + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = controllers.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = controllers.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()) + + k8sManager, err = ctrl.NewManager( + cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + reconciler = &controllers.CFAPIReconciler{ + Client: k8sManager.GetClient(), + Scheme: scheme.Scheme, + EventRecorder: k8sManager.GetEventRecorderFor("tests"), + FinalState: operatorkymaprojectiov1alpha1.StateReady, + FinalDeletionState: operatorkymaprojectiov1alpha1.StateDeleting, + } + + err = reconciler.SetupWithManager(k8sManager, rateLimiter) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + By("canceling the context for the manager to shutdown") + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/controllers/test/busybox/.helmignore b/controllers/test/busybox/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/controllers/test/busybox/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/controllers/test/busybox/Chart.yaml b/controllers/test/busybox/Chart.yaml new file mode 100644 index 0000000..b5b459e --- /dev/null +++ b/controllers/test/busybox/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +description: A utility chart to verify clusters and processes +home: https://www.busybox.net/ +icon: https://www.busybox.net/images/busybox1.png +keywords: +- busybox +- testing +- sanity check +- network +- connectivity +name: busybox-helm +version: 0.1.0 diff --git a/controllers/test/busybox/manifest/resources.yaml b/controllers/test/busybox/manifest/resources.yaml new file mode 100644 index 0000000..2b641e3 --- /dev/null +++ b/controllers/test/busybox/manifest/resources.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: redis +--- +# Source: busybox/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: busybox-pod + namespace: redis + labels: + chart: "busybox-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: busybox + selector: + control-plane: busybox-deployment +--- +# Source: busybox/templates/deployment.yaml +apiVersion: v1 +kind: Pod +metadata: + name: busybox-pod + namespace: redis + labels: + chart: "busybox-0.1.0" +spec: + containers: + - name: busybox + image: "busybox:latest" + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + command: ["tail", "-f", "/dev/null"] diff --git a/controllers/test/busybox/templates/deployment.yaml b/controllers/test/busybox/templates/deployment.yaml new file mode 100644 index 0000000..7c8ab68 --- /dev/null +++ b/controllers/test/busybox/templates/deployment.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ .Chart.Name }}-pod + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.internalPort }} + command: ["tail", "-f", "/dev/null"] \ No newline at end of file diff --git a/controllers/test/busybox/templates/service.yaml b/controllers/test/busybox/templates/service.yaml new file mode 100644 index 0000000..202cc91 --- /dev/null +++ b/controllers/test/busybox/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }}-pod + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + control-plane: busybox-deployment diff --git a/controllers/test/busybox/values.yaml b/controllers/test/busybox/values.yaml new file mode 100644 index 0000000..fd88e57 --- /dev/null +++ b/controllers/test/busybox/values.yaml @@ -0,0 +1,21 @@ +# Default values for busybox. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: busybox + tag: latest + pullPolicy: IfNotPresent +service: + name: busybox + type: ClusterIP + externalPort: 80 + internalPort: 80 +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + diff --git a/default-cr.yaml b/default-cr.yaml new file mode 100644 index 0000000..cec0de2 --- /dev/null +++ b/default-cr.yaml @@ -0,0 +1,8 @@ +apiVersion: operator.kyma-project.io/v1alpha1 +kind: CFAPI +metadata: + name: default-cf-api + namespace: cfapi-system +spec: + rootNamespace: cf + appImagePullSecret: "" diff --git a/docs/user/Setup-Prod.md b/docs/user/Setup-Prod.md new file mode 100644 index 0000000..a3ffb59 --- /dev/null +++ b/docs/user/Setup-Prod.md @@ -0,0 +1,89 @@ +## How to enable CF API on a productive BTP landscape + + +1. ### Kyma environment ### + + You need a Kyma environment which is configured with UAA as an OIDC provider, with following parameters +``` yaml +{ + "administrators": [ + "sap.ids:myfirstname.mysecondname@sap.com" + ], + "autoScalerMax": 3, + "autoScalerMin": 3, + "oidc": { + "clientID": "cf", + "groupsClaim": "", + "issuerURL": "https://uaa.cf.eu10.hana.ondemand.com/oauth/token", + "signingAlgs": [ + "RS256" + ], + "usernameClaim": "user_name", + "usernamePrefix": "sap.ids:" + } +} +``` + +2. ### Kyma Access ### + + Use that script to generate a stable kubeconfig:
+ https://github.tools.sap/unified-runtime/trinity/blob/main/scripts/tools/generate-kyma-kubeconfig.sh + + Note: that step requires an UAA client (uaac), which requires Ruby runtime + +3. ### Registry secret ### + + Create a system docker registry with name **cfapi-system-registry**, namespace cfapi-system. That registry contains the system images for the kyma module. Currently that is manual step, that the cfapi-kyma-module dev team has to provide credentials to SAP artifactory + The easiest way is to execute: +``` bash +export DOCKER_REGISTRY=trinity.common.repositories.cloud.sap +export DOCKER_REGISTRY_USER=korifi-dev +export DOCKER_REGISTRY_PASS=******************* + +kubectl create namespace cfapi-system --dry-run=client -o yaml | kubectl apply -f - +kubectl create -n cfapi-system secret docker-registry cfapi-system-registry --docker-server=${DOCKER_REGISTRY} --docker-username=${DOCKER_REGISTRY_USER} --docker-password=${DOCKER_REGISTRY_PASS} +``` + +4. ### Istio installed ### + + If Istio Kyma module is not installed, you can do it with: + +*make* from this repository +``` +make install-istio +``` +Or directly with kubectl +``` + kubectl label namespace cfapi-system istio-injection=enabled --overwrite + kubectl apply -f https://github.com/kyma-project/istio/releases/latest/download/istio-manager.yaml + kubectl apply -f https://github.com/kyma-project/istio/releases/latest/download/istio-default-cr.yaml +``` + +5. ### Deploy CF API ### + + Deploy the resources from a particular release version to kyma +``` +kubectl apply -f cfapi-crd.yaml +kubectl apply -f cfapi-manager.yaml +kubectl apply -f cfapi-default-cr.yaml +``` + + Wait for a Ready state of the CFAPI resource and read the CF URL +``` +kubectl get -n cfapi-system cfapi +NAME STATE URL +default-cf-api Ready https://cfapi.cc6e362.kyma.ondemand.com +``` + +7. ### Configure CF cli ### + + Set cf cli to point to CF API +``` +cf api https://cfapi.cc6e362.kyma.ondemand.com +``` + +8. ### CF Login ### + +``` +cf login --sso +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..520d8f2 --- /dev/null +++ b/go.mod @@ -0,0 +1,163 @@ +module github.tools.sap/unified-runtime/cfapi-kyma-module + +go 1.22.1 + +replace github.tools.sap/unified-runtime/cfapi-kyma-module/api => ./api + +require ( + github.com/go-logr/logr v1.4.1 + github.com/onsi/ginkgo/v2 v2.17.1 + github.com/onsi/gomega v1.32.0 + github.tools.sap/unified-runtime/cfapi-kyma-module/api v0.0.0-00010101000000-000000000000 + golang.org/x/time v0.5.0 + helm.sh/helm/v3 v3.14.3 + k8s.io/api v0.29.3 + k8s.io/apimachinery v0.29.3 + k8s.io/client-go v0.29.3 + sigs.k8s.io/controller-runtime v0.17.2 + sigs.k8s.io/yaml v1.4.0 +) + +require github.com/Masterminds/semver/v3 v3.2.1 // indirect + +require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/containerd v1.7.12 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v24.0.6+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.7+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/glob v0.2.3 // 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.4 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc5 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rubenv/sql-migrate v1.5.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.58.3 // indirect + google.golang.org/protobuf v1.33.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.29.0 // indirect + k8s.io/apiserver v0.29.0 // indirect + k8s.io/cli-runtime v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/kubectl v0.29.0 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + oras.land/oras-go v1.2.4 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect + sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cbd68dc --- /dev/null +++ b/go.sum @@ -0,0 +1,601 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +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/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +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/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= +github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= +github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +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/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= +github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= +github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/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 v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= +github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/errors v0.8.0/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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= +github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +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/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/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.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +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/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +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/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +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.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.3.0/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +helm.sh/helm/v3 v3.14.3 h1:HmvRJlwyyt9HjgmAuxHbHv3PhMz9ir/XNWHyXfmnOP4= +helm.sh/helm/v3 v3.14.3/go.mod h1:v6myVbyseSBJTzhmeE39UcPLNv6cQK6qss3dvgAySaE= +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.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= +k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= +k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= +k8s.io/apiserver v0.29.0 h1:Y1xEMjJkP+BIi0GSEv1BBrf1jLU9UPfAnnGGbbDdp7o= +k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM= +k8s.io/cli-runtime v0.29.0 h1:q2kC3cex4rOBLfPOnMSzV2BIrrQlx97gxHJs21KxKS4= +k8s.io/cli-runtime v0.29.0/go.mod h1:VKudXp3X7wR45L+nER85YUzOQIru28HQpXr0mTdeCrk= +k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= +k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= +k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= +k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= +k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY= +oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +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/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= +sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= +sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..29c55ec --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..9dd82f3 --- /dev/null +++ b/main.go @@ -0,0 +1,169 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "os" + "time" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.tools.sap/unified-runtime/cfapi-kyma-module/api/v1alpha1" + "github.tools.sap/unified-runtime/cfapi-kyma-module/controllers" + //+kubebuilder:scaffold:imports +) + +const ( + rateLimiterBurstDefault = 200 + rateLimiterFrequencyDefault = 30 + failureBaseDelayDefault = 1 * time.Second + failureMaxDelayDefault = 1000 * time.Second + operatorName = "template-operator" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +type FlagVar struct { + metricsAddr string + enableLeaderElection bool + probeAddr string + failureBaseDelay time.Duration + failureMaxDelay time.Duration + rateLimiterFrequency int + rateLimiterBurst int + finalState string + finalDeletionState string + printVersion bool +} + +func init() { //nolint:gochecknoinits + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(controllers.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +//nolint:gochecknoglobals +var buildVersion = "not_provided" + +func main() { + flagVar := defineFlagVar() + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + if flagVar.printVersion { + msg := fmt.Sprintf("Template Operator version: %s\n", buildVersion) + _, err := os.Stdout.WriteString(msg) + if err != nil { + os.Exit(1) + } + os.Exit(0) + } + + rateLimiter := controllers.RateLimiter{ + Burst: flagVar.rateLimiterBurst, + Frequency: flagVar.rateLimiterFrequency, + BaseDelay: flagVar.failureBaseDelay, + FailureMaxDelay: flagVar.failureMaxDelay, + } + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: flagVar.metricsAddr, + }, + WebhookServer: webhook.NewServer(webhook.Options{ + Port: 9443}), + HealthProbeBindAddress: flagVar.probeAddr, + LeaderElection: flagVar.enableLeaderElection, + LeaderElectionID: "76223278.kyma-project.io", + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = (&controllers.CFAPIReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor(operatorName), + FinalState: v1alpha1.State(flagVar.finalState), + FinalDeletionState: v1alpha1.State(flagVar.finalDeletionState), + }).SetupWithManager(mgr, rateLimiter); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Sample") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +func defineFlagVar() *FlagVar { + flagVar := new(FlagVar) + flag.StringVar(&flagVar.metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&flagVar.probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&flagVar.enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.IntVar(&flagVar.rateLimiterBurst, "rate-limiter-burst", rateLimiterBurstDefault, + "Indicates the burst value for the bucket rate limiter.") + flag.IntVar(&flagVar.rateLimiterFrequency, "rate-limiter-frequency", rateLimiterFrequencyDefault, + "Indicates the bucket rate limiter frequency, signifying no. of events per second.") + flag.DurationVar(&flagVar.failureBaseDelay, "failure-base-delay", failureBaseDelayDefault, + "Indicates the failure base delay in seconds for rate limiter.") + flag.DurationVar(&flagVar.failureMaxDelay, "failure-max-delay", failureMaxDelayDefault, + "Indicates the failure max delay in seconds") + flag.StringVar(&flagVar.finalState, "final-state", string(v1alpha1.StateReady), + "Customize final state, to mimic state behaviour like Ready, Warning") + flag.StringVar(&flagVar.finalDeletionState, "final-deletion-state", string(v1alpha1.StateDeleting), + "Customize final state when module marked for deletion, to mimic state behaviour like Ready, Warning") + flag.BoolVar(&flagVar.printVersion, "version", false, "Prints the operator version and exits") + return flagVar +} diff --git a/module-data/README.md b/module-data/README.md new file mode 100644 index 0000000..f570a90 --- /dev/null +++ b/module-data/README.md @@ -0,0 +1,14 @@ +### Template-Operator Sample Data + +This directory contains sample data for the template-operator. +The directory is by default embedded in the template-operator docker image, so the operator can access the files placed here when deployed in the cluster. +You can use that to test how the operator works. + +The `yaml` subdirectory contains a YAML manifest (multi-document YAML file) that also installs a `redis` deployment. +You can install the manifest by creating a `Sample` CustomResource. See a working example in the `/config/samples/operator.kyma-project.io_v1alpha1_sample.yaml` file. + +If you want to install your own chart/manifest, you have two options: +1. Change the sample data in the current subdirectories and build your own custom docker image that you'll then use in deployment: `make docker-build`. Refer to the main `README.md` file for details. +2. Deploy the template-operator as it is and reconfigure it's deployment to mount additional files into the operator Pod. You can use Kubernetes volume mount feature for that. Then refer to the mounted folder in the `Sample` to trigger the installation. + +Note: When running the controller locally with `make run`, the controller has access to your local filesystem, so use local paths in the `Sample` configuration. diff --git a/module-data/dns-entries/dns-entries.tmpl b/module-data/dns-entries/dns-entries.tmpl new file mode 100644 index 0000000..25b2065 --- /dev/null +++ b/module-data/dns-entries/dns-entries.tmpl @@ -0,0 +1,30 @@ +{{ define "dnsEntries" }} +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + # Let Gardener manage this DNS entry. + dns.gardener.cloud/class: garden + name: cf-api-ingress + namespace: korifi +spec: + dnsName: {{ .KorifiAPI }} + ttl: 600 + targets: + - {{ .IngressHost }} +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + # Let Gardener manage this DNS entry. + dns.gardener.cloud/class: garden + name: cf-apps-ingress + namespace: korifi +spec: + dnsName: "*.{{ .AppsDomain }}" + ttl: 600 + targets: + - {{ .IngressHost }} +{{ end }} \ No newline at end of file diff --git a/module-data/envoy-filter/empty-envoy-filter.yaml b/module-data/envoy-filter/empty-envoy-filter.yaml new file mode 100644 index 0000000..60d4d89 --- /dev/null +++ b/module-data/envoy-filter/empty-envoy-filter.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: EnvoyFilter +metadata: + name: ef-removeserver + namespace: istio-system +spec: + configPatches: + - applyTo: NETWORK_FILTER + match: + listener: + filterChain: + filter: + name: "envoy.filters.network.http_connection_manager" + patch: + operation: MERGE + value: + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager" + server_name: " " diff --git a/module-data/ingress-certificates/ingress-certificates.tmpl b/module-data/ingress-certificates/ingress-certificates.tmpl new file mode 100644 index 0000000..f11f944 --- /dev/null +++ b/module-data/ingress-certificates/ingress-certificates.tmpl @@ -0,0 +1,33 @@ +{{ define "ingressCerts" }} +apiVersion: v1 +kind: Namespace +metadata: + name: korifi +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: korifi-api-ingress-cert + namespace: korifi +spec: + commonName: {{ .CFDomain }} + dnsNames: + - "{{ .KorifiAPIDomain }}" + - "*.{{ .AppsDomain }}" + secretRef: + name: korifi-api-ingress-cert + namespace: korifi +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: korifi-workloads-ingress-cert + namespace: korifi +spec: + commonName: {{ .CFDomain }} + dnsNames: + - "*.{{ .AppsDomain }}" + secretRef: + name: korifi-workloads-ingress-cert + namespace: korifi +{{ end }} \ No newline at end of file diff --git a/module-data/istio/istio-default-cr-experimental.yaml b/module-data/istio/istio-default-cr-experimental.yaml new file mode 100644 index 0000000..476b3c4 --- /dev/null +++ b/module-data/istio/istio-default-cr-experimental.yaml @@ -0,0 +1,11 @@ +apiVersion: operator.kyma-project.io/v1alpha2 +kind: Istio +metadata: + name: default + namespace: kyma-system + labels: + app.kubernetes.io/name: default +spec: + experimental: + pilot: + enableAlphaGatewayAPI: true \ No newline at end of file diff --git a/module-data/istio/istio-default-cr.yaml b/module-data/istio/istio-default-cr.yaml new file mode 100644 index 0000000..9e3607a --- /dev/null +++ b/module-data/istio/istio-default-cr.yaml @@ -0,0 +1,7 @@ +apiVersion: operator.kyma-project.io/v1alpha2 +kind: Istio +metadata: + name: default + namespace: kyma-system + labels: + app.kubernetes.io/name: default diff --git a/module-data/korifi/korifi-helm-0.11.0.tar.gz b/module-data/korifi/korifi-helm-0.11.0.tar.gz new file mode 100644 index 0000000..84b8a41 Binary files /dev/null and b/module-data/korifi/korifi-helm-0.11.0.tar.gz differ diff --git a/module-data/korifi/values-0.11.0.yaml b/module-data/korifi/values-0.11.0.yaml new file mode 100644 index 0000000..ef188c7 --- /dev/null +++ b/module-data/korifi/values-0.11.0.yaml @@ -0,0 +1,111 @@ +--- +adminUserName: null +api: + apiServer: + internalPort: 9000 + port: 0 + timeouts: + idle: 900 + read: 900 + readHeader: 10 + write: 900 + url: "" + authProxy: + caCert: "" + host: "" + image: trinity.common.repositories.cloud.sap/trinity/korifi-api-0.11.0@sha256:10c2b580b32155892ddbdf0b6de093a82fea60be2de4a7b0b3c9e7ac05d4523b + include: true + lifecycle: + stack: cflinuxfs3 + type: buildpack + logcache: + url: "" + replicas: 1 + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 50m + memory: 100Mi + uaaURL: "" + userCertificateExpirationWarningDuration: 168h +containerRegistryCACertSecret: null +containerRegistrySecrets: +- image-registry-credentials +controllers: + extraVCAPApplicationValues: {} + image: trinity.common.repositories.cloud.sap/trinity/korifi-controllers-0.11.0@sha256:e5d932ee0d447abc28a1fc9b220e2714d9c4e610f71d54f401fe1317de015b78 + maxRetainedBuildsPerApp: 5 + maxRetainedPackagesPerApp: 5 + namespaceLabels: {} + processDefaults: + diskQuotaMB: 1024 + memoryMB: 1024 + replicas: 1 + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 50m + memory: 100Mi + taskTTL: 30d + workloadsTLSSecret: korifi-workloads-ingress-cert +debug: false +defaultAppDomainName: null +eksContainerRegistryRoleARN: "" +generateIngressCertificates: false +helm: + hooksImage: alpine/k8s:1.25.2 +jobTaskRunner: + include: true + jobTTL: 24h + replicas: 1 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + temporarySetPodSeccompProfile: false +kpackImageBuilder: + builderReadinessTimeout: 30s + builderRepository: "" + clusterBuilderName: "" + clusterStackBuildImage: paketobuildpacks/build-jammy-full + clusterStackID: io.buildpacks.stacks.jammy + clusterStackRunImage: paketobuildpacks/run-jammy-full + include: true + replicas: 1 + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 50m + memory: 100Mi +logLevel: info +networking: + gatewayClass: null +reconcilers: + build: kpack-image-builder + run: statefulset-runner +rootNamespace: cf +stagingRequirements: + buildCacheMB: 2048 + diskMB: 0 + memoryMB: 0 +statefulsetRunner: + include: true + replicas: 1 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + temporarySetPodSeccompProfile: false +systemImagePullSecrets: [] diff --git a/module-data/korifi/values.yaml b/module-data/korifi/values.yaml new file mode 100644 index 0000000..d5c17d4 --- /dev/null +++ b/module-data/korifi/values.yaml @@ -0,0 +1,16 @@ +--- +adminUserName: cf-admin +containerRegistryCACertSecret: null +containerRegistrySecrets: +- cfapi-app-registry +containerRepositoryPrefix: "" +packageRegistrySecretNames: +- cfapi-app-registry +debug: false +defaultAppDomainName: null +eksContainerRegistryRoleARN: "" +generateIngressCertificates: false +logLevel: info +networking: + gatewayClass: istio +Secrets: ["cfapi-system-registry"] diff --git a/module-data/namespaces/namespaces.yaml b/module-data/namespaces/namespaces.yaml new file mode 100644 index 0000000..034bc68 --- /dev/null +++ b/module-data/namespaces/namespaces.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cf + labels: + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/enforce: restricted +--- +apiVersion: v1 +kind: Namespace +metadata: + name: korifi + labels: + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/enforce: restricted diff --git a/module-data/oidc/oidc-uaa-experimental.tmpl b/module-data/oidc/oidc-uaa-experimental.tmpl new file mode 100644 index 0000000..70a515f --- /dev/null +++ b/module-data/oidc/oidc-uaa-experimental.tmpl @@ -0,0 +1,14 @@ +{{ define "oidcUAA" }} +apiVersion: authentication.gardener.cloud/v1alpha1 +kind: OpenIDConnect +metadata: + name: oidc-uaa +spec: + issuerURL: {{.UAA}}/oauth/token + clientID: cf + usernameClaim: "user_name" + usernamePrefix: "sap.ids:" + groupsClaim: "" + supportedSigningAlgs: + - RS256 +{{ end }} \ No newline at end of file diff --git a/module-data/twuni-certificate/certificate.tmpl b/module-data/twuni-certificate/certificate.tmpl new file mode 100644 index 0000000..57bd16a --- /dev/null +++ b/module-data/twuni-certificate/certificate.tmpl @@ -0,0 +1,14 @@ +{{ define "twuniCert" }} +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: docker-registry-ingress-cert + namespace: cfapi-system +spec: + commonName: {{ .CFDomain }} + dnsNames: + - "{{ .TwuniDomain }}" + secretRef: + name: docker-registry-ingress-cert + namespace: cfapi-system +{{ end }} \ No newline at end of file diff --git a/module-data/twuni-dns-entry/dnsentry.tmpl b/module-data/twuni-dns-entry/dnsentry.tmpl new file mode 100644 index 0000000..35a3840 --- /dev/null +++ b/module-data/twuni-dns-entry/dnsentry.tmpl @@ -0,0 +1,16 @@ +{{ define "twuniDNSEntry" }} +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + # Let Gardener manage this DNS entry. + dns.gardener.cloud/class: garden + name: twuni-ingress + namespace: cfapi-system +spec: + dnsName: {{ .TwuniDomain }} + ttl: 600 + targets: + - {{ .IngressHost }} +{{ end }} \ No newline at end of file diff --git a/module-data/twuni-referencegrant/referencegrant.yaml b/module-data/twuni-referencegrant/referencegrant.yaml new file mode 100644 index 0000000..890382e --- /dev/null +++ b/module-data/twuni-referencegrant/referencegrant.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: docker-registry-reference-grant + namespace: cfapi-system +spec: + from: + - group: gateway.networking.k8s.io + kind: TLSRoute + namespace: korifi + to: + - group: '' + kind: Service + name: localregistry-docker-registry diff --git a/module-data/twuni-tlsroute/tlsroute.tmpl b/module-data/twuni-tlsroute/tlsroute.tmpl new file mode 100644 index 0000000..d562109 --- /dev/null +++ b/module-data/twuni-tlsroute/tlsroute.tmpl @@ -0,0 +1,22 @@ +{{ define "twuniTLSRoute" }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: docker-registry + namespace: korifi +spec: + hostnames: + - {{ .TwuniDomain }} + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: korifi + namespace: korifi-gateway + rules: + - backendRefs: + - group: "" + kind: Service + name: localregistry-docker-registry + namespace: cfapi-system + port: 30050 +{{ end }} \ No newline at end of file diff --git a/scripts/.DS_Store b/scripts/.DS_Store new file mode 100644 index 0000000..fffd2d5 Binary files /dev/null and b/scripts/.DS_Store differ diff --git a/scripts/clean-kyma.sh b/scripts/clean-kyma.sh new file mode 100644 index 0000000..16d73fb --- /dev/null +++ b/scripts/clean-kyma.sh @@ -0,0 +1,12 @@ + #!/bin/bash + kubectl delete all --all -n cf + kubectl delete ns cf + kubectl delete all --all -n korifi + kubectl delete ns korifi + kubectl delete all --all -n korifi-gateway + kubectl delete ns korifi-gateway + kubectl delete all --all -n kpack + kubectl delete ns kpack + kubectl delete deployment localregistry-docker-registry + kubectl delete all --all -n cfapi-system + kubectl delete ns cfapi-system diff --git a/scripts/local/get_kyma_file_name.sh b/scripts/local/get_kyma_file_name.sh new file mode 100644 index 0000000..fbaffd3 --- /dev/null +++ b/scripts/local/get_kyma_file_name.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +function get_kyma_file_name () { + _OS_TYPE="$(uname -s)" + _OS_ARCH="$(uname -m)" + + [ "$_OS_TYPE" == "Linux" ] && [ "$_OS_ARCH" == "x86_64" ] && echo "kyma-linux" || + [ "$_OS_TYPE" == "Linux" ] && [ "$_OS_ARCH" == "arm64" ] && echo "kyma-linux-arm" || + [ "$_OS_TYPE" == "Windows" ] && [ "$_OS_ARCH" == "x86_64" ] && echo "kyma.exe" || + [ "$_OS_TYPE" == "Windows" ] && [ "$_OS_ARCH" == "arm64" ] && echo "kyma-arm.exe" || + [ "$_OS_TYPE" == "Darwin" ] && [ "$_OS_ARCH" == "x86_64" ] && echo "kyma-darwin" +} + +get_kyma_file_name diff --git a/scripts/local/patch_template_file.sh b/scripts/local/patch_template_file.sh new file mode 100644 index 0000000..0188dfb --- /dev/null +++ b/scripts/local/patch_template_file.sh @@ -0,0 +1,8 @@ +#! /bin/bash + +QUERY='(.spec.descriptor.component.repositoryContexts.[] | select(.).baseUrl)' +REPO=$(yq e "$QUERY" ./template.yaml) +# shellcheck disable=SC2001 +NEW_REPO=$(echo "$REPO" | sed -E 's/:[0-9]+/:5000/g') +echo "$NEW_REPO" +yq -i e "$QUERY |= \"$NEW_REPO\"" ./template.yaml \ No newline at end of file diff --git a/scripts/release/create_changelog.sh b/scripts/release/create_changelog.sh new file mode 100644 index 0000000..e2dbb85 --- /dev/null +++ b/scripts/release/create_changelog.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +PREVIOUS_RELEASE=$2 + +set -o nounset +set -o errexit +set -E +set -o pipefail + +RELEASE_VERSION=$1 + +if [ "${PREVIOUS_RELEASE}" == "" ] +then + PREVIOUS_RELEASE=$(git describe --tags --abbrev=0) +fi + +REPOSITORY=${REPOSITORY:-kyma-project/template-operator} +GITHUB_URL=https://api.github.com/repos/${REPOSITORY} +GITHUB_AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" +CHANGELOG_FILE="CHANGELOG.md" + +echo "## What has changed" >> ${CHANGELOG_FILE} +git log "${PREVIOUS_RELEASE}"..HEAD --pretty=tformat:"%h" --reverse | while read -r commit +do + COMMIT_AUTHOR=$(curl -H "${GITHUB_AUTH_HEADER}" -sS "${GITHUB_URL}/commits/${commit}" | jq -r '.author.login') + if [ "${COMMIT_AUTHOR}" != "kyma-bot" ]; then + git show -s "${commit}" --format="* %s by @${COMMIT_AUTHOR}" >> ${CHANGELOG_FILE} + fi +done + +NEW_CONTRIB=$$.new + +join -v2 \ +<(curl -H "${GITHUB_AUTH_HEADER}" -sS "${GITHUB_URL}/compare/$(git rev-list --max-parents=0 HEAD)...${PREVIOUS_RELEASE}" | jq -r '.commits[].author.login' | sort -u) \ +<(curl -H "${GITHUB_AUTH_HEADER}" -sS "${GITHUB_URL}/compare/${PREVIOUS_RELEASE}...HEAD" | jq -r '.commits[].author.login' | sort -u) >${NEW_CONTRIB} + +if [ -s ${NEW_CONTRIB} ] +then + echo -e "\n## New contributors" >> ${CHANGELOG_FILE} + while read -r user + do + REF_PR=$(grep "@${user}" ${CHANGELOG_FILE} | head -1 | grep -o " (#[0-9]\+)" || true) + if [ -n "${REF_PR}" ] + then + REF_PR=" in ${REF_PR}" + fi + echo "* @${user} made first contribution${REF_PR}" >> ${CHANGELOG_FILE} + done <${NEW_CONTRIB} +fi + +echo -e "\n**Full changelog**: https://github.com/$REPOSITORY/compare/${PREVIOUS_RELEASE}...${RELEASE_VERSION}" >> ${CHANGELOG_FILE} + +rm ${NEW_CONTRIB} || echo "cleaned up" diff --git a/scripts/release/draft_release.sh b/scripts/release/draft_release.sh new file mode 100644 index 0000000..bc7cc7b --- /dev/null +++ b/scripts/release/draft_release.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -E +set -o pipefail + +RELEASE_TAG=$1 + +REPOSITORY=${REPOSITORY:-kyma-project/template-operator} +GITHUB_URL=https://api.github.com/repos/${REPOSITORY} +GITHUB_AUTH_HEADER="Authorization: Bearer ${GITHUB_TOKEN}" +CHANGELOG_FILE=$(cat CHANGELOG.md) + +JSON_PAYLOAD=$(jq -n \ + --arg tag_name "$RELEASE_TAG" \ + --arg name "$RELEASE_TAG" \ + --arg body "$CHANGELOG_FILE" \ + '{ + "tag_name": $tag_name, + "name": $name, + "body": $body, + "draft": true + }') + +CURL_RESPONSE=$(curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "${GITHUB_AUTH_HEADER}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${GITHUB_URL}"/releases \ + -d "$JSON_PAYLOAD") + +# return the id of the release draft +echo "$CURL_RESPONSE" | jq -r ".id" diff --git a/scripts/release/publish_release.sh b/scripts/release/publish_release.sh new file mode 100644 index 0000000..9faa6a9 --- /dev/null +++ b/scripts/release/publish_release.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -E +set -o pipefail + +RELEASE_VERSION=$1 + +REPOSITORY=${REPOSITORY:-kyma-project/template-operator} +GITHUB_URL=https://api.github.com/repos/${REPOSITORY} +GITHUB_AUTH_HEADER="Authorization: Bearer ${GITHUB_TOKEN}" + +CURL_RESPONSE=$(curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "${GITHUB_AUTH_HEADER}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${GITHUB_URL}"/releases/"${RELEASE_VERSION}" \ + -d '{"draft":false}') +echo "$CURL_RESPONSE" diff --git a/scripts/release/upload_assets.sh b/scripts/release/upload_assets.sh new file mode 100644 index 0000000..02f52f1 --- /dev/null +++ b/scripts/release/upload_assets.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -E +set -o pipefail + +uploadFile() { + filePath=${1} + ghAsset=${2} + + echo "Uploading ${filePath} as ${ghAsset}" + response=$(curl -s -o output.txt -w "%{http_code}" \ + --request POST --data-binary @"$filePath" \ + -H "Authorization: token $BOT_GITHUB_TOKEN" \ + -H "Content-Type: text/yaml" \ + "$ghAsset") + if [[ "$response" != "201" ]]; then + echo "Unable to upload the asset ($filePath): " + echo "HTTP Status: $response" + cat output.txt + exit 1 + else + echo "$filePath uploaded" + fi +} + +echo "PULL_BASE_REF= ${PULL_BASE_REF}" + +MODULE_VERSION=${PULL_BASE_REF} make build-manifests +echo "Generated template-operator.yaml:" +cat template-operator.yaml + +MODULE_VERSION=${PULL_BASE_REF} make build-module +echo "Generated module-template.yaml:" +cat module-template.yaml + +echo "Fetching releases" +CURL_RESPONSE=$(curl -w "%{http_code}" -sL \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $BOT_GITHUB_TOKEN"\ + https://api.github.com/repos/kyma-project/template-operator/releases) +JSON_RESPONSE=$(sed '$ d' <<< "${CURL_RESPONSE}") +HTTP_CODE=$(tail -n1 <<< "${CURL_RESPONSE}") +if [[ "${HTTP_CODE}" != "200" ]]; then + echo "${CURL_RESPONSE}" + exit 1 +fi + +echo "Finding release id for: ${PULL_BASE_REF}" +RELEASE_ID=$(jq <<< "${JSON_RESPONSE}" --arg tag "${PULL_BASE_REF}" '.[] | select(.tag_name == $ARGS.named.tag) | .id') + +echo "Got '${RELEASE_ID}' release id" +if [ -z "${RELEASE_ID}" ] +then + echo "No release with tag = ${PULL_BASE_REF}" + exit 1 +fi + +echo "Adding assets to Github release" +UPLOAD_URL="https://uploads.github.com/repos/kyma-project/template-operator/releases/${RELEASE_ID}/assets" + +echo "$UPLOAD_URL" +uploadFile "template-operator.yaml" "${UPLOAD_URL}?name=template-operator.yaml" +uploadFile "module-template.yaml" "${UPLOAD_URL}?name=module-template.yaml" +uploadFile "config/samples/default-sample-cr.yaml" "${UPLOAD_URL}?name=default-sample-cr.yaml" +uploadFile "module-config.yaml" "${UPLOAD_URL}?name=module-config.yaml" diff --git a/scripts/release/validate_pipeline_status.sh b/scripts/release/validate_pipeline_status.sh new file mode 100644 index 0000000..f6b00ef --- /dev/null +++ b/scripts/release/validate_pipeline_status.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +echo "Checking status of 'post-*' pipelines for template-operator" +REF_NAME="${1:-"main"}" +STATUS_URL="https://api.github.com/repos/kyma-project/template-operator/commits/${REF_NAME}/status" +STATUS=$(curl -L -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${STATUS_URL}" | head -n 2 ) +if [[ "$STATUS" == *"success"* ]]; then + echo "All recent jobs succeeded, post-pipelines are green." +else + echo "Latest post-pipelines are failing or pending! Reason:" + echo "$STATUS" + exit 1 +fi diff --git a/scripts/release/validate_versions.sh b/scripts/release/validate_versions.sh new file mode 100644 index 0000000..867a83b --- /dev/null +++ b/scripts/release/validate_versions.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -ue +source ./../../.version + +DESIRED_VERSION=$1 +if [[ "$DESIRED_VERSION" != "$MODULE_VERSION" ]]; then + echo "Versions don't match! Expected ${MODULE_VERSION} but got $DESIRED_VERSION." + echo "Please update .version file or change desired version!" + exit 1 +fi +echo "Versions match." + +IMAGE_TO_CHECK="${2:-europe-docker.pkg.dev/kyma-project/prod/template-operator}" +BUMPED_IMAGE_TAG=$(grep "${IMAGE_TO_CHECK}" ../../sec-scanners-config.yaml | cut -d : -f 2) +if [[ "$BUMPED_IMAGE_TAG" != "$DESIRED_VERSION" ]]; then + echo "Version tag in sec-scanners-config.yaml file is incorrect!" + echo "Could not find $DESIRED_VERSION." + exit 1 +fi +echo "Image version tag in sec-scanners-config.yaml does match with release tag." +exit 0