diff --git a/composition/.dockerignore b/composition/.dockerignore new file mode 100644 index 0000000..a3aab7a --- /dev/null +++ b/composition/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/composition/.gitignore b/composition/.gitignore new file mode 100644 index 0000000..b523105 --- /dev/null +++ b/composition/.gitignore @@ -0,0 +1,32 @@ +# test logs +test-logs/* + +bin/* +expanders/bin/* + +release/test/* + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/composition/.golangci.yml b/composition/.golangci.yml new file mode 100644 index 0000000..7274f92 --- /dev/null +++ b/composition/.golangci.yml @@ -0,0 +1,50 @@ +# Copyright 2024 Google LLC +# +# 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. + +run: + deadline: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + #- path: "api/*" + # linters: + # - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - exportloopref + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - staticcheck + - typecheck + - unconvert + - unparam + - unused diff --git a/composition/Dockerfile b/composition/Dockerfile new file mode 100644 index 0000000..b44f192 --- /dev/null +++ b/composition/Dockerfile @@ -0,0 +1,49 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Build the manager binary +FROM golang:1.23.2 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/controller/ internal/controller/ +COPY proto/ proto/ +COPY pkg/ pkg/ + +# 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 -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/composition/Dockerfile.getter.expander b/composition/Dockerfile.getter.expander new file mode 100644 index 0000000..2810507 --- /dev/null +++ b/composition/Dockerfile.getter.expander @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# 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. + +# syntax=docker/dockerfile:1 + +# ----------------- Build Container --------------------------- +# Build the go app. +# Explicitly set to latest vs golang:1.22 +FROM golang:1.23.2 AS build-stage + +# Set destination for COPY +WORKDIR /go/src/app + +# Download Go modules +# https://docs.docker.com/reference/dockerfile/#copy +COPY . ./ +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -v -o expander expanders/getter/main.go + +# ----------------- Expander Container --------------------------- +#FROM marketplace.gcr.io/google/debian10:latest as bootstrap +FROM scratch as inline + +EXPOSE 8443 + +WORKDIR / +COPY --from=build-stage /go/src/app/expander . + +ENTRYPOINT ["/expander"] diff --git a/composition/Dockerfile.inline b/composition/Dockerfile.inline new file mode 100644 index 0000000..7807d2f --- /dev/null +++ b/composition/Dockerfile.inline @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 + +# ----------------- Build Container --------------------------- +# Build the go app. +FROM golang:1.23.2 AS build-stage + +# Set destination for COPY +WORKDIR /go/src/app + +# Download Go modules +# https://docs.docker.com/reference/dockerfile/#copy +COPY go.mod go.sum ./ +COPY ./cmd/inline/main.go ./ +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -v -o inline main.go + +# ----------------- Final Container --------------------------- +#FROM marketplace.gcr.io/google/debian10:latest as bootstrap +FROM scratch as inline + +WORKDIR /root + +COPY --from=build-stage /go/src/app/inline . + +ENTRYPOINT ["/root/inline"] diff --git a/composition/Dockerfile.jinja2.expander b/composition/Dockerfile.jinja2.expander new file mode 100644 index 0000000..f688052 --- /dev/null +++ b/composition/Dockerfile.jinja2.expander @@ -0,0 +1,51 @@ +# Copyright 2024 Google LLC +# +# 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. + +# syntax=docker/dockerfile:1 + +# ----------------- Build Container --------------------------- +# Build the go app. +# Explicitly set to latest vs golang:1.22 +FROM golang:1.23.2 AS build-stage + +# Set destination for COPY +WORKDIR /go/src/app + +# Download Go modules +# https://docs.docker.com/reference/dockerfile/#copy +COPY go.mod go.sum ./ +COPY ./expanders ./expanders +COPY ./proto ./proto +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -v -o expander expanders/jinja2/main.go + +# ----------------- Expander Container --------------------------- +FROM python:3.7-slim as expander + +EXPOSE 50051 + +WORKDIR / +COPY --from=build-stage /go/src/app/expander . + + +COPY ./expanders/jinja2/requirements.txt ./ +RUN pip install --require-hashes -r requirements.txt +COPY ./expanders/jinja2/parse_template.py ./ + +# Required when setting pod .spec.securityContext.runAsNonRoot: true +#USER 65532:65532 + +ENTRYPOINT ["jinja2"] diff --git a/composition/Makefile b/composition/Makefile new file mode 100644 index 0000000..c670507 --- /dev/null +++ b/composition/Makefile @@ -0,0 +1,264 @@ +# GCP project to use for pushing manifests +GCP_PROJECT_ID ?= $(shell gcloud config get-value project) + +# Image URLs to use for building/pushing image targets +GIT_IMG_VERSION ?= $(shell git rev-parse --short HEAD) +IMG_VERSION ?= v0.0.1 +IMG_REGISTRY ?= gcr.io/$(GCP_PROJECT_ID) +IMG ?= $(IMG_REGISTRY)/composition:$(IMG_VERSION) +JINJA_IMG ?= $(IMG_REGISTRY)/expander-jinja2:$(IMG_VERSION) +GETTER_IMG ?= $(IMG_REGISTRY)/expander-getter:$(IMG_VERSION) +GOPREFIX ?= GOWORK=off + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.28.0 +KIND_CLUSTER ?= kind + + + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(GOPREFIX) go run sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(GOPREFIX) go run sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(GOPREFIX) go run -mod=readonly golang.org/x/tools/cmd/goimports@latest -w api cmd internal tests proto + +.PHONY: fmt +fmt: ## Run go fmt against code. + $(GOPREFIX) go fmt ./... + GOFLAGS= $(GOPREFIX) go run github.com/google/addlicense@04bfe4ee9ca5764577b029acc6a1957fd1997153 -c "Google LLC" -l apache ./ + $(GOPREFIX) go run -mod=readonly golang.org/x/tools/cmd/goimports@latest -w api cmd internal tests proto + +.PHONY: vet +vet: ## Run go vet against code. + $(GOPREFIX) go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GOPREFIX) go test ./... -coverprofile cover.out + +GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint +GOLANGCI_LINT_VERSION ?= v1.54.2 +golangci-lint: + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\ + } + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter & yamllint + $(GOPREFIX) $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOPREFIX) $(GOLANGCI_LINT) run --fix + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + $(GOPREFIX) go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + $(GOPREFIX) go run ./cmd/main.go + +.PHONY: debug +debug: generate fmt vet manifests + GO111MODULE=on dlv debug ./cmd/main.go + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name project-v3-builder + $(CONTAINER_TOOL) buildx use project-v3-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Testing + +.PHONY: common-test-manifests +common-test-manifests: manifests kustomize + mkdir -p release/test + cd ../facade && $(KUSTOMIZE) build config/crd -o ../composition/release/test/facade_crds.yaml + $(KUSTOMIZE) build config/crd -o release/test/crds.yaml + +.PHONY: release-manifests +release-manifests: manifests kustomize + $(KUSTOMIZE) build config/crd -o release/crds.yaml + $(KUSTOMIZE) build config/release -o release/manifest.yaml + +.PHONY: release-test-cc-manifests +release-test-cc-manifests: common-test-manifests + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd config/default && $(KUSTOMIZE) edit add patch --namespace system --name controller-manager --kind Deployment --patch "[{\"op\": \"add\", \"path\": \"/spec/template/spec/containers/1/imagePullPolicy\", \"value\": \"Always\"}]" + cd config/expanders && $(KUSTOMIZE) edit set image expander-jinja2=${JINJA_IMG} + cd config/expanders && $(KUSTOMIZE) edit set image expander-getter=${GETTER_IMG} + $(KUSTOMIZE) build config/default -o release/test/cc-operator.yaml + cd config/manager && $(KUSTOMIZE) edit set image controller=composition:latest + cd config/default && $(KUSTOMIZE) edit remove patch --namespace system --name controller-manager --kind Deployment --patch "[{\"op\": \"add\", \"path\": \"/spec/template/spec/containers/1/imagePullPolicy\", \"value\": \"Always\"}]" + cd config/expanders && $(KUSTOMIZE) edit set image expander-jinja2=expander-jinja2:latest + cd config/expanders && $(KUSTOMIZE) edit set image expander-getter=expander-getter:latest + +.PHONY: release-kind-manifests +release-test-kind-manifests: common-test-manifests + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd config/expanders && $(KUSTOMIZE) edit set image expander-jinja2=${JINJA_IMG} + cd config/expanders && $(KUSTOMIZE) edit set image expander-getter=${GETTER_IMG} + $(KUSTOMIZE) build config/default -o release/test/kind-operator.yaml + cd config/manager && $(KUSTOMIZE) edit set image controller=composition:latest + cd config/expanders && $(KUSTOMIZE) edit set image expander-jinja2=expander-jinja2:latest + cd config/expanders && $(KUSTOMIZE) edit set image expander-getter=expander-getter:latest + +.PHONY: deploy-kind +deploy-kind: release-test-kind-manifests docker-build docker-build-inline docker-build-expander-jinja2 docker-build-expander-getter + kind delete clusters ${KIND_CLUSTER} || true + kind create cluster --name ${KIND_CLUSTER} + kind load docker-image ${IMG} --name ${KIND_CLUSTER} + # for testing samples/FirstComposition + docker image pull nginx:1.16.0 + kind load docker-image nginx:1.16.0 --name ${KIND_CLUSTER} + kind load docker-image ${INLINE_IMG} --name ${KIND_CLUSTER} + kind load docker-image ${JINJA_IMG} --name ${KIND_CLUSTER} + kind load docker-image ${GETTER_IMG} --name ${KIND_CLUSTER} + kubectl --context kind-${KIND_CLUSTER} apply -f release/test/crds.yaml + kubectl --context kind-${KIND_CLUSTER} apply -f release/test/facade_crds.yaml + sleep 5 # for CRDs to be registered + kubectl --context kind-${KIND_CLUSTER} apply -f release/test/kind-operator.yaml + sleep 5 + kubectl --context kind-${KIND_CLUSTER} get pods -A + + +.PHONY: run-test +run-test: release-test-kind-manifests docker-build docker-build-inline docker-build-expander-jinja2 docker-build-expander-getter fmt #docker-push docker-push-inline docker-push-expander-jinja2 docker-push-expander-getter + cd tests/testcases/ && $(GOPREFIX) go test -v -timeout 3600s -run "^${TESTCASE}" --images=${IMG},${INLINE_IMG},${JINJA_IMG},${GETTER_IMG} + +.PHONY: e2e-test +e2e-test: release-test-kind-manifests docker-build docker-build-inline docker-build-expander-jinja2 docker-build-expander-getter fmt #docker-push docker-push-inline docker-push-expander-jinja2 docker-push-expander-getter + cd tests/testcases/ && $(GOPREFIX) go test -v -timeout 3600s -run ./... --images=${IMG},${INLINE_IMG},${JINJA_IMG},${GETTER_IMG} + +.PHONY: kcc-test +kcc-test: release-test-cc-manifests docker-build docker-build-inline docker-build-expander-jinja2 docker-build-expander-getter fmt docker-push docker-push-inline docker-push-expander-jinja2 docker-push-expander-getter + cd tests/testcases/ && $(GOPREFIX) go test -v -timeout 3600s -run "^TestKCCSampleCloudSQL" --use-cc=true --images=${IMG},${INLINE_IMG},${JINJA_IMG},${GETTER_IMG} + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + cd config/manager && $(KUSTOMIZE) edit set image controller=composition:latest + + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.2.1 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on $(GOPREFIX) go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) $(GOPREFIX) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + + +###### ----------- include other make targets +include inline.mk +include expanders.mk diff --git a/composition/PROJECT b/composition/PROJECT new file mode 100644 index 0000000..a51be0c --- /dev/null +++ b/composition/PROJECT @@ -0,0 +1,65 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: google.com +layout: +- go.kubebuilder.io/v4 +projectName: composition +repo: github.com/cloud-native-compositions/compositions/composition +resources: +- api: + crdVersion: v1alpha1 + namespaced: true + controller: true + domain: google.com + group: composition + kind: Composition + path: github.com/cloud-native-compositions/compositions/composition/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1alpha1 + namespaced: true + controller: true + domain: google.com + group: composition + kind: Context + path: github.com/cloud-native-compositions/compositions/composition/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1alpha1 + namespaced: true + controller: true + domain: google.com + group: composition + kind: Plan + path: github.com/cloud-native-compositions/compositions/composition/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: google.com + group: composition + kind: Facade + path: github.com/cloud-native-compositions/compositions/composition/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: google.com + group: composition + kind: ExpanderVersion + path: github.com/cloud-native-compositions/compositions/composition/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: google.com + group: composition + kind: GetterConfiguration + path: github.com/cloud-native-compositions/compositions/composition/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/composition/README.md b/composition/README.md new file mode 100644 index 0000000..096a278 --- /dev/null +++ b/composition/README.md @@ -0,0 +1,92 @@ +# composition +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started + +### Prerequisites +- go version v1.20.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. + +### To Deploy on the cluster +**Build and push your image to the location specified by `IMG`:** + +```sh +make docker-build docker-push IMG=/composition:tag +``` + +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. + +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/composition:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +>**NOTE**: Ensure that the samples has default values to test it out. + +### To Uninstall +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + + +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/composition/TODO.md b/composition/TODO.md new file mode 100644 index 0000000..4c8f676 --- /dev/null +++ b/composition/TODO.md @@ -0,0 +1,11 @@ +# Code Organization + +1. Move expander composition/expanders/jinja2 -> expanders/jinja2 +2. Move expander composition/expanders/getter -> expanders/getter +3. Remove `inline` and Job based expander code and dockerfile, make targets +4. move away from using github.com/wzshiming/easycel +5. Add fields to composition/config/samples/* +6. kube-rbac-proxy is not needed anymore. remove it from manifests. ASO PR: Azure/azure-service-operator#3833 . controller-runtime feature: kubernetes-sigs/controller-runtime#2407, they're dropping it in the scaffolding too here: kubernetes-sigs/kubebuilder#3871 +7. composition/config/manager/manager.yaml add readOnlyRootFilesystem +8. expanderConfig.configRef.namespace => force this to be the same ns as composition. so remove this ? +9. Should compositions be namespace scoped ? \ No newline at end of file diff --git a/composition/api/v1alpha1/composition_types.go b/composition/api/v1alpha1/composition_types.go new file mode 100644 index 0000000..6ff6b9a --- /dev/null +++ b/composition/api/v1alpha1/composition_types.go @@ -0,0 +1,183 @@ +/* +Copyright 2024. + +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 + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ConditionType defines the type of ManagedConfigSync condition +type ConditionType string + +// The valid conditions of Compositions +const ( + Ready ConditionType = "Ready" + // Error implies the last reconcile attempt failed + Error ConditionType = "Error" + // Validation implies the validation failed + ValidationFailed ConditionType = "ValidationFailed" + // Waiting - Plan is waiting for values to progress + Waiting ConditionType = "Waiting" +) + +type Jinja2 struct { + Template string `json:"template"` +} + +// ConfigReference - For BYO Expanders, we can extend it +type ConfigReference struct { + //+kubebuilder:validation:Required + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +type ExpanderConfig struct { + // Built in expanders + Jinja2 *Jinja2 `json:"jinja2,omitempty"` + // For BYO Expanders use generic template or ref for external config + Template string `json:"template,omitempty"` + ConfigRef *ConfigReference `json:"configref,omitempty"` +} + +type Expander struct { + //+kubebuilder:validation:Required + Name string `json:"name"` + + // Type indicates what expander to use + // jinja - jinja2 expander + // ... + // +kubebuilder:default=jinja2 + Type string `json:"type"` + // +kubebuilder:default=latest + Version string `json:"version,omitempty"` + + // TODO (barney-s): Make ConfigReference the only way to specify and dont have any inline expander configs + // This would make the UX experience uniform. + ExpanderConfig `json:""` +} + +type NamespaceMode string + +const ( + // NamespaceModeNone is when nothing is set, this is the same as Inherit + NamespaceModeNone NamespaceMode = "" + // NamespaceModeInherit implies all the objects namespace is replaced with the input api object's namespace + NamespaceModeInherit NamespaceMode = "inherit" + // NamespaceModeExplicit implies the objects in the template must have its namespace set + NamespaceModeExplicit NamespaceMode = "explicit" +) + +// ReadyOn defines ready condition for a GVK +type ReadyOn struct { + //+kubebuilder:validation:Required + Group string `json:"group"` + Version string `json:"version,omitempty"` + //+kubebuilder:validation:Required + Kind string `json:"kind"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + //+kubebuilder:validation:Required + Ready string `json:"readyIf"` +} + +// CompositionSpec defines the desired state of Composition +type CompositionSpec struct { + // NOTE: Tighten the Composition API to include fields that are used in the controller + // As we add features we can uncomment these fields + //Name string `json:"name"` + //Namespace string `json:"namespace"` + //InputName string `json:"inputName,omitempty"` + //InputNamespace string `json:"inputNamespace,omitempty"` + //Sinc Sinc `json:"sinc,omitempty"` + + Description string `json:"description,omitempty"` + + // TODO (barney -s) rename to FacadeAPIGroup,facadeAPIGroup + + // Use existing KRM API + InputAPIGroup string `json:"inputAPIGroup,omitempty"` + + //+kubebuilder:validation:MinItems=1 + Expanders []Expander `json:"expanders"` + // Namespace mode indicates how compositions set the namespace of the objects from expanders. + // ""|inherit implies inherit the facade api's namespace. Only namespaced objects are allowed. + // explicit implies the objects in the template must have the namespace set. + // +kubebuilder:validation:Enum=inherit;explicit + NamespaceMode NamespaceMode `json:"namespaceMode,omitempty"` + + // Readiness + Readiness []ReadyOn `json:"readiness,omitempty"` +} + +type ValidationStatus string + +const ( + // ValidationStatusUnkown is when it is not validated + ValidationStatusUnknown ValidationStatus = "unknown" + // ValidationStatusSuccess is when valdiation succeeds + ValidationStatusSuccess ValidationStatus = "success" + // ValidationStatusFailed is when valdiation fails + ValidationStatusFailed ValidationStatus = "failed" + // ValidationStatusError is when validation was not called + ValidationStatusError ValidationStatus = "error" +) + +// StageStatus captures the status of a stage +type StageValidationStatus struct { + ValidationStatus ValidationStatus `json:"validationStatus,omitempty"` + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` +} + +// CompositionStatus defines the observed state of Composition +type CompositionStatus struct { + Generation int64 `json:"generation,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + Stages map[string]StageValidationStatus `json:"stages,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Cluster + +// Composition is the Schema for the compositions API +type Composition struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CompositionSpec `json:"spec,omitempty"` + Status CompositionStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// CompositionList contains a list of Composition +type CompositionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Composition `json:"items"` +} + +// Status helpers +func (s *CompositionStatus) ClearCondition(condition ConditionType) { + meta.RemoveStatusCondition(&s.Conditions, string(condition)) +} + +func init() { + SchemeBuilder.Register(&Composition{}, &CompositionList{}) +} diff --git a/composition/api/v1alpha1/context_types.go b/composition/api/v1alpha1/context_types.go new file mode 100644 index 0000000..3cacc3f --- /dev/null +++ b/composition/api/v1alpha1/context_types.go @@ -0,0 +1,61 @@ +/* +Copyright 2024. + +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 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ContextSpec defines the desired state of Context +type ContextSpec struct { + // Project is passed to the expander. + Project string `json:"project,omitempty"` +} + +// ContextStatus defines the observed state of Context +type ContextStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Context is used to set a set of common parameters per namespace. +// Currently it includes a string that identifies a cloud project +type Context struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ContextSpec `json:"spec,omitempty"` + Status ContextStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ContextList contains a list of Context +type ContextList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Context `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Context{}, &ContextList{}) +} diff --git a/composition/api/v1alpha1/expanderversion_types.go b/composition/api/v1alpha1/expanderversion_types.go new file mode 100644 index 0000000..186049f --- /dev/null +++ b/composition/api/v1alpha1/expanderversion_types.go @@ -0,0 +1,145 @@ +/* +Copyright 2024. + +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 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + semver "github.com/Masterminds/semver/v3" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +type ExpanderType string + +const ( + // ExpanderTypeJob run expander in a job + ExpanderTypeJob ExpanderType = "job" + // ExpanderTypeGRPC expect expander service to be present + ExpanderTypeGRPC ExpanderType = "grpc" +) + +type ExpanderConfigGVK struct { + //+kubebuilder:validation:Required + Group string `json:"group"` + //+kubebuilder:validation:Required + Version string `json:"version"` + //+kubebuilder:validation:Required + Kind string `json:"kind"` +} + +// ExpanderVersionSpec defines the desired state of ExpanderVersion +type ExpanderVersionSpec struct { + // ImageRegistry is the designated registry for where to pull the named expander image + ImageRegistry string `json:"imageRegistry,omitempty"` + + // Image if different from removePrefix(expanderversion.name , "composition-") + Image string `json:"image,omitempty"` + + // ValidVersions is a list of valid versions of the named expander + //+kubebuilder:validation:Required + ValidVersions []string `json:"validVersions"` + + // Type indicates what sort of expander: + // job - job based expander. ephemeral + // grpc - grpc service expander. persistent + // +kubebuilder:validation:Enum=job;grpc + // +kubebuilder:default=job + Type ExpanderType `json:"type"` + + // ExpanderConfig GVK + Config ExpanderConfigGVK `json:"config"` +} + +// ExpanderVersionStatus defines the observed state of ExpanderVersion +type ExpanderVersionStatus struct { + VersionMap map[string]string `json:"versionMap,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ExpanderVersion is the Schema for the expanderversions API +type ExpanderVersion struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ExpanderVersionSpec `json:"spec,omitempty"` + Status ExpanderVersionStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ExpanderVersionList contains a list of ExpanderVersion +type ExpanderVersionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ExpanderVersion `json:"items"` +} + +// Status helpers +func (s *ExpanderVersionStatus) ClearCondition(condition ConditionType) { + meta.RemoveStatusCondition(&s.Conditions, string(condition)) +} + +// Validation +func (ev *ExpanderVersion) Validate() bool { + // Several of these validations should eventually be CEL rules on the composition CRD + ev.Status.ClearCondition(ValidationFailed) + // Validate + message := "" + if ev.Spec.ImageRegistry == "" && ev.Spec.Type == ExpanderTypeJob { + message += "spec.imageRegistry required for type=job; " + } + if len(ev.Spec.ValidVersions) == 0 { + message += "spec.validVersions required; " + } + + invalidVersions := []string{} + invalidVersionMessage := "" + for _, r := range ev.Spec.ValidVersions { + _, err := semver.NewVersion(r) + if err != nil { + invalidVersions = append(invalidVersions, r) + invalidVersionMessage += fmt.Sprintf("%s: %v\n", r, err) + } + } + if invalidVersionMessage != "" { + message += fmt.Sprintf("invalid versions: %v\n", invalidVersions) + message += invalidVersionMessage + } + + if message != "" { + ev.Status.Conditions = append(ev.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: message, + Reason: "ValidationFailed", + Type: string(ValidationFailed), + Status: metav1.ConditionTrue, + }) + return false + } + return true +} + +func init() { + SchemeBuilder.Register(&ExpanderVersion{}, &ExpanderVersionList{}) +} diff --git a/composition/api/v1alpha1/facade_types.go b/composition/api/v1alpha1/facade_types.go new file mode 100644 index 0000000..024bd05 --- /dev/null +++ b/composition/api/v1alpha1/facade_types.go @@ -0,0 +1,86 @@ +/* +Copyright 2024. + +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 + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +type SimpleSchema struct { + Spec runtime.RawExtension `json:"spec"` + Status runtime.RawExtension `json:"status"` + Required []string `json:"required,omitempty"` +} + +// FacadeSpec defines the desired state of Facade +type FacadeSpec struct { + // Bring your own open API spec + // +kubebuilder:validation:Schemaless + // +kubebuilder:validation:Type=object + // +kubebuilder:pruning:PreserveUnknownFields + // +k8s:conversion-gen=false + OpenAPIV3Schema *apiextensionsv1.JSONSchemaProps `json:"openAPIV3Schema,omitempty"` + + // Simple Schema + SimpleSchema *SimpleSchema `json:"simpleSchema,omitempty"` + + //+kubebuilder:validation:Required + FacadeKind string `json:"facadeKind"` +} + +// FacadeStatus defines the observed state of Facade +type FacadeStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Facade is the Schema for the facades API +type Facade struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FacadeSpec `json:"spec,omitempty"` + Status FacadeStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// FacadeList contains a list of Facade +type FacadeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Facade `json:"items"` +} + +// Status helpers +func (s *FacadeStatus) ClearCondition(condition ConditionType) { + meta.RemoveStatusCondition(&s.Conditions, string(condition)) +} + +// Validation +func (f *Facade) Validate() bool { + return true +} + +func init() { + SchemeBuilder.Register(&Facade{}, &FacadeList{}) +} diff --git a/composition/api/v1alpha1/getter_types.go b/composition/api/v1alpha1/getter_types.go new file mode 100644 index 0000000..f183240 --- /dev/null +++ b/composition/api/v1alpha1/getter_types.go @@ -0,0 +1,123 @@ +/* +Copyright 2024. + +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 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ResourceRef struct { + // OPTION 1 + // ./// + // resource: ServiceIdentity.serviceusage.cnrm.cloud.google.com/v1beta1//sqladmin.googleapis.com + + // OPTION 2 + Group string `json:"group,omitempty"` + Version string `json:"version,omitempty"` + //+kubebuilder:validation:Required + Resource string `json:"resource"` + //+kubebuilder:validation:Required + Kind string `json:"kind"` + + // OneOf validation added for Name and NameSuffix + Name string `json:"name,omitempty"` + NameSuffix string `json:"nameSuffix,omitempty"` +} + +type FieldRef struct { + //+kubebuilder:validation:Required + Path string `json:"path"` + //+kubebuilder:validation:Required + As string `json:"as"` +} + +type ValuesFrom struct { + //+kubebuilder:validation:Required + Name string `json:"name"` + //+kubebuilder:validation:Required + ResourceRef ResourceRef `json:"resourceRef"` + //+kubebuilder:validation:Required + FieldRef []FieldRef `json:"fieldRef"` +} + +// GetterConfigurationSpec defines the desired state of GetterConfiguration +type GetterConfigurationSpec struct { + ValuesFrom []ValuesFrom `json:"valuesFrom,omitempty"` +} + +// GetterConfigurationStatus defines the observed state of GetterConfiguration +type GetterConfigurationStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// GetterConfiguration is the Schema for the getters API +type GetterConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GetterConfigurationSpec `json:"spec,omitempty"` + Status GetterConfigurationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// GetterConfigurationList contains a list of GetterConfiguration +type GetterConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GetterConfiguration `json:"items"` +} + +// Status helpers +func (g *GetterConfigurationStatus) ClearCondition(condition ConditionType) { + meta.RemoveStatusCondition(&g.Conditions, string(condition)) +} + +// Validation +func (g *GetterConfiguration) Validate() bool { + // Several of these validations should br CEL rules on the composition CRD + // However for now they help me shape the controller. + g.Status.ClearCondition(ValidationFailed) + // Validate Expanders + message := "" + for index, vf := range g.Spec.ValuesFrom { + if vf.ResourceRef.Name == "" && vf.ResourceRef.NameSuffix == "" { + message += fmt.Sprintf(".spec.valuesFrom[%d] requires name or nameSuffix; ", index) + } + } + if message != "" { + g.Status.Conditions = append(g.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: message, + Reason: "GetterConfigurationValidationFailed", + Type: string(ValidationFailed), + Status: metav1.ConditionTrue, + }) + return false + } + return true +} + +func init() { + SchemeBuilder.Register(&GetterConfiguration{}, &GetterConfigurationList{}) +} diff --git a/composition/api/v1alpha1/groupversion_info.go b/composition/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..02fee75 --- /dev/null +++ b/composition/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +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 composition v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=composition.google.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "composition.google.com", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/composition/api/v1alpha1/plan_types.go b/composition/api/v1alpha1/plan_types.go new file mode 100644 index 0000000..2cfe88b --- /dev/null +++ b/composition/api/v1alpha1/plan_types.go @@ -0,0 +1,126 @@ +/* +Copyright 2024. + +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 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +type Stage struct { + Manifest string `json:"manifest,omitempty"` + Values string `json:"values,omitempty"` +} + +// PlanSpec defines the desired state of Plan +type PlanSpec struct { + Stages map[string]Stage `json:"stages,omitempty"` +} + +type HealthType string + +const ( + Healthy HealthType = "Healthy" + Unhealthy HealthType = "Unhealthy" +) + +type ResourceStatus struct { + Group string `json:"group,omitempty"` + Version string `json:"version,omitempty"` + //+kubebuilder:validation:Required + Kind string `json:"kind"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Health HealthType `json:"health"` +} + +// StageStatus captures the status of a stage +type StageStatus struct { + ResourceCount int `json:"resourceCount"` + AppliedCount int `json:"appliedCount,omitempty"` + LastApplied []ResourceStatus `json:"lastApplied,omitempty"` +} + +// PlanStatus defines the observed state of Plan +type PlanStatus struct { + // Facade's generation last successfully reconciled + InputGeneration int64 `json:"inputGeneration"` + // Composition generation last successfully reconciled + CompositionGeneration int64 `json:"compositionGeneration"` + // Composition UID + CompositionUID types.UID `json:"compositionUID,omitempty"` + + // Plan generation we last successfully reconciled + Generation int64 `json:"generation,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + Stages map[string]*StageStatus `json:"stages,omitempty"` + LastPruned []ResourceStatus `json:"lastPruned,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Plan is the Schema for the plans API +type Plan struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PlanSpec `json:"spec,omitempty"` + Status PlanStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// PlanList contains a list of Plan +type PlanList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Plan `json:"items"` +} + +// Status helpers +func (s *PlanStatus) ClearCondition(condition ConditionType) { + meta.RemoveStatusCondition(&s.Conditions, string(condition)) +} + +func (s *PlanStatus) AppendCondition(t ConditionType, sts metav1.ConditionStatus, m, r string) { + s.Conditions = append(s.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: m, + Reason: r, + Type: string(t), + Status: sts, + }) +} + +func (s *PlanStatus) AppendWaitingCondition(e, m, r string) { + message := fmt.Sprintf("Expander: %s, Message: %s", e, m) + s.AppendCondition(Waiting, metav1.ConditionTrue, message, r) +} + +func (s *PlanStatus) AppendErrorCondition(e, m, r string) { + message := fmt.Sprintf("Expander: %s, Message: %s", e, m) + s.AppendCondition(Error, metav1.ConditionTrue, message, r) +} + +func init() { + SchemeBuilder.Register(&Plan{}, &PlanList{}) +} diff --git a/composition/api/v1alpha1/zz_generated.deepcopy.go b/composition/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..ee72965 --- /dev/null +++ b/composition/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,910 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024. + +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 ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Composition) DeepCopyInto(out *Composition) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Composition. +func (in *Composition) DeepCopy() *Composition { + if in == nil { + return nil + } + out := new(Composition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Composition) 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 *CompositionList) DeepCopyInto(out *CompositionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Composition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionList. +func (in *CompositionList) DeepCopy() *CompositionList { + if in == nil { + return nil + } + out := new(CompositionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompositionList) 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 *CompositionSpec) DeepCopyInto(out *CompositionSpec) { + *out = *in + if in.Expanders != nil { + in, out := &in.Expanders, &out.Expanders + *out = make([]Expander, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Readiness != nil { + in, out := &in.Readiness, &out.Readiness + *out = make([]ReadyOn, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionSpec. +func (in *CompositionSpec) DeepCopy() *CompositionSpec { + if in == nil { + return nil + } + out := new(CompositionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositionStatus) DeepCopyInto(out *CompositionStatus) { + *out = *in + 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]) + } + } + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make(map[string]StageValidationStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionStatus. +func (in *CompositionStatus) DeepCopy() *CompositionStatus { + if in == nil { + return nil + } + out := new(CompositionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigReference) DeepCopyInto(out *ConfigReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigReference. +func (in *ConfigReference) DeepCopy() *ConfigReference { + if in == nil { + return nil + } + out := new(ConfigReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Context) DeepCopyInto(out *Context) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Context. +func (in *Context) DeepCopy() *Context { + if in == nil { + return nil + } + out := new(Context) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Context) 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 *ContextList) DeepCopyInto(out *ContextList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Context, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContextList. +func (in *ContextList) DeepCopy() *ContextList { + if in == nil { + return nil + } + out := new(ContextList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ContextList) 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 *ContextSpec) DeepCopyInto(out *ContextSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContextSpec. +func (in *ContextSpec) DeepCopy() *ContextSpec { + if in == nil { + return nil + } + out := new(ContextSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContextStatus) DeepCopyInto(out *ContextStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContextStatus. +func (in *ContextStatus) DeepCopy() *ContextStatus { + if in == nil { + return nil + } + out := new(ContextStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Expander) DeepCopyInto(out *Expander) { + *out = *in + in.ExpanderConfig.DeepCopyInto(&out.ExpanderConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Expander. +func (in *Expander) DeepCopy() *Expander { + if in == nil { + return nil + } + out := new(Expander) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExpanderConfig) DeepCopyInto(out *ExpanderConfig) { + *out = *in + if in.Jinja2 != nil { + in, out := &in.Jinja2, &out.Jinja2 + *out = new(Jinja2) + **out = **in + } + if in.ConfigRef != nil { + in, out := &in.ConfigRef, &out.ConfigRef + *out = new(ConfigReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpanderConfig. +func (in *ExpanderConfig) DeepCopy() *ExpanderConfig { + if in == nil { + return nil + } + out := new(ExpanderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExpanderConfigGVK) DeepCopyInto(out *ExpanderConfigGVK) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpanderConfigGVK. +func (in *ExpanderConfigGVK) DeepCopy() *ExpanderConfigGVK { + if in == nil { + return nil + } + out := new(ExpanderConfigGVK) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExpanderVersion) DeepCopyInto(out *ExpanderVersion) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpanderVersion. +func (in *ExpanderVersion) DeepCopy() *ExpanderVersion { + if in == nil { + return nil + } + out := new(ExpanderVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExpanderVersion) 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 *ExpanderVersionList) DeepCopyInto(out *ExpanderVersionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ExpanderVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpanderVersionList. +func (in *ExpanderVersionList) DeepCopy() *ExpanderVersionList { + if in == nil { + return nil + } + out := new(ExpanderVersionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExpanderVersionList) 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 *ExpanderVersionSpec) DeepCopyInto(out *ExpanderVersionSpec) { + *out = *in + if in.ValidVersions != nil { + in, out := &in.ValidVersions, &out.ValidVersions + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Config = in.Config +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpanderVersionSpec. +func (in *ExpanderVersionSpec) DeepCopy() *ExpanderVersionSpec { + if in == nil { + return nil + } + out := new(ExpanderVersionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExpanderVersionStatus) DeepCopyInto(out *ExpanderVersionStatus) { + *out = *in + if in.VersionMap != nil { + in, out := &in.VersionMap, &out.VersionMap + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + 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 ExpanderVersionStatus. +func (in *ExpanderVersionStatus) DeepCopy() *ExpanderVersionStatus { + if in == nil { + return nil + } + out := new(ExpanderVersionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Facade) DeepCopyInto(out *Facade) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Facade. +func (in *Facade) DeepCopy() *Facade { + if in == nil { + return nil + } + out := new(Facade) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Facade) 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 *FacadeList) DeepCopyInto(out *FacadeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Facade, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FacadeList. +func (in *FacadeList) DeepCopy() *FacadeList { + if in == nil { + return nil + } + out := new(FacadeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FacadeList) 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 *FacadeSpec) DeepCopyInto(out *FacadeSpec) { + *out = *in + if in.OpenAPIV3Schema != nil { + in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema + *out = (*in).DeepCopy() + } + if in.SimpleSchema != nil { + in, out := &in.SimpleSchema, &out.SimpleSchema + *out = new(SimpleSchema) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FacadeSpec. +func (in *FacadeSpec) DeepCopy() *FacadeSpec { + if in == nil { + return nil + } + out := new(FacadeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FacadeStatus) DeepCopyInto(out *FacadeStatus) { + *out = *in + 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 FacadeStatus. +func (in *FacadeStatus) DeepCopy() *FacadeStatus { + if in == nil { + return nil + } + out := new(FacadeStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FieldRef) DeepCopyInto(out *FieldRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldRef. +func (in *FieldRef) DeepCopy() *FieldRef { + if in == nil { + return nil + } + out := new(FieldRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GetterConfiguration) DeepCopyInto(out *GetterConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GetterConfiguration. +func (in *GetterConfiguration) DeepCopy() *GetterConfiguration { + if in == nil { + return nil + } + out := new(GetterConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GetterConfiguration) 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 *GetterConfigurationList) DeepCopyInto(out *GetterConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GetterConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GetterConfigurationList. +func (in *GetterConfigurationList) DeepCopy() *GetterConfigurationList { + if in == nil { + return nil + } + out := new(GetterConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GetterConfigurationList) 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 *GetterConfigurationSpec) DeepCopyInto(out *GetterConfigurationSpec) { + *out = *in + if in.ValuesFrom != nil { + in, out := &in.ValuesFrom, &out.ValuesFrom + *out = make([]ValuesFrom, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GetterConfigurationSpec. +func (in *GetterConfigurationSpec) DeepCopy() *GetterConfigurationSpec { + if in == nil { + return nil + } + out := new(GetterConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GetterConfigurationStatus) DeepCopyInto(out *GetterConfigurationStatus) { + *out = *in + 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 GetterConfigurationStatus. +func (in *GetterConfigurationStatus) DeepCopy() *GetterConfigurationStatus { + if in == nil { + return nil + } + out := new(GetterConfigurationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Jinja2) DeepCopyInto(out *Jinja2) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Jinja2. +func (in *Jinja2) DeepCopy() *Jinja2 { + if in == nil { + return nil + } + out := new(Jinja2) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Plan) DeepCopyInto(out *Plan) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plan. +func (in *Plan) DeepCopy() *Plan { + if in == nil { + return nil + } + out := new(Plan) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Plan) 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 *PlanList) DeepCopyInto(out *PlanList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Plan, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanList. +func (in *PlanList) DeepCopy() *PlanList { + if in == nil { + return nil + } + out := new(PlanList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlanList) 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 *PlanSpec) DeepCopyInto(out *PlanSpec) { + *out = *in + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make(map[string]Stage, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSpec. +func (in *PlanSpec) DeepCopy() *PlanSpec { + if in == nil { + return nil + } + out := new(PlanSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlanStatus) DeepCopyInto(out *PlanStatus) { + *out = *in + 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]) + } + } + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make(map[string]*StageStatus, len(*in)) + for key, val := range *in { + var outVal *StageStatus + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(StageStatus) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + if in.LastPruned != nil { + in, out := &in.LastPruned, &out.LastPruned + *out = make([]ResourceStatus, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanStatus. +func (in *PlanStatus) DeepCopy() *PlanStatus { + if in == nil { + return nil + } + out := new(PlanStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReadyOn) DeepCopyInto(out *ReadyOn) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadyOn. +func (in *ReadyOn) DeepCopy() *ReadyOn { + if in == nil { + return nil + } + out := new(ReadyOn) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceRef) DeepCopyInto(out *ResourceRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRef. +func (in *ResourceRef) DeepCopy() *ResourceRef { + if in == nil { + return nil + } + out := new(ResourceRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus. +func (in *ResourceStatus) DeepCopy() *ResourceStatus { + if in == nil { + return nil + } + out := new(ResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SimpleSchema) DeepCopyInto(out *SimpleSchema) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + if in.Required != nil { + in, out := &in.Required, &out.Required + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SimpleSchema. +func (in *SimpleSchema) DeepCopy() *SimpleSchema { + if in == nil { + return nil + } + out := new(SimpleSchema) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Stage) DeepCopyInto(out *Stage) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stage. +func (in *Stage) DeepCopy() *Stage { + if in == nil { + return nil + } + out := new(Stage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StageStatus) DeepCopyInto(out *StageStatus) { + *out = *in + if in.LastApplied != nil { + in, out := &in.LastApplied, &out.LastApplied + *out = make([]ResourceStatus, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageStatus. +func (in *StageStatus) DeepCopy() *StageStatus { + if in == nil { + return nil + } + out := new(StageStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StageValidationStatus) DeepCopyInto(out *StageValidationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageValidationStatus. +func (in *StageValidationStatus) DeepCopy() *StageValidationStatus { + if in == nil { + return nil + } + out := new(StageValidationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValuesFrom) DeepCopyInto(out *ValuesFrom) { + *out = *in + out.ResourceRef = in.ResourceRef + if in.FieldRef != nil { + in, out := &in.FieldRef, &out.FieldRef + *out = make([]FieldRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValuesFrom. +func (in *ValuesFrom) DeepCopy() *ValuesFrom { + if in == nil { + return nil + } + out := new(ValuesFrom) + in.DeepCopyInto(out) + return out +} diff --git a/composition/cmd/inline/main.go b/composition/cmd/inline/main.go new file mode 100644 index 0000000..9c86791 --- /dev/null +++ b/composition/cmd/inline/main.go @@ -0,0 +1,335 @@ +// Copyright 2024 Google LLC +// +// 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 ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "tailscale.com/atomicfile" +) + +var planGVR schema.GroupVersionResource = schema.GroupVersionResource{ + Group: "composition.google.com", + Version: "v1alpha1", + Resource: "plans", +} + +var compositionGVR schema.GroupVersionResource = schema.GroupVersionResource{ + Group: "composition.google.com", + Version: "v1alpha1", + Resource: "compositions", +} + +var contextGVR schema.GroupVersionResource = schema.GroupVersionResource{ + Group: "composition.google.com", + Version: "v1alpha1", + Resource: "contexts", +} + +type InlineSyncer struct { + dynamicClient dynamic.Interface + ctx context.Context + gvr schema.GroupVersionResource + name string + namespace string + templateName string + expanderName string + planName string + path string + timeout int +} + +func (s *InlineSyncer) useConfig(config *rest.Config) error { + var err error + s.dynamicClient, err = dynamic.NewForConfig(config) + return err +} + +func (s *InlineSyncer) getObject(namespace, name string, + gvr schema.GroupVersionResource) (*unstructured.Unstructured, error) { + if namespace != "" { + return s.dynamicClient.Resource(gvr).Namespace(namespace).Get(s.ctx, name, metav1.GetOptions{}) + } else { + return s.dynamicClient.Resource(gvr).Namespace("default").Get(s.ctx, name, metav1.GetOptions{}) + } + +} + +func (s *InlineSyncer) CopyToFileSystem() error { + // TODO (barni@): assuming name is fixed + contextObj, err := s.getObject(s.namespace, "context", contextGVR) + if err != nil { + return fmt.Errorf("failed to get context object, error: %v", err) + } + crdv, err := s.getObject(s.namespace, s.name, s.gvr) + if err != nil { + return fmt.Errorf("failed to get values object, error: %v", err) + } + crdt, err := s.getObject("", s.templateName, compositionGVR) + if err != nil { + return fmt.Errorf("failed to get template object, error: %v", err) + } + crdp, err := s.getObject(s.namespace, s.planName, planGVR) + if err != nil { + return fmt.Errorf("failed to get plan object, error: %v", err) + } + + // Get crdt.spec.expanders[.name=name].template + expanders, found, err := unstructured.NestedSlice(crdt.Object, "spec", "expanders") + if err != nil { + return fmt.Errorf("composition .spec.expanders was not a slice: %v", err) + } + if !found { + return fmt.Errorf("composition .spec.expanders not found") + } + + var template string + for index := range expanders { + if expander, ok := expanders[index].(map[string]interface{}); ok { + if name, ok := expander["name"].(string); ok { + if name != s.expanderName { + continue + } + if tmpl, ok := expander["template"].(string); ok { + template = tmpl + break + } + } + } + } + + if template == "" { + return fmt.Errorf("template not found for expander: " + s.expanderName) + } + + // Get crdp.spec.stages[name] + stages, found, err := unstructured.NestedMap(crdp.Object, "spec", "stages") + if err != nil { + return fmt.Errorf("Plan CR .spec.stages was not a map: %v", err) + } + if !found { + klog.Infof("Input API .spec.stages not found") + stages = map[string]interface{}{} + } + // Get Values for the expander if present in crdv + jsonString := "" + if _, ok := stages[s.expanderName]; ok { + if stage, ok := stages[s.expanderName].(map[string]interface{}); ok { + if v, ok := stage["values"].(string); ok { + jsonString = v + klog.Infof("values present: %s", jsonString) + } + } + } + + expanderValues := map[string]interface{}{} + if jsonString != "" { + // Unmarshal the JSON string into the map + err := json.Unmarshal([]byte(jsonString), &expanderValues) + if err != nil { + return fmt.Errorf("Error unmarshalling expander jsonValues: %v", err) + } + } + + // Construct the values object from crdv and context + valuesObj := map[string]interface{}{ + "context": contextObj.Object, + s.gvr.Resource: crdv.Object, + "values": expanderValues, + } + + // marshall values + values, err := json.Marshal(valuesObj) + if err != nil { + return fmt.Errorf("failed to marshal values: %v", err) + } + + // Write values to file + // https://github.com/golang/go/issues/56174 + err = atomicfile.WriteFile(s.path+"/values", values, 0644) + if err != nil { + return fmt.Errorf("failed to write values to file: %v", err) + } + + // Write template to file + err = atomicfile.WriteFile(s.path+"/template", []byte(template), 0644) + if err != nil { + return fmt.Errorf("failed to write template to file: %v", err) + } + + return nil +} + +func (s *InlineSyncer) CopyFromFileSystem() error { + crdp, err := s.getObject(s.namespace, s.planName, planGVR) + if err != nil { + return fmt.Errorf("failed to get Plan object, error: %v", err) + } + + // wait for /expanded/expanded to be created and then read it + for i := 0; i < s.timeout; i++ { + if _, err := os.Stat(s.path + "/expanded"); err == nil { + break + } + // sleep for 1 second + time.Sleep(time.Duration(1) * time.Second) + } + + manifests, err := os.ReadFile(s.path + "/expanded") + if err != nil { + return fmt.Errorf("failed to read expanded file, error: %v", err) + } + + stageObj := map[string]interface{}{ + "manifest": string(manifests), + } + // Get crdp.spec.stages[name] + stages, found, err := unstructured.NestedMap(crdp.Object, "spec", "stages") + if err != nil { + return fmt.Errorf("Plan CR .spec.stages was not a map: %v", err) + } + if !found { + klog.Infof("Input API .spec.stages not found") + stages = map[string]interface{}{} + } + + // Get Values for the stage if present in crdp + if _, ok := stages[s.expanderName]; ok { + if stage, ok := stages[s.expanderName].(map[string]interface{}); ok { + if v, ok := stage["values"].(string); ok { + stageObj["values"] = v + } + } + } + + stages[s.expanderName] = stageObj + err = unstructured.SetNestedMap(crdp.Object, stages, "spec", "stages") + if err != nil { + return fmt.Errorf("failed to set .spec.stages field, error: %v", err) + } + + // Using dynamic client update the crdp + _, err = s.dynamicClient.Resource(planGVR).Namespace(s.namespace).Update(s.ctx, crdp, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update Plan CR: %v", err) + } + return nil +} + +func main() { + group := flag.String("group", "", "") + version := flag.String("version", "", "") + resource := flag.String("resource", "", "") + namespace := flag.String("namespace", "", "values object namespace") + name := flag.String("name", "", "values object name") + templateName := flag.String("template", "", "Composition object name") + planName := flag.String("plan", "", "Plan object name") + expanderName := flag.String("expander", "", "Composition expander name") + timeout := flag.Int("timeout", 60, "timeout waiting for expanded file") + + path := flag.String("path", "", "path for manifests") + stage := flag.String("stage", "", "allowed values: beforeExpansion|afterExpansion") + flag.Parse() + + if *namespace == "" { + klog.Fatalf("namespace is required") + } + + if *name == "" { + klog.Fatalf("name is required") + } + + if *path == "" { + klog.Fatalf("path is required") + } + + if *resource == "" { + klog.Fatalf("resource is required") + } + + if *group == "" { + klog.Fatalf("group is required") + } + + if *version == "" { + klog.Fatalf("version is required") + } + + if *stage != "beforeExpansion" && *stage != "afterExpansion" { + klog.Fatalf("allowed stages: beforeExpansion|afterExpansion") + } + + if *templateName == "" { + klog.Fatalf("template is required") + } + + if *expanderName == "" { + klog.Fatalf("expander is required") + } + + if *planName == "" { + klog.Fatalf("plan is required") + } + + klog.Infof("template: %s, stage: %s, plan:%s", *templateName, *stage, *planName) + klog.Infof("group: %s, version: %s, resource: %s, namespace: %s, name: %s", + *group, *version, *resource, *namespace, *name) + klog.Infof("path: %s", *path) + + syncer := &InlineSyncer{ + ctx: context.Background(), + } + + syncer.gvr = schema.GroupVersionResource{Group: *group, Version: *version, Resource: *resource} + syncer.path = *path + syncer.templateName = *templateName + syncer.expanderName = *expanderName + syncer.planName = *planName + syncer.namespace = *namespace + syncer.name = *name + syncer.timeout = *timeout + + // Get dynamic client. + + config := ctrl.GetConfigOrDie() + err := syncer.useConfig(config) + if err != nil { + klog.Fatalf("failed to get dynamic client error: %v", err) + } + + if *stage == "beforeExpansion" { + err := syncer.CopyToFileSystem() + if err != nil { + klog.Fatalf("failed to copy template to file system, error: %v", err) + } + } else { + err := syncer.CopyFromFileSystem() + if err != nil { + klog.Fatalf("failed to copy template from file system, error: %v", err) + } + } +} diff --git a/composition/cmd/main.go b/composition/cmd/main.go new file mode 100644 index 0000000..036226e --- /dev/null +++ b/composition/cmd/main.go @@ -0,0 +1,157 @@ +/* +Copyright 2024. + +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" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/composition/internal/controller" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(compositionv1alpha1.AddToScheme(scheme)) + utilruntime.Must(extv1.AddToScheme(scheme)) + utilruntime.Must(apiextensions.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{BindAddress: metricsAddr}, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "60e39cac.google.com", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = (&controller.CompositionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("composition"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Composition") + os.Exit(1) + } + if err = (&controller.ContextReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Context") + os.Exit(1) + } + if err = (&controller.PlanReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Plan") + os.Exit(1) + } + if err = (&controller.FacadeReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Facade") + os.Exit(1) + } + if err = (&controller.ExpanderVersionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ExpanderVersion") + os.Exit(1) + } + if err = (&controller.GetterConfigurationReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "GetterConfiguration") + 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 the composition manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/composition/config/crd/bases/composition.google.com_compositions.yaml b/composition/config/crd/bases/composition.google.com_compositions.yaml new file mode 100644 index 0000000..6741610 --- /dev/null +++ b/composition/config/crd/bases/composition.google.com_compositions.yaml @@ -0,0 +1,232 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: compositions.composition.google.com +spec: + group: composition.google.com + names: + kind: Composition + listKind: CompositionList + plural: compositions + singular: composition + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Composition is the Schema for the compositions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: CompositionSpec defines the desired state of Composition + properties: + description: + type: string + expanders: + items: + properties: + configref: + description: ConfigReference - For BYO Expanders, we can extend + it + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + jinja2: + description: Built in expanders + properties: + template: + type: string + required: + - template + type: object + name: + type: string + template: + description: For BYO Expanders use generic template or ref for + external config + type: string + type: + default: jinja2 + description: |- + Type indicates what expander to use + jinja - jinja2 expander + ... + type: string + version: + default: latest + type: string + required: + - name + - type + type: object + minItems: 1 + type: array + inputAPIGroup: + description: Use existing KRM API + type: string + namespaceMode: + description: |- + Namespace mode indicates how compositions set the namespace of the objects from expanders. + ""|inherit implies inherit the facade api's namespace. Only namespaced objects are allowed. + explicit implies the objects in the template must have the namespace set. + enum: + - inherit + - explicit + type: string + readiness: + description: Readiness + items: + description: ReadyOn defines ready condition for a GVK + properties: + group: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + readyIf: + type: string + version: + type: string + required: + - group + - kind + - readyIf + type: object + type: array + required: + - expanders + type: object + status: + description: CompositionStatus defines the observed state of Composition + properties: + conditions: + 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 + generation: + format: int64 + type: integer + stages: + additionalProperties: + description: StageStatus captures the status of a stage + properties: + message: + type: string + reason: + type: string + validationStatus: + type: string + type: object + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/config/crd/bases/composition.google.com_contexts.yaml b/composition/config/crd/bases/composition.google.com_contexts.yaml new file mode 100644 index 0000000..e187842 --- /dev/null +++ b/composition/config/crd/bases/composition.google.com_contexts.yaml @@ -0,0 +1,69 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: contexts.composition.google.com +spec: + group: composition.google.com + names: + kind: Context + listKind: ContextList + plural: contexts + singular: context + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + Context is used to set a set of common parameters per namespace. + Currently it includes a string that identifies a cloud project + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ContextSpec defines the desired state of Context + properties: + project: + description: Project is passed to the expander. + type: string + type: object + status: + description: ContextStatus defines the observed state of Context + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/config/crd/bases/composition.google.com_expanderversions.yaml b/composition/config/crd/bases/composition.google.com_expanderversions.yaml new file mode 100644 index 0000000..8540cef --- /dev/null +++ b/composition/config/crd/bases/composition.google.com_expanderversions.yaml @@ -0,0 +1,180 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: expanderversions.composition.google.com +spec: + group: composition.google.com + names: + kind: ExpanderVersion + listKind: ExpanderVersionList + plural: expanderversions + singular: expanderversion + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ExpanderVersion is the Schema for the expanderversions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExpanderVersionSpec defines the desired state of ExpanderVersion + properties: + config: + description: ExpanderConfig GVK + properties: + group: + type: string + kind: + type: string + version: + type: string + required: + - group + - kind + - version + type: object + image: + description: Image if different from removePrefix(expanderversion.name + , "composition-") + type: string + imageRegistry: + description: ImageRegistry is the designated registry for where to + pull the named expander image + type: string + type: + default: job + description: |- + Type indicates what sort of expander: + job - job based expander. ephemeral + grpc - grpc service expander. persistent + enum: + - job + - grpc + type: string + validVersions: + description: ValidVersions is a list of valid versions of the named + expander + items: + type: string + type: array + required: + - config + - type + - validVersions + type: object + status: + description: ExpanderVersionStatus defines the observed state of ExpanderVersion + properties: + conditions: + 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 + versionMap: + additionalProperties: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/config/crd/bases/composition.google.com_facades.yaml b/composition/config/crd/bases/composition.google.com_facades.yaml new file mode 100644 index 0000000..064faab --- /dev/null +++ b/composition/config/crd/bases/composition.google.com_facades.yaml @@ -0,0 +1,159 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: facades.composition.google.com +spec: + group: composition.google.com + names: + kind: Facade + listKind: FacadeList + plural: facades + singular: facade + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Facade is the Schema for the facades API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FacadeSpec defines the desired state of Facade + properties: + facadeKind: + type: string + openAPIV3Schema: + description: Bring your own open API spec + type: object + x-kubernetes-preserve-unknown-fields: true + simpleSchema: + description: Simple Schema + properties: + required: + items: + type: string + type: array + spec: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - spec + - status + type: object + required: + - facadeKind + type: object + status: + description: FacadeStatus defines the observed state of Facade + properties: + conditions: + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/config/crd/bases/composition.google.com_getterconfigurations.yaml b/composition/config/crd/bases/composition.google.com_getterconfigurations.yaml new file mode 100644 index 0000000..174d874 --- /dev/null +++ b/composition/config/crd/bases/composition.google.com_getterconfigurations.yaml @@ -0,0 +1,177 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: getterconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: GetterConfiguration + listKind: GetterConfigurationList + plural: getterconfigurations + singular: getterconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GetterConfiguration is the Schema for the getters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: GetterConfigurationSpec defines the desired state of GetterConfiguration + properties: + valuesFrom: + items: + properties: + fieldRef: + items: + properties: + as: + type: string + path: + type: string + required: + - as + - path + type: object + type: array + name: + type: string + resourceRef: + properties: + group: + description: OPTION 2 + type: string + kind: + type: string + name: + description: OneOf validation added for Name and NameSuffix + type: string + nameSuffix: + type: string + resource: + type: string + version: + type: string + required: + - kind + - resource + type: object + required: + - fieldRef + - name + - resourceRef + type: object + type: array + type: object + status: + description: GetterConfigurationStatus defines the observed state of GetterConfiguration + properties: + conditions: + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/config/crd/bases/composition.google.com_plans.yaml b/composition/config/crd/bases/composition.google.com_plans.yaml new file mode 100644 index 0000000..176599e --- /dev/null +++ b/composition/config/crd/bases/composition.google.com_plans.yaml @@ -0,0 +1,217 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: plans.composition.google.com +spec: + group: composition.google.com + names: + kind: Plan + listKind: PlanList + plural: plans + singular: plan + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Plan is the Schema for the plans API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PlanSpec defines the desired state of Plan + properties: + stages: + additionalProperties: + properties: + manifest: + type: string + values: + type: string + type: object + type: object + type: object + status: + description: PlanStatus defines the observed state of Plan + properties: + compositionGeneration: + description: Composition generation last successfully reconciled + format: int64 + type: integer + compositionUID: + description: Composition UID + type: string + conditions: + 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 + generation: + description: Plan generation we last successfully reconciled + format: int64 + type: integer + inputGeneration: + description: Facade's generation last successfully reconciled + format: int64 + type: integer + lastPruned: + items: + properties: + group: + type: string + health: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + status: + type: string + version: + type: string + required: + - health + - kind + type: object + type: array + stages: + additionalProperties: + description: StageStatus captures the status of a stage + properties: + appliedCount: + type: integer + lastApplied: + items: + properties: + group: + type: string + health: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + status: + type: string + version: + type: string + required: + - health + - kind + type: object + type: array + resourceCount: + type: integer + required: + - resourceCount + type: object + type: object + required: + - compositionGeneration + - inputGeneration + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/config/crd/kustomization.yaml b/composition/config/crd/kustomization.yaml new file mode 100644 index 0000000..904d50d --- /dev/null +++ b/composition/config/crd/kustomization.yaml @@ -0,0 +1,51 @@ +# Copyright 2024 Google LLC +# +# 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. + +# 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/composition.google.com_compositions.yaml +- bases/composition.google.com_contexts.yaml +- bases/composition.google.com_plans.yaml +- bases/composition.google.com_facades.yaml +- bases/composition.google.com_expanderversions.yaml +- bases/composition.google.com_getterconfigurations.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- path: patches/webhook_in_compositions.yaml +#- path: patches/webhook_in_contexts.yaml +#- path: patches/webhook_in_plans.yaml +#- path: patches/webhook_in_facades.yaml +#- path: patches/webhook_in_getterconfigurations.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 +#- path: patches/cainjection_in_compositions.yaml +#- path: patches/cainjection_in_contexts.yaml +#- path: patches/cainjection_in_plans.yaml +#- path: patches/cainjection_in_facades.yaml +#- path: patches/cainjection_in_expanderversions.yaml +#- path: patches/cainjection_in_getterconfigurations.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +#configurations: +#- kustomizeconfig.yaml diff --git a/composition/config/crd/kustomizeconfig.yaml b/composition/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..a2d0584 --- /dev/null +++ b/composition/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# 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. + +# 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/composition/config/default/kustomization.yaml b/composition/config/default/kustomization.yaml new file mode 100644 index 0000000..cd2083e --- /dev/null +++ b/composition/config/default/kustomization.yaml @@ -0,0 +1,161 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Adds namespace to all resources. +namespace: composition-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: composition- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +- ../expanders +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true + + +patches: +- path: manager_auth_proxy_patch.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/composition/config/default/manager_auth_proxy_patch.yaml b/composition/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000..a4e64d5 --- /dev/null +++ b/composition/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 Google LLC +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 5m + memory: 128Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/composition/config/default/manager_config_patch.yaml b/composition/config/default/manager_config_patch.yaml new file mode 100644 index 0000000..d2581c2 --- /dev/null +++ b/composition/config/default/manager_config_patch.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/composition/config/expanders/expander_versions.yaml b/composition/config/expanders/expander_versions.yaml new file mode 100644 index 0000000..198472e --- /dev/null +++ b/composition/config/expanders/expander_versions.yaml @@ -0,0 +1,60 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: jinja2-job + namespace: system +spec: + type: job + imageRegistry: gcr.io/krmapihosting-release + image: expander-jinja2 + validVersions: + # current golang semver package only support version comparison + # in the format of `v{num}.{num}.{num}-{alphabet}`, `v{num}.{num}.{num}` and `{num}.{num}.{num}`. + # Removing the `.alpha` here for now. Will be added back in the test. + - v0.0.115 + - v0.0.114 + - v0.0.113 + - v0.0.112 + - v0.0.111 + - v0.0.110 + - v0.0.1 + - v0.0.0 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: jinja2 + namespace: system +spec: + type: grpc + validVersions: + - v0.0.1 + - v0.0.0 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: getter + namespace: system +spec: + type: grpc + validVersions: + - v0.0.1 + config: + group: composition.google.com + version: v1alpha1 + kind: GetterConfiguration \ No newline at end of file diff --git a/composition/config/expanders/getter-v0.0.1.yaml b/composition/config/expanders/getter-v0.0.1.yaml new file mode 100644 index 0000000..02d8c22 --- /dev/null +++ b/composition/config/expanders/getter-v0.0.1.yaml @@ -0,0 +1,166 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: getter-expander-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: getter-expander + namespace: system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: getter-expander +rules: +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch +- apiGroups: ["*"] + resources: ["*"] + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: getter-expander-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: getter-expander-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: getter-expander +subjects: +- kind: ServiceAccount + name: getter-expander + namespace: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: getter-v0.0.1 + namespace: system + labels: + control-plane: expander-getter-v0.0.1 + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: getter-v0.0.1 + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: expander-getter-v0.0.1 + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-getter-v0.0.1 + spec: + # (user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + #securityContext: + # runAsNonRoot: true + # (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: + - /expander + args: + - --port=8443 + image: expander-getter:v0.0.1 + name: getter + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + # (user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + serviceAccountName: getter-expander + terminationGracePeriodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: expander-getter + app.kubernetes.io/created-by: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + app.kubernetes.io/instance: getter-v0.0.1 + app.kubernetes.io/component: expanders + name: getter-v0-0-1 + namespace: system +spec: + # type: LoadBalancer + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-getter-v0.0.1 \ No newline at end of file diff --git a/composition/config/expanders/jinja2-v0.0.1.yaml b/composition/config/expanders/jinja2-v0.0.1.yaml new file mode 100644 index 0000000..e406986 --- /dev/null +++ b/composition/config/expanders/jinja2-v0.0.1.yaml @@ -0,0 +1,131 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jinja2-v0.0.1 + namespace: system + labels: + control-plane: expander-jinja2-v0.0.1 + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: jinja2-v0.0.1 + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: expander-jinja2-v0.0.1 + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-jinja2-v0.0.1 + spec: + # (user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + #securityContext: + # runAsNonRoot: true + # (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: + - /expander + args: + - --port=8443 + image: expander-jinja2:v0.0.1 + name: jinja2 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + # (user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + #serviceAccountName: jinja2-expander + terminationGracePeriodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: expander-jinja2 + app.kubernetes.io/created-by: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + app.kubernetes.io/instance: jinja2-v0.0.1 + app.kubernetes.io/component: expanders + name: jinja2-v0-0-1 + namespace: system +spec: + # type: LoadBalancer + # Useful when testing the expander pod inside a kind cluster + # + # kubectl get nodes -o wide + # NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME + # kind-control-plane Ready control-plane,master 22m v1.21.1 172.17.0.5 Ubuntu 21.04 6.6.15-2rodete2-amd64 containerd://1.5.2 + # ^^^^^^^^ + # kubectl get service -n composition-system composition-jinja2-v0-0-1 + # NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE + # composition-jinja2-v0-0-1 LoadBalancer 10.96.154.242 8443:30066/TCP 19m + # ^^^^ + # + # run tests: + # cd expanders/jinja2 + # go test -v --addr=172.17.0.5:30066 + # + + #type: LoadBalancer + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-jinja2-v0.0.1 \ No newline at end of file diff --git a/composition/config/expanders/kustomization.yaml b/composition/config/expanders/kustomization.yaml new file mode 100644 index 0000000..0a64fd8 --- /dev/null +++ b/composition/config/expanders/kustomization.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# 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. + +resources: +- expander_versions.yaml +- jinja2-v0.0.1.yaml +- getter-v0.0.1.yaml +images: +- name: expander-getter + newName: expander-getter + newTag: latest +- name: expander-jinja2 + newName: expander-jinja2 + newTag: latest +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/composition/config/manager/kustomization.yaml b/composition/config/manager/kustomization.yaml new file mode 100644 index 0000000..7f92945 --- /dev/null +++ b/composition/config/manager/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# 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. + +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: composition + newTag: latest diff --git a/composition/config/manager/manager.yaml b/composition/config/manager/manager.yaml new file mode 100644 index 0000000..275b58b --- /dev/null +++ b/composition/config/manager/manager.yaml @@ -0,0 +1,116 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: composition-controller-manager-namespace + app.kubernetes.io/instance: composition-controller-manager-system + app.kubernetes.io/component: composition-controller-manager + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: composition-controller-manager + app.kubernetes.io/instance: composition-controller-manager + app.kubernetes.io/component: composition-controller-manager + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: composition-controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: composition-controller-manager + spec: + # (user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # (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 + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # (user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/composition/config/prometheus/kustomization.yaml b/composition/config/prometheus/kustomization.yaml new file mode 100644 index 0000000..28c68f4 --- /dev/null +++ b/composition/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright 2024 Google LLC +# +# 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. + +resources: +- monitor.yaml diff --git a/composition/config/prometheus/monitor.yaml b/composition/config/prometheus/monitor.yaml new file mode 100644 index 0000000..a86c699 --- /dev/null +++ b/composition/config/prometheus/monitor.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/composition/config/rbac/auth_proxy_client_clusterrole.yaml b/composition/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 0000000..66ef0cf --- /dev/null +++ b/composition/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/composition/config/rbac/auth_proxy_role.yaml b/composition/config/rbac/auth_proxy_role.yaml new file mode 100644 index 0000000..ef18a47 --- /dev/null +++ b/composition/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,38 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/composition/config/rbac/auth_proxy_role_binding.yaml b/composition/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 0000000..43f7411 --- /dev/null +++ b/composition/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/composition/config/rbac/auth_proxy_service.yaml b/composition/config/rbac/auth_proxy_service.yaml new file mode 100644 index 0000000..33c2239 --- /dev/null +++ b/composition/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,35 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/composition/config/rbac/cloudsql_editor_role.yaml b/composition/config/rbac/cloudsql_editor_role.yaml new file mode 100644 index 0000000..242e2e9 --- /dev/null +++ b/composition/config/rbac/cloudsql_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit cloudsqls. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: cloudsql-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: facade + app.kubernetes.io/part-of: facade + app.kubernetes.io/managed-by: kustomize + name: cloudsql-editor-role +rules: +- apiGroups: + - facade.facade + resources: + - cloudsqls + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - facade.facade + resources: + - cloudsqls/status + verbs: + - get diff --git a/composition/config/rbac/cloudsql_viewer_role.yaml b/composition/config/rbac/cloudsql_viewer_role.yaml new file mode 100644 index 0000000..97ba092 --- /dev/null +++ b/composition/config/rbac/cloudsql_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view cloudsqls. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: cloudsql-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: facade + app.kubernetes.io/part-of: facade + app.kubernetes.io/managed-by: kustomize + name: cloudsql-viewer-role +rules: +- apiGroups: + - facade.facade + resources: + - cloudsqls + verbs: + - get + - list + - watch +- apiGroups: + - facade.facade + resources: + - cloudsqls/status + verbs: + - get diff --git a/composition/config/rbac/composition_editor_role.yaml b/composition/config/rbac/composition_editor_role.yaml new file mode 100644 index 0000000..ad7cfda --- /dev/null +++ b/composition/config/rbac/composition_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit compositions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: composition-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: composition-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - compositions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - compositions/status + verbs: + - get diff --git a/composition/config/rbac/composition_viewer_role.yaml b/composition/config/rbac/composition_viewer_role.yaml new file mode 100644 index 0000000..dc00417 --- /dev/null +++ b/composition/config/rbac/composition_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view compositions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: composition-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: composition-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - compositions + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - compositions/status + verbs: + - get diff --git a/composition/config/rbac/context_editor_role.yaml b/composition/config/rbac/context_editor_role.yaml new file mode 100644 index 0000000..8426c2e --- /dev/null +++ b/composition/config/rbac/context_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit contexts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: context-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: context-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - contexts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - contexts/status + verbs: + - get diff --git a/composition/config/rbac/context_viewer_role.yaml b/composition/config/rbac/context_viewer_role.yaml new file mode 100644 index 0000000..253b0db --- /dev/null +++ b/composition/config/rbac/context_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view contexts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: context-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: context-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - contexts + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - contexts/status + verbs: + - get diff --git a/composition/config/rbac/expanderversion_editor_role.yaml b/composition/config/rbac/expanderversion_editor_role.yaml new file mode 100644 index 0000000..6d8e3d8 --- /dev/null +++ b/composition/config/rbac/expanderversion_editor_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit expanderversions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: composition + app.kubernetes.io/managed-by: kustomize + name: expanderversion-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - expanderversions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - expanderversions/status + verbs: + - get diff --git a/composition/config/rbac/expanderversion_viewer_role.yaml b/composition/config/rbac/expanderversion_viewer_role.yaml new file mode 100644 index 0000000..b753361 --- /dev/null +++ b/composition/config/rbac/expanderversion_viewer_role.yaml @@ -0,0 +1,37 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view expanderversions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: composition + app.kubernetes.io/managed-by: kustomize + name: expanderversion-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - expanderversions + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - expanderversions/status + verbs: + - get diff --git a/composition/config/rbac/facade_editor_role.yaml b/composition/config/rbac/facade_editor_role.yaml new file mode 100644 index 0000000..138bf2c --- /dev/null +++ b/composition/config/rbac/facade_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit facades. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: facade-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: facade-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - facades + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - facades/status + verbs: + - get diff --git a/composition/config/rbac/facade_viewer_role.yaml b/composition/config/rbac/facade_viewer_role.yaml new file mode 100644 index 0000000..971b337 --- /dev/null +++ b/composition/config/rbac/facade_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view facades. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: facade-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: facade-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - facades + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - facades/status + verbs: + - get diff --git a/composition/config/rbac/getterconfiguration_editor_role.yaml b/composition/config/rbac/getterconfiguration_editor_role.yaml new file mode 100644 index 0000000..b38fca5 --- /dev/null +++ b/composition/config/rbac/getterconfiguration_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit getterconfigurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: getterconfiguration-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: getterconfiguration-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - getterconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - getterconfigurations/status + verbs: + - get diff --git a/composition/config/rbac/getterconfiguration_viewer_role.yaml b/composition/config/rbac/getterconfiguration_viewer_role.yaml new file mode 100644 index 0000000..1f939bb --- /dev/null +++ b/composition/config/rbac/getterconfiguration_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view getterconfigurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: getterconfiguration-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: getterconfiguration-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - getterconfigurations + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - getterconfigurations/status + verbs: + - get diff --git a/composition/config/rbac/kustomization.yaml b/composition/config/rbac/kustomization.yaml new file mode 100644 index 0000000..e41aa59 --- /dev/null +++ b/composition/config/rbac/kustomization.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# 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. + +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 +- leader_election_role.yaml +- leader_election_role_binding.yaml +- cloudsql_editor_role.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- expanderversion_editor_role.yaml +- expanderversion_viewer_role.yaml diff --git a/composition/config/rbac/leader_election_role.yaml b/composition/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..aa3fa48 --- /dev/null +++ b/composition/config/rbac/leader_election_role.yaml @@ -0,0 +1,58 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/composition/config/rbac/leader_election_role_binding.yaml b/composition/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..17d5c1e --- /dev/null +++ b/composition/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/composition/config/rbac/plan_editor_role.yaml b/composition/config/rbac/plan_editor_role.yaml new file mode 100644 index 0000000..d6d08d3 --- /dev/null +++ b/composition/config/rbac/plan_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to edit plans. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: plan-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: plan-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - plans + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - plans/status + verbs: + - get diff --git a/composition/config/rbac/plan_viewer_role.yaml b/composition/config/rbac/plan_viewer_role.yaml new file mode 100644 index 0000000..741c599 --- /dev/null +++ b/composition/config/rbac/plan_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +# permissions for end users to view plans. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: plan-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: plan-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - plans + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - plans/status + verbs: + - get diff --git a/composition/config/rbac/role.yaml b/composition/config/rbac/role.yaml new file mode 100644 index 0000000..6b7c22b --- /dev/null +++ b/composition/config/rbac/role.yaml @@ -0,0 +1,272 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - patch +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - get + - list + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch +- apiGroups: + - composition.google.com + resources: + - compositions + - contexts + - expanderversions + - facades + - getterconfigurations + - plans + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - compositions/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - compositions/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - contexts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - contexts/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - contexts/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - expanderversions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - expanderversions/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - expanderversions/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - facades + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - facades/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - facades/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - getterconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - getterconfigurations/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - getterconfigurations/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - plans + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - plans/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - plans/status + verbs: + - get + - patch + - update +- apiGroups: + - facade.compositions.google.com + resources: + - '*' + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - facade.compositions.google.com + resources: + - '*/status' + verbs: + - get + - update +- apiGroups: + - facade.facade + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch diff --git a/composition/config/rbac/role_binding.yaml b/composition/config/rbac/role_binding.yaml new file mode 100644 index 0000000..bb1d94d --- /dev/null +++ b/composition/config/rbac/role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/composition/config/rbac/service_account.yaml b/composition/config/rbac/service_account.yaml new file mode 100644 index 0000000..53d2c67 --- /dev/null +++ b/composition/config/rbac/service_account.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/composition/config/release/kustomization.yaml b/composition/config/release/kustomization.yaml new file mode 100644 index 0000000..224b29e --- /dev/null +++ b/composition/config/release/kustomization.yaml @@ -0,0 +1,185 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Adds namespace to all resources. +namespace: composition-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: composition- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +- ../expanders +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true + + +patches: +- path: manager_auth_proxy_patch.yaml +- patch: '[{"op": "add", "path": "/spec/template/spec/containers/1/imagePullPolicy", + "value": "Always"}]' + target: + kind: Deployment + name: controller-manager + namespace: system +- patch: '[{"op": "replace", "path": "/spec/template/spec/containers/1/image", + "value": "gcr.io/krmapihosting-release/composition:v0.0.406"}]' + target: + kind: Deployment + name: controller-manager + namespace: system +- patch: '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", + "value": "gcr.io/krmapihosting-release/expander-jinja2:v0.0.1"}]' + target: + kind: Deployment + name: jinja2-v0.0.1 + namespace: system +- patch: '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", + "value": "gcr.io/krmapihosting-release/expander-getter:v0.0.1"}]' + target: + kind: Deployment + name: getter-v0.0.1 + namespace: system +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/composition/config/release/manager_auth_proxy_patch.yaml b/composition/config/release/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000..a4e64d5 --- /dev/null +++ b/composition/config/release/manager_auth_proxy_patch.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 Google LLC +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 5m + memory: 128Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/composition/config/release/manager_config_patch.yaml b/composition/config/release/manager_config_patch.yaml new file mode 100644 index 0000000..d2581c2 --- /dev/null +++ b/composition/config/release/manager_config_patch.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/composition/config/samples/composition_v1alpha1_composition.yaml b/composition/config/samples/composition_v1alpha1_composition.yaml new file mode 100644 index 0000000..fcc842d --- /dev/null +++ b/composition/config/samples/composition_v1alpha1_composition.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + labels: + app.kubernetes.io/name: composition + app.kubernetes.io/instance: composition-sample + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: composition + name: composition-sample +spec: + # (user): Add fields here diff --git a/composition/config/samples/composition_v1alpha1_context.yaml b/composition/config/samples/composition_v1alpha1_context.yaml new file mode 100644 index 0000000..163585a --- /dev/null +++ b/composition/config/samples/composition_v1alpha1_context.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + labels: + app.kubernetes.io/name: context + app.kubernetes.io/instance: context-sample + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: composition + name: context-sample +spec: + # TODO(user): Add fields here diff --git a/composition/config/samples/composition_v1alpha1_expanderversion.yaml b/composition/config/samples/composition_v1alpha1_expanderversion.yaml new file mode 100644 index 0000000..b212770 --- /dev/null +++ b/composition/config/samples/composition_v1alpha1_expanderversion.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + labels: + app.kubernetes.io/name: composition + app.kubernetes.io/managed-by: kustomize + name: expanderversion-sample +spec: + # TODO(user): Add fields here diff --git a/composition/config/samples/composition_v1alpha1_facade.yaml b/composition/config/samples/composition_v1alpha1_facade.yaml new file mode 100644 index 0000000..9970d21 --- /dev/null +++ b/composition/config/samples/composition_v1alpha1_facade.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Facade +metadata: + labels: + app.kubernetes.io/name: facade + app.kubernetes.io/instance: facade-sample + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: composition + name: facade-sample +spec: + # TODO(user): Add fields here diff --git a/composition/config/samples/composition_v1alpha1_getterconfiguration.yaml b/composition/config/samples/composition_v1alpha1_getterconfiguration.yaml new file mode 100644 index 0000000..8471ce4 --- /dev/null +++ b/composition/config/samples/composition_v1alpha1_getterconfiguration.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + labels: + app.kubernetes.io/name: getter + app.kubernetes.io/instance: getter-sample + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: composition + name: getter-sample +spec: + # TODO(user): Add fields here diff --git a/composition/config/samples/composition_v1alpha1_plan.yaml b/composition/config/samples/composition_v1alpha1_plan.yaml new file mode 100644 index 0000000..9467d1f --- /dev/null +++ b/composition/config/samples/composition_v1alpha1_plan.yaml @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + labels: + app.kubernetes.io/name: plan + app.kubernetes.io/instance: plan-sample + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: composition + name: plan-sample +spec: + # TODO(user): Add fields here diff --git a/composition/config/samples/kustomization.yaml b/composition/config/samples/kustomization.yaml new file mode 100644 index 0000000..d5ebc24 --- /dev/null +++ b/composition/config/samples/kustomization.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# 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. + +## Append samples of your project ## +resources: +- composition_v1alpha1_composition.yaml +- composition_v1alpha1_context.yaml +- composition_v1alpha1_plan.yaml +- composition_v1alpha1_facade.yaml +- composition_v1alpha1_expanderversion.yaml +- composition_v1alpha1_getterconfigurations.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/composition/dev/tasks/build-protos b/composition/dev/tasks/build-protos new file mode 100755 index 0000000..78e47b3 --- /dev/null +++ b/composition/dev/tasks/build-protos @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Copyright 2024 Google LLC +# +# 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. + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd ${REPO_ROOT}/experiments/compositions/composition + +SRC_DIR=$(pwd) + +mkdir -p .build/bin/ +pushd .build/ +BUILD_DIR=$(pwd) +popd + +# Note: It would be nice to be able use e.g. a docker container here +export GOBIN=${BUILD_DIR}/bin/ + +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 + +wget -N -O ${BUILD_DIR}/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v27.1/protoc-27.1-linux-x86_64.zip +cd ${BUILD_DIR}/ +unzip -o protoc.zip + +cd ${SRC_DIR} +PATH=${BUILD_DIR}/bin protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/expander.proto diff --git a/composition/expanders.mk b/composition/expanders.mk new file mode 100644 index 0000000..f8637cb --- /dev/null +++ b/composition/expanders.mk @@ -0,0 +1,94 @@ +## Variables + +###### ----------- Build protos -------------------------------------------- + +##@ Protos + +.PHONY: build-protos +build-protos: + dev/tasks/build-protos + +###### ----------- Expander client command--------------------------- + +##@ Expander expand client command + +.PHONY: build-expand +build-expand: build-protos fmt vet ## Build binary. + $(GOPREFIX) go build -v -o expanders/bin/expand ./expanders/expand + +.PHONY: clean-expand +clean-expand: ## clean binary. + rm -fr expanders/bin/expand + +###### ----------- Jinja2 Expander ---------------------------- + +##@ Jinja2 expander pod + +.PHONY: build-expander-jinja2 +build-expander-jinja2: build-protos fmt vet ## Build binary. + $(GOPREFIX) go build -v -o expanders/bin/jinja2 ./expanders/jinja2 + +.PHONY: clean-expander-jinja2 +clean-expander-jinja2: ## clean binary. + rm -fr expanders/bin/jinja2 + docker rmi ${JINJA_IMG} . + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build-expander-jinja2 +docker-build-expander-jinja2: build-expander-jinja2 ## Build docker image with the manager. + docker build -t ${JINJA_IMG} -f Dockerfile.jinja2.expander . + +.PHONY: docker-push-expander-jinja2 +docker-push-expander-jinja2: ## Push docker image with the manager. + docker push ${JINJA_IMG} + +.PHONY: docker-run-expander-jinja2 +docker-run-expander-jinja2: docker-build-expander-jinja2 + docker run -p 8443:8443 ${JINJA_IMG} + +.PHONY: unit-test-expander-jinja2 +unit-test-expander-jinja2: deploy-kind + kubectl patch service -n composition-system composition-jinja2-v0-0-1 -p '{"spec":{"type":"LoadBalancer"}}' + nodeip=$$(kubectl get nodes -o json | jq '.items[0].status.addresses[0].address' | xargs echo );\ + nodeport=$$(kubectl get service -n composition-system composition-jinja2-v0-0-1 -o json | jq ".spec.ports[0].nodePort");\ + echo $$nodeip:$$nodeport; \ + cd expanders/jinja2 && $(GOPREFIX) go test -v --addr=$$nodeip:$$nodeport + + +###### ----------- Getter Expander ---------------------------- + +##@ Getter expander pod + +.PHONY: build-expander-getter +build-expander-getter: build-protos fmt vet ## Build binary. + $(GOPREFIX) go build -v -o expanders/bin/getter ./expanders/getter + +.PHONY: clean-expander-getter +clean-expander-getter: ## clean binary. + rm -fr expanders/bin/getter + docker rmi ${GETTER_IMG} . + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build-expander-getter +docker-build-expander-getter: build-expander-getter ## Build docker image with the manager. + docker build -t ${GETTER_IMG} -f Dockerfile.getter.expander . + +.PHONY: docker-push-expander-getter +docker-push-expander-getter: ## Push docker image with the manager. + docker push ${GETTER_IMG} + +.PHONY: docker-run-expander-getter +docker-run-expander-getter: docker-build-expander-getter + docker run -p 8443:8443 ${GETTER_IMG} + +.PHONY: unit-test-expander-getter +unit-test-expander-getter: deploy-kind + kubectl patch service -n composition-system composition-getter-v0-0-1 -p '{"spec":{"type":"LoadBalancer"}}' + nodeip=$$(kubectl get nodes -o json | jq '.items[0].status.addresses[0].address' | xargs echo );\ + nodeport=$$(kubectl get service -n composition-system composition-getter-v0-0-1 -o json | jq ".spec.ports[0].nodePort");\ + echo $$nodeip:$$nodeport; \ + cd expanders/getter && $(GOPREFIX) go test -v --addr=$$nodeip:$$nodeport diff --git a/composition/expanders/expand/main.go b/composition/expanders/expand/main.go new file mode 100644 index 0000000..bd8803d --- /dev/null +++ b/composition/expanders/expand/main.go @@ -0,0 +1,76 @@ +// Copyright 2024 Google LLC +// +// 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 ( + "context" + "flag" + "log" + "time" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + addr = flag.String("addr", "[::]:50051", "the address to connect to") + cmd = flag.String("command", "validate", "validate|evaluate") +) + +func main() { + flag.Parse() + // Set up a connection to the server. + conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer func() { + err := conn.Close() + if err != nil { + log.Fatalf("error closing connection: %v", err) + } + }() + c := pb.NewExpanderClient(conn) + + // Contact the server and print out its response. + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if *cmd == "validate" { + r, err := c.Validate(ctx, + &pb.ValidateRequest{ + Config: []byte{}, + Context: []byte{}, + Facade: []byte{}, + Value: []byte{}, + }) + if err != nil { + log.Fatalf("could not validate: %v", err) + } + log.Printf("status: %s", r.GetStatus()) + } else { + r, err := c.Evaluate(ctx, + &pb.EvaluateRequest{ + Config: []byte{}, + Context: []byte{}, + Facade: []byte{}, + Value: []byte{}, + }) + if err != nil { + log.Fatalf("could not evaluate: %v", err) + } + log.Printf("status: %s", r.GetStatus()) + } +} diff --git a/composition/expanders/getter/main.go b/composition/expanders/getter/main.go new file mode 100644 index 0000000..3818a79 --- /dev/null +++ b/composition/expanders/getter/main.go @@ -0,0 +1,264 @@ +// Copyright 2024 Google LLC +// +// 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 ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "strings" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + port = flag.Int("port", 8443, "The server port") +) + +type Getter struct { + req *pb.EvaluateRequest + facade *unstructured.Unstructured + getter *compositionv1alpha1.GetterConfiguration + ctx context.Context + client dynamic.Interface + values map[string]interface{} +} + +func NewGetter(ctx context.Context, client dynamic.Interface, req *pb.EvaluateRequest) *Getter { + return &Getter{ + req: req, + client: client, + ctx: ctx, + values: make(map[string]interface{}), + } +} + +func (g *Getter) LoadInputs() error { + g.getter = &compositionv1alpha1.GetterConfiguration{} + if string(g.req.Config) != "" { + err := json.Unmarshal(g.req.Config, g.getter) + if err != nil { + return fmt.Errorf("error unmarshalling req.Config into GetterConfiguration{}: %w", err) + } + } else { + g.getter.Spec.ValuesFrom = []compositionv1alpha1.ValuesFrom{} + } + + context := &compositionv1alpha1.Context{} + if string(g.req.Context) != "" { + err := json.Unmarshal(g.req.Context, &context) + if err != nil { + return fmt.Errorf("error unmarshalling req.Context: %w", err) + } + } + + g.facade = &unstructured.Unstructured{} + err := g.facade.UnmarshalJSON(g.req.Facade) + if err != nil { + return fmt.Errorf("error unmarshalling req.Facade into Unstructured: %w", err) + } + + if g.facade.GetNamespace() == "" { + return fmt.Errorf("missing namespace in req.Facade object") + } + + getterValues := map[string]interface{}{} + if string(g.req.Value) != "" { + err := json.Unmarshal(g.req.Value, &getterValues) + if err != nil { + return fmt.Errorf("error unmarshalling req.Values: %w", err) + } + } + + return nil +} + +func (g *Getter) updateValues(obj *unstructured.Unstructured, vf *compositionv1alpha1.ValuesFrom) (string, bool) { + wait := false + for index := range vf.FieldRef { + fr := &vf.FieldRef[index] + path := strings.Split(strings.TrimLeft(fr.Path, "."), ".") + identifier := "" + gvk := obj.GroupVersionKind() + if obj.GetNamespace() != "" { + identifier = fmt.Sprintf("%s.%s(%s)/%s/%s[%s]", + gvk.Kind, gvk.Group, gvk.Version, + obj.GetNamespace(), obj.GetName(), fr.Path) + } else { + identifier = fmt.Sprintf("%s.%s(%s)/%s[%s]", + gvk.Kind, gvk.Group, gvk.Version, obj.GetName(), fr.Path) + } + v, ok, err := unstructured.NestedFieldCopy(obj.Object, path...) + if err != nil { + message := fmt.Sprintf("Error traversing field path: %s", identifier) + log.Print(message) + return message, wait + } + if ok { + if g.values[vf.Name] == nil { + g.values[vf.Name] = map[string]interface{}{} + } + g.values[vf.Name].(map[string]interface{})[fr.As] = v + } else { + message := fmt.Sprintf("Field path not present in object yet: %s", identifier) + log.Print(message) + wait = true + return message, wait + } + } + return "", wait +} + +func (g *Getter) getObject(vf *compositionv1alpha1.ValuesFrom, + name string) (*unstructured.Unstructured, string, bool, error) { + failMessage := "" + wait := false + gvr := schema.GroupVersionResource{ + Group: vf.ResourceRef.Group, + Version: vf.ResourceRef.Version, + Resource: vf.ResourceRef.Resource, + } + namespace := g.facade.GetNamespace() + identifier := "" + if namespace != "" { + identifier = fmt.Sprintf("%s.%s(%s)/%s/%s", gvr.Resource, gvr.Group, gvr.Version, namespace, name) + } else { + identifier = fmt.Sprintf("%s.%s(%s)/%s", gvr.Resource, gvr.Group, gvr.Version, name) + } + log.Printf("Fetching :%s", identifier) + obj, err := g.client.Resource(gvr).Namespace(namespace).Get(g.ctx, name, metav1.GetOptions{}) + if err != nil { + if client.IgnoreNotFound(err) != nil { + log.Printf("Error getting dependent object: %s", identifier) + return nil, failMessage, wait, err + } + failMessage = fmt.Sprintf("Dependent object not found: GVR: %s", identifier) + wait := true + return nil, failMessage, wait, nil + } + return obj, failMessage, wait, nil +} + +func (g *Getter) Fetch() (string, bool, error) { + wait := false + message := "" + for index := range g.getter.Spec.ValuesFrom { + vf := &g.getter.Spec.ValuesFrom[index] + name := vf.ResourceRef.Name + if name == "" { + name = g.facade.GetName() + vf.ResourceRef.NameSuffix + } + obj, failMessage, wait, err := g.getObject(vf, name) + if err != nil || failMessage != "" || wait { + return failMessage, wait, err + } + + message, wait = g.updateValues(obj, vf) + if message != "" || wait { + return message, wait, nil + } + } + return "", wait, nil +} + +func (g *Getter) GetValues() map[string]interface{} { + return g.values +} + +// ------------- GRPC Server implementation ---------------- + +// server is used to implement exander.Evaluator +type grpcServer struct { + pb.UnimplementedExpanderServer + dynamicClient dynamic.Interface +} + +// Verify the expander config/template +func (s *grpcServer) Validate(context.Context, *pb.ValidateRequest) (*pb.ValidateResult, error) { + return &pb.ValidateResult{Status: pb.Status_SUCCESS}, nil +} + +// Evaluate the expander config in context of inputs and return manifests +func (s *grpcServer) Evaluate(ctx context.Context, req *pb.EvaluateRequest) (*pb.EvaluateResult, error) { + result := &pb.EvaluateResult{ + Status: pb.Status_SUCCESS, + Type: pb.ResultType_VALUES, + Error: &pb.Error{}, + } + + log.Printf("Evaluate called") + g := NewGetter(ctx, s.dynamicClient, req) + err := g.LoadInputs() + if err != nil { + return nil, err + } + failMessage, wait, err := g.Fetch() + if err != nil { + return nil, err + } + if failMessage != "" { + result.Error.Message = failMessage + if wait { + result.Status = pb.Status_EVALUATE_WAIT + } else { + result.Status = pb.Status_EVALUATE_FAILED + } + return result, nil + } + + values, err := json.Marshal(g.GetValues()) + if err != nil { + log.Printf("Error marshalling extracted values: %v", err) + return nil, fmt.Errorf("Failed to marshal extracted values: %v", err) + } + result.Values = values + return result, nil +} + +// -------------------- MAIN ------------------------------- +func main() { + flag.Parse() + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + + server := &grpcServer{} + + config := ctrl.GetConfigOrDie() + server.dynamicClient, err = dynamic.NewForConfig(config) + if err != nil { + log.Fatalf("failed to get dynamic client error: %v", err) + } + + pb.RegisterExpanderServer(s, server) + log.Printf("server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } + +} diff --git a/composition/expanders/getter/main_test.go b/composition/expanders/getter/main_test.go new file mode 100644 index 0000000..9f40d76 --- /dev/null +++ b/composition/expanders/getter/main_test.go @@ -0,0 +1,484 @@ +// Copyright 2024 Google LLC +// +// 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_test + +import ( + "context" + "encoding/json" + "flag" + "log" + "os" + "strings" + "testing" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + addr = flag.String("addr", "[::]:8443", "the address to connect to") +) + +// Test values +var ( + expanderClient pb.ExpanderClient + + facadeGVK schema.GroupVersionKind = schema.GroupVersionKind{ + Group: "facade.composition.google.com", + Version: "v1", + Kind: "AppTeam", + } + + // Marshalled values + sampleFacade []byte + emptyGetterConfiguration []byte + + // getter objects + emptyGetterConfigurationObj *compositionv1alpha1.GetterConfiguration = &compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{}, + }, + } + + // Evaluate results + emptyGetterConfigurationEvaluateResult = "{}" +) + +// TestMain - umbrella test that runs all test cases +func TestMain(m *testing.M) { + flag.Parse() + + // Set up a connection to the server. + conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + expanderClient = pb.NewExpanderClient(conn) + + // Set an empty facade object with just GVK + facade := &unstructured.Unstructured{} + facade.SetGroupVersionKind(facadeGVK) + facade.SetName("foo") + facade.SetNamespace("composition-system") + sampleFacade, _ = json.Marshal(facade.Object) + + // Marshall GetterConfiguration config for test values + emptyGetterConfiguration, err = json.Marshal(emptyGetterConfigurationObj) + if err != nil { + log.Fatalf("unable to marshall emptyGetterConfiguration: %v", err) + } + + exitCode := m.Run() + os.Exit(exitCode) +} + +func TestValidate(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte{}, + Context: []byte{}, + Facade: []byte{}, + Value: []byte{}, + }) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected SUCCESS, got: %s", r) + } +} + +func TestEvaluateEmptyConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: []byte("{\"project\": \"foo\"}"), + Facade: sampleFacade, + Value: []byte("{\"key\": \"val\"}"), + }) + if err != nil { + t.Fatalf("could not evaluate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestEvaluateEmptyContext(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: emptyGetterConfiguration, + Resource: "sqls", + Context: []byte{}, + Facade: sampleFacade, + Value: []byte("{\"key\": \"val\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Values) != emptyGetterConfigurationEvaluateResult { + t.Fatalf("\nexpected: %s\n got: %s", emptyGetterConfigurationEvaluateResult, r.Values) + } +} + +func TestEvaluateEmptyResourceString(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: emptyGetterConfiguration, + Context: []byte{}, + Facade: sampleFacade, + Value: []byte("{\"key\": \"val\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Values) != emptyGetterConfigurationEvaluateResult { + t.Fatalf("\nexpected: %s\n got: %s", emptyGetterConfigurationEvaluateResult, r.Values) + } +} + +func TestEvaluateEmptyFacade(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: emptyGetterConfiguration, + Resource: "sqls", + Context: []byte("{\"key\": \"val\"}"), + Facade: []byte{}, + Value: []byte("{\"key\": \"val\"}"), + }) + if err == nil { + t.Fatalf("expected error. got nil") + } + if r != nil { + t.Fatalf("\nexpected: nil result\n got: non-nil value") + } +} + +func evaluateGetterConfigurationMissingObject(t *testing.T, getter []byte, expectedErrorString string) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: getter, + Facade: sampleFacade, + }) + if err != nil { + t.Fatalf("expected no error. got %v", err) + } + + if r.Status != pb.Status_EVALUATE_WAIT { + t.Fatalf("\nexpected: EVALUATE_WAIT \n got: %s", r) + } + + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %q \n got: %q", expectedErrorString, r) + } +} + +func TestEvaluateGetterConfigurationMissingObjectGroup(t *testing.T) { + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "missinggroup", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "acme.something.com", + Version: "v1", + Resource: "foobars", + Kind: "FooBar", + Name: "missing", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{}, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + expectedErrorString := "Dependent object not found: GVR: foobars.acme.something.com(v1)/composition-system/missing" + evaluateGetterConfigurationMissingObject(t, getter, expectedErrorString) +} + +func TestEvaluateGetterConfigurationMissingObjectResource(t *testing.T) { + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "missingresource", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "apps", + Version: "v1", + Resource: "unknown", + Kind: "unknown", + Name: "missing", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{}, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + expectedErrorString := "Dependent object not found: GVR: unknown.apps(v1)/composition-system/missing" + evaluateGetterConfigurationMissingObject(t, getter, expectedErrorString) +} + +func TestEvaluateGetterConfigurationMissingObject(t *testing.T) { + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "missing", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "apps", + Version: "v1", + Resource: "deployments", + Kind: "Deployment", + Name: "missing", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{}, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: getter, + Facade: sampleFacade, + }) + if err != nil { + t.Fatalf("expected no error. got %v", err) + } + + if r.Status != pb.Status_EVALUATE_WAIT { + t.Fatalf("\nexpected: EVALUATE_WAIT \n got: %s", r) + } + + expectedErrorString := "Dependent object not found: GVR: deployments.apps(v1)/composition-system/missing" + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} + +func TestEvaluateGetterConfigurationValidObjectNoFieldRef(t *testing.T) { + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "missing", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "apps", + Version: "v1", + Resource: "deployments", + Kind: "Deployment", + Name: "composition-controller-manager", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{}, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: getter, + Facade: sampleFacade, + }) + if err != nil { + t.Fatalf("expected no error. got %v", err) + } + + if r.Status != pb.Status_SUCCESS { + t.Fatalf("\nexpected: SUCCESS \n got: %s", r) + } + + if string(r.Values) != emptyGetterConfigurationEvaluateResult { + t.Fatalf("\nexpected: %s\n got: %s", emptyGetterConfigurationEvaluateResult, r.Values) + } +} + +func TestEvaluateGetterConfigurationValidObjectWithMissingField(t *testing.T) { + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "missing", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "apps", + Version: "v1", + Resource: "deployments", + Kind: "Deployment", + Name: "composition-controller-manager", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{ + { + Path: ".spec.template.foobar", + As: "foobar", + }, + }, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: getter, + Facade: sampleFacade, + }) + if err != nil { + t.Fatalf("expected no error. got %v", err) + } + + if r.Status != pb.Status_EVALUATE_WAIT { + t.Fatalf("\nexpected: EVALUATE_WAIT \n got: %s", r) + } + + expectedErrorString := "Field path not present in object yet: " + + "Deployment.apps(v1)/composition-system/composition-controller-manager" + + "[.spec.template.foobar]" + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} + +func TestEvaluateGetterConfigurationValidObjectWithValidField(t *testing.T) { + resultValues := "{\"manager\":{\"replicas\":1}}" + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "manager", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "apps", + Version: "v1", + Resource: "deployments", + Kind: "Deployment", + Name: "composition-controller-manager", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{ + { + Path: ".spec.replicas", + As: "replicas", + }, + }, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: getter, + Facade: sampleFacade, + }) + if err != nil { + t.Fatalf("expected no error. got %v", err) + } + + if r.Status != pb.Status_SUCCESS { + t.Fatalf("\nexpected: SUCCESS \n got: %s", r) + } + + if string(r.Values) != resultValues { + t.Fatalf("\nexpected: %s\n got: %s", resultValues, r.Values) + } +} + +func TestEvaluateGetterConfigurationValidObjectWithMultipleFields(t *testing.T) { + resultValues := "{\"manager\":{\"deadline\":600,\"replicas\":1}}" + // Marshall GetterConfiguration config + getter, err := json.Marshal(&compositionv1alpha1.GetterConfiguration{ + Spec: compositionv1alpha1.GetterConfigurationSpec{ + ValuesFrom: []compositionv1alpha1.ValuesFrom{ + { + Name: "manager", + ResourceRef: compositionv1alpha1.ResourceRef{ + Group: "apps", + Version: "v1", + Resource: "deployments", + Kind: "Deployment", + Name: "composition-controller-manager", + }, + // Empty fieldref + FieldRef: []compositionv1alpha1.FieldRef{ + { + Path: ".spec.replicas", + As: "replicas", + }, + { + Path: ".spec.progressDeadlineSeconds", + As: "deadline", + }, + }, + }, + }, + }, + }) + if err != nil { + log.Fatalf("unable to marshall getter: %v", err) + } + + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: getter, + Facade: sampleFacade, + }) + if err != nil { + t.Fatalf("expected no error. got %v", err) + } + + if r.Status != pb.Status_SUCCESS { + t.Fatalf("\nexpected: SUCCESS \n got: %s", r) + } + + if string(r.Values) != resultValues { + t.Fatalf("\nexpected: %s\n got: %s", resultValues, r.Values) + } +} diff --git a/composition/expanders/jinja2/main.go b/composition/expanders/jinja2/main.go new file mode 100644 index 0000000..1734a27 --- /dev/null +++ b/composition/expanders/jinja2/main.go @@ -0,0 +1,245 @@ +// Copyright 2024 Google LLC +// +// 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 ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "os" + "os/exec" + "path/filepath" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + "tailscale.com/atomicfile" +) + +var ( + port = flag.Int("port", 8443, "The server port") +) + +// server is used to implement exander.Evaluator +type server struct { + pb.UnimplementedExpanderServer +} + +type expander struct { + path string + context []byte + config []byte + value []byte + facade []byte + resource string +} + +// Verify the expander config/template +func (s *server) Validate(ctx context.Context, req *pb.ValidateRequest) (*pb.ValidateResult, error) { + result := &pb.ValidateResult{ + Status: pb.Status_SUCCESS, + Error: &pb.Error{}, + } + + //log.Printf("ValidateRequest:\n config: %s\n context: %s\n facade: %s\n values: %s\n", + // req.Config, req.Context, req.Facade, req.Value) + dir, err := os.MkdirTemp("", "jinja2") + if err != nil { + newerr := fmt.Errorf("unexpected tmp file creation failure. %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + // cleanup + defer func() { + err := os.RemoveAll(dir) + log.Printf("Error removing directory: %s, %v", dir, err) + }() + + e := expander{ + path: dir, + context: req.Context, + facade: req.Facade, + value: req.Value, + config: req.Config, + resource: req.Resource, + } + if err = e.WriteInputsToFileSystem(); err != nil { + newerr := fmt.Errorf("error processing inputs: %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + + return e.Validate(result) +} + +// Evaluate the expander config in context of inputs and return manifests +func (s *server) Evaluate(ctx context.Context, req *pb.EvaluateRequest) (*pb.EvaluateResult, error) { + result := &pb.EvaluateResult{ + Status: pb.Status_SUCCESS, + Type: pb.ResultType_MANIFESTS, + Error: &pb.Error{}, + } + + //log.Printf("EvaluateRequest:\n config: %s\n context: %s\n facade: " + // "%s\n values: %s\n", req.Config, req.Context, req.Facade, req.Value) + dir, err := os.MkdirTemp("", "jinja2") + if err != nil { + newerr := fmt.Errorf("unexpected tmp file creation failure. %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + // cleanup + defer func() { + err := os.RemoveAll(dir) + log.Printf("Error removing directory: %s, %v", dir, err) + }() + + e := expander{ + path: dir, + context: req.Context, + facade: req.Facade, + value: req.Value, + config: req.Config, + resource: req.Resource, + } + if err = e.WriteInputsToFileSystem(); err != nil { + newerr := fmt.Errorf("error processing inputs: %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + + return e.Evaluate(result) +} + +func (e *expander) WriteInputsToFileSystem() error { + context := map[string]interface{}{} + if string(e.context) != "" { + err := json.Unmarshal(e.context, &context) + if err != nil { + return fmt.Errorf("error unmarshalling req.Context: %w", err) + } + } + + facade := map[string]interface{}{} + if string(e.facade) != "" { + err := json.Unmarshal(e.facade, &facade) + if err != nil { + return fmt.Errorf("error unmarshalling req.Facade: %w", err) + } + } + + getterValues := map[string]interface{}{} + if string(e.value) != "" { + err := json.Unmarshal(e.value, &getterValues) + if err != nil { + return fmt.Errorf("error unmarshalling req.Values: %w", err) + } + } + + valuesObj := map[string]interface{}{ + "context": context, + e.resource: facade, + "values": getterValues, + } + + // marshall values + values, err := json.Marshal(valuesObj) + if err != nil { + return fmt.Errorf("failed to marshal values: %w", err) + } + + // Write values to file + err = atomicfile.WriteFile(filepath.Join(e.path, "/values"), values, 0644) + if err != nil { + return fmt.Errorf("failed to write req.[context,facade,values] to file: %w", err) + } + + // Write template to file + err = atomicfile.WriteFile(filepath.Join(e.path, "/template"), e.config, 0644) + if err != nil { + return fmt.Errorf("failed to write req.Config to file: %w", err) + } + + return nil +} + +// nolint:unparam +func (e *expander) Validate(result *pb.ValidateResult) (*pb.ValidateResult, error) { + // usage: python parse_template.py + args := []string{ + "parse_template.py", + filepath.Join(e.path, "/template"), + } + + op, err := exec.Command("python", args...).CombinedOutput() + if err != nil { + result.Error.Message = fmt.Sprintf("failed validating template:\n %s", string(op)) + result.Status = pb.Status_VALIDATE_FAILED + log.Print(result.Error.Message) + return result, nil + } + + //log.Printf("Result:\n type: %s\n error: %s\n status: %s\n", result.Type, result.Errors, result.Status) + return result, nil +} + +func (e *expander) Evaluate(result *pb.EvaluateResult) (*pb.EvaluateResult, error) { + // wait for /expanded/expanded to be created and then read it + args := []string{ + filepath.Join(e.path, "/template"), + filepath.Join(e.path, "/values"), + "--format=json", + "-o", + filepath.Join(e.path, "/expanded"), + } + + op, err := exec.Command("jinja2", args...).CombinedOutput() + if err != nil { + result.Error.Message = fmt.Sprintf("failed evaluating template:\n %s", string(op)) + result.Status = pb.Status_EVALUATE_FAILED + log.Print(result.Error.Message) + return result, nil + } + + manifests, err := os.ReadFile(filepath.Join(e.path, "/expanded")) + if err != nil { + newerr := fmt.Errorf("failed reading expanded file: %w", err) + log.Print(newerr) + return nil, newerr + } + + result.Manifests = manifests + + //log.Printf("Result:\n type: %s\n error: %s\n status: %s\n " + //" manifests: %s\n values: %s\n", result.Type, result.Errors, + // result.Status, result.Manifests, result.Values) + return result, nil +} + +func main() { + flag.Parse() + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterExpanderServer(s, &server{}) + log.Printf("server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/composition/expanders/jinja2/main_test.go b/composition/expanders/jinja2/main_test.go new file mode 100644 index 0000000..22b06ed --- /dev/null +++ b/composition/expanders/jinja2/main_test.go @@ -0,0 +1,435 @@ +// Copyright 2024 Google LLC +// +// 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_test + +import ( + "context" + "flag" + "log" + "os" + "strings" + "testing" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const ( + trivialJsonObject string = "{\"key\": \"val\"}" + noAttributeZoneErrorString string = "has no attribute 'zone'" +) + +var ( + addr = flag.String("addr", "[::]:8443", "the address to connect to") +) + +var expanderClient pb.ExpanderClient + +// TestMain - umbrella test that runs all test cases +func TestMain(m *testing.M) { + flag.Parse() + + // Set up a connection to the server. + conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + expanderClient = pb.NewExpanderClient(conn) + + exitCode := m.Run() + os.Exit(exitCode) +} + +func TestEvaluateEmptyConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: []byte("{\"project\": \"foo\"}"), + Facade: []byte(trivialJsonObject), + Value: []byte(trivialJsonObject), + }) + if err != nil { + t.Fatalf("could not evaluate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestEvaluateEmptyContext(t *testing.T) { + config := trivialJsonObject + manifests := trivialJsonObject + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + Context: []byte{}, + Facade: []byte(trivialJsonObject), + Value: []byte(trivialJsonObject), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Manifests) != manifests { + t.Fatalf("\nexpected: %s\n got: %s", manifests, r.Manifests) + } +} + +func TestEvaluateEmptyFacade(t *testing.T) { + manifests := trivialJsonObject + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(trivialJsonObject), + Resource: "sqls", + Context: []byte(trivialJsonObject), + Facade: []byte{}, + Value: []byte(trivialJsonObject), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Manifests) != manifests { + t.Fatalf("\nexpected: %s\n got: %s", manifests, r.Manifests) + } +} + +func TestEvaluateEmptyValue(t *testing.T) { + config := trivialJsonObject + manifests := trivialJsonObject + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + Context: []byte(trivialJsonObject), + Facade: []byte(trivialJsonObject), + Value: []byte{}, + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Manifests) != manifests { + t.Fatalf("\nexpected: %s\n got: %s", manifests, r.Manifests) + } +} + +func TestEvaluateTemplateUsesFacade(t *testing.T) { + config := "region: {{ sqls.region }}" + manifests := "region: us-west1" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Manifests) != manifests { + t.Fatalf("\nexpected: %s\n got: %s", manifests, r.Manifests) + } +} + +func TestEvaluateTemplateMissingFacadeField(t *testing.T) { + // expects zone providing region + config := "region: {{ sqls.zone.foobar }}" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + // expects zone providing region + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_EVALUATE_FAILED { + t.Fatalf("\nexpected: EVALUATE_FAILED \n got: %s", r) + } + + expectedErrorString := noAttributeZoneErrorString + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} + +func TestEvaluateTemplateUsesContext(t *testing.T) { + config := "project: {{ context.project }}, region: {{ sqls.region }}" + manifests := "project: foobar, region: us-west1" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Context: []byte("{\"project\": \"foobar\"}"), + Resource: "sqls", + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Manifests) != manifests { + t.Fatalf("\nexpected: %s\n got: %s", manifests, r.Manifests) + } +} + +func TestEvaluateTemplateMissingContextField(t *testing.T) { + config := "region: {{ context.zone.foobar }}" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + // expects zone providing project + Context: []byte("{\"project\": \"foobar\"}"), + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_EVALUATE_FAILED { + t.Fatalf("\nexpected: EVALUATE_FAILED \n got: %s", r) + } + + expectedErrorString := noAttributeZoneErrorString + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} + +func TestEvaluateTemplateUsesValues(t *testing.T) { + config := "identity: {{ values.email }}, region: {{ sqls.region }}" + manifests := "identity: foobar@acme.com, region: us-west1" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Value: []byte("{\"email\": \"foobar@acme.com\"}"), + Resource: "sqls", + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if string(r.Manifests) != manifests { + t.Fatalf("\nexpected: %s\n got: %s", manifests, r.Manifests) + } +} + +func TestEvaluateTemplateMissingValuesField(t *testing.T) { + config := "email: {{ values.zone.foobar }}" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + // expects zone providing email + Value: []byte("{\"email\": \"foobar@acme.comm\"}"), + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_EVALUATE_FAILED { + t.Fatalf("\nexpected: EVALUATE_FAILED \n got: %s", r) + } + + expectedErrorString := noAttributeZoneErrorString + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} + +func TestEvaluateTemplateWrongTopLevelField(t *testing.T) { + // no top level zone + config := "email: {{ zone.foobar }}" + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte(config), + Resource: "sqls", + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_EVALUATE_FAILED { + t.Fatalf("\nexpected: EVALUATE_FAILED \n got: %s", r) + } + + expectedErrorString := "'zone' is undefined" + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} + +// -------------------------------------------------------- +// ----------------- Validate Tests ----------------------- +// -------------------------------------------------------- + +func TestValidate(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte{}, + Context: []byte{}, + Facade: []byte{}, + Value: []byte{}, + }) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected SUCCESS, got: %s", r) + } +} + +func TestValidateEmptyConfig(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: []byte("{\"project\": \"foo\"}"), + Facade: []byte(trivialJsonObject), + Value: []byte(trivialJsonObject), + }) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateEmptyContext(t *testing.T) { + config := trivialJsonObject + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte(config), + Resource: "sqls", + Context: []byte{}, + Facade: []byte(trivialJsonObject), + Value: []byte(trivialJsonObject), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected success. got: %s", r) + } +} + +func TestValidateEmptyFacade(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte(trivialJsonObject), + Resource: "sqls", + Context: []byte(trivialJsonObject), + Facade: []byte{}, + Value: []byte(trivialJsonObject), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected success. got: %s", r) + } +} + +func TestValidateEmptyValue(t *testing.T) { + config := trivialJsonObject + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte(config), + Resource: "sqls", + Context: []byte(trivialJsonObject), + Facade: []byte(trivialJsonObject), + Value: []byte{}, + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected success. got: %s", r) + } +} + +func TestValidateTemplateUsesFacade(t *testing.T) { + config := "region: {{ sqls.region }}" + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte(config), + Resource: "sqls", + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected success. got: %s", r) + } +} + +func TestValidateTemplateMissingFacadeField(t *testing.T) { + // expects zone providing region + config := "region: {{ sqls.zone.foobar }}" + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte(config), + Resource: "sqls", + // expects zone providing region + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + // semantic validation is not done. so it should pass + if r.Status != pb.Status_SUCCESS { + t.Fatalf("expected success. got: %s", r) + } +} + +func TestValidateJinja2ErrorTemplate(t *testing.T) { + // expects zone providing region + config := "region: {{% sqls.zone.foobar %}}" + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte(config), + Resource: "sqls", + // expects zone providing region + Facade: []byte("{\"region\": \"us-west1\"}"), + }) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + // semantic validation is not done. so it should pass + if r.Status != pb.Status_VALIDATE_FAILED { + t.Fatalf("expected VALIDATE_FAILED. got: %s", r) + } + + expectedErrorString := "template\njinja2.exceptions.TemplateSyntaxError: unexpected '%'" + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error contains: %s \n got: %s", expectedErrorString, r) + } +} diff --git a/composition/expanders/jinja2/parse_template.py b/composition/expanders/jinja2/parse_template.py new file mode 100644 index 0000000..f5dd40e --- /dev/null +++ b/composition/expanders/jinja2/parse_template.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# Copyright 2024 Google LLC +# +# 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. + +# +# filename: parse_template.py +# usage: python parse_template.py + +import sys +from jinja2 import Environment + +env = Environment() +with open(sys.argv[1]) as template: + env.parse(template.read()) \ No newline at end of file diff --git a/composition/expanders/jinja2/requirements.txt b/composition/expanders/jinja2/requirements.txt new file mode 100644 index 0000000..b3e61d2 --- /dev/null +++ b/composition/expanders/jinja2/requirements.txt @@ -0,0 +1,9 @@ +pip==24.0 --hash=sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc +jinja2==v3.1.3 --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa +jinja2-cli==v0.8.2 --hash=sha256:b91715c79496beaddad790171e7258a87db21c1a0b6d2b15bca3ba44b74aac5d +MarkupSafe==v2.1.5 --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a +PyYAML==v6.0.1 --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c +setuptools==v57.5.0 --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b +toml==v0.10.2 --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b +wheel==v0.41.2 --hash=sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852 +xmltodict==v0.13.0 --hash=sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852 \ No newline at end of file diff --git a/composition/go.mod b/composition/go.mod new file mode 100644 index 0000000..cd8fddb --- /dev/null +++ b/composition/go.mod @@ -0,0 +1,108 @@ +module github.com/cloud-native-compositions/compositions/composition + +go 1.22.0 + +toolchain go1.23.2 + +require ( + github.com/Masterminds/semver/v3 v3.2.1 + github.com/go-logr/logr v1.4.1 + github.com/gobuffalo/flect v1.0.2 + github.com/google/cel-go v0.17.7 + github.com/google/go-cmp v0.6.0 + github.com/google/safetext v0.0.0-20240104143208-7a7d9b3d812f + github.com/onsi/ginkgo/v2 v2.14.0 + github.com/onsi/gomega v1.30.0 + github.com/wzshiming/easycel v0.5.0 + golang.org/x/time v0.5.0 + google.golang.org/grpc v1.64.1 + google.golang.org/protobuf v1.34.2 + k8s.io/api v0.29.1 + k8s.io/apiextensions-apiserver v0.29.1 + k8s.io/apimachinery v0.29.1 + k8s.io/client-go v0.29.1 + k8s.io/klog/v2 v2.120.1 + k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 + sigs.k8s.io/cli-utils v0.35.0 + sigs.k8s.io/controller-runtime v0.17.2 + sigs.k8s.io/kubebuilder-declarative-pattern v0.15.0-beta.1.0.20240614185435-a248ed1e894c + sigs.k8s.io/kubebuilder-declarative-pattern/applylib v0.0.0-20240830014331-1a63d5a3bb9d + sigs.k8s.io/kustomize/kstatus v0.0.2-0.20200509233124-065f70705d4d + tailscale.com v1.62.0 +) + +require ( + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.2 // indirect + github.com/evanphx/json-patch/v5 v5.8.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.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.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.7 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pborman/uuid v1.2.1 // 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.46.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // 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/apiserver v0.29.1 // indirect + k8s.io/component-base v0.29.1 // indirect + k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/composition/go.sum b/composition/go.sum new file mode 100644 index 0000000..040d39e --- /dev/null +++ b/composition/go.sum @@ -0,0 +1,599 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= +github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.1 h1:iPEdwg0XayoS+E7Mth9JxwUtOgyVxnDTXHtKhZPlZxA= +github.com/evanphx/json-patch/v5 v5.8.1/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +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/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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 v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +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/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= +github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +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/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= +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.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 v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/safetext v0.0.0-20240104143208-7a7d9b3d812f h1:o2yGZLlsOj5H5uvtQNEdi6DeA0GbUP3lm0gWW5RvY0s= +github.com/google/safetext v0.0.0-20240104143208-7a7d9b3d812f/go.mod h1:H3K1Iu/utuCfa10JO+GsmKUYSWi7ug57Rk6GaDRHaaQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +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/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +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.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +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 v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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.8/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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +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.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/wzshiming/easycel v0.5.0 h1:pnMBpOuEfr9EYLNpKD52NfpAhUrOQ3uAHFQT7bF76ZM= +github.com/wzshiming/easycel v0.5.0/go.mod h1:4Jue5gr86TomO6urajHxWCNbEKwwYnQVsyjKOWT/V/U= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +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 v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +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 v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +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-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190320064053-1272bf9dcd53/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-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +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-20190227155943-e225da77a7e6/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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190911201528-7ad0cfa0b7b5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190206041539-40960b6deb8e/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +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.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +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.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/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.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= +k8s.io/apiextensions-apiserver v0.29.1 h1:S9xOtyk9M3Sk1tIpQMu9wXHm5O2MX6Y1kIpPMimZBZw= +k8s.io/apiextensions-apiserver v0.29.1/go.mod h1:zZECpujY5yTW58co8V2EQR4BD6A9pktVgHhvc0uLfeU= +k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= +k8s.io/apiserver v0.29.1 h1:e2wwHUfEmMsa8+cuft8MT56+16EONIEK8A/gpBSco+g= +k8s.io/apiserver v0.29.1/go.mod h1:V0EpkTRrJymyVT3M49we8uh2RvXf7fWC5XLB0P3SwRw= +k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= +k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= +k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= +k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= +k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= +k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= +k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw= +k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15 h1:m6dl1pkxz3HuE2mP9MUYPCCGyy6IIFlv/vTlLBDxIwA= +k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= +sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y= +sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE= +sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= +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/kubebuilder-declarative-pattern v0.15.0-beta.1.0.20240614185435-a248ed1e894c h1:oDDOYsfrwJlLZ0pyzZiG7L/rF2JuQvvut+vFOYYZKQQ= +sigs.k8s.io/kubebuilder-declarative-pattern v0.15.0-beta.1.0.20240614185435-a248ed1e894c/go.mod h1:56THnwsHGyrijk2GYKsTzcagxDoevccrdl+gBJWNocs= +sigs.k8s.io/kubebuilder-declarative-pattern/applylib v0.0.0-20240830014331-1a63d5a3bb9d h1:9Re+DU/Qzv7mIz6qDFmVFvYqfFSAuc6ssi1CH/2yQX8= +sigs.k8s.io/kubebuilder-declarative-pattern/applylib v0.0.0-20240830014331-1a63d5a3bb9d/go.mod h1:HReseUdDPJYAh1jxBreYJsh/mJ9l7AtvoSB30Jdlfmc= +sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20230303024857-d1f76c15e05b h1:VcgyLj8SawHulvC24SRgI37mnhMmR3hW+ZVqZGBmkbc= +sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20230303024857-d1f76c15e05b/go.mod h1:smCBkOX4Z3K9+MUGAscbschHAYgbuRZ+Pri49+x2zTc= +sigs.k8s.io/kustomize/kstatus v0.0.2-0.20200509233124-065f70705d4d h1:k+m3LgwVsvSDvcUWoaG2yUGcyN/HKR6Dq2lb0FYgbq8= +sigs.k8s.io/kustomize/kstatus v0.0.2-0.20200509233124-065f70705d4d/go.mod h1:6qUKWLy4+yGExtjbs+fibz2tOBZG7413yx2NHyAzIU0= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= +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/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +tailscale.com v1.62.0 h1:iI1fPDNXXETMbVEatos7xSR6Bv6aCuonD7B1X3glnPE= +tailscale.com v1.62.0/go.mod h1:cC0b0vYCoSDOLufJX5J5zDUrvV3lYwOLqlt9NW8y4cY= diff --git a/composition/hack/boilerplate.go.txt b/composition/hack/boilerplate.go.txt new file mode 100644 index 0000000..ff72ff2 --- /dev/null +++ b/composition/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2024. + +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/composition/inline.mk b/composition/inline.mk new file mode 100644 index 0000000..7610cd7 --- /dev/null +++ b/composition/inline.mk @@ -0,0 +1,25 @@ +###### ----------- Inline manifest tool section ---------------------------- + +##@ Inline sidecar pod + +INLINE_IMG ?= $(IMG_REGISTRY)/manifests-inline:$(IMG_VERSION) + +.PHONY: build-inline +build-inline: fmt vet ## Build binary. + $(GOPREFIX) go build -v -o bin/inline ./cmd/inline + +.PHONY: clean-inline +clean-inline: ## clean binary. + rm -fr bin/inline + docker rmi ${IMG} . + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build-inline +docker-build-inline: ## Build docker image with the manager. + docker build -t ${INLINE_IMG} -f Dockerfile.inline . + +.PHONY: docker-push-inline +docker-push-inline: ## Push docker image with the manager. + docker push ${INLINE_IMG} diff --git a/composition/internal/controller/composition_controller.go b/composition/internal/controller/composition_controller.go new file mode 100644 index 0000000..f65e3c0 --- /dev/null +++ b/composition/internal/controller/composition_controller.go @@ -0,0 +1,411 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "sync" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "github.com/go-logr/logr" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + corev1 "k8s.io/api/core/v1" + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "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/tools/record" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var FacadeControllers sync.Map +var errDuplicate = fmt.Errorf("duplicate composition") + +// CompositionReconciler reconciles a Composition object +type CompositionReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + mgr ctrl.Manager + handoffChannels map[schema.GroupVersionKind]chan event.GenericEvent +} + +// TODO: To simplify preview for customers, grant superuser to the composition controller. This should be revisited going forward. +//+kubebuilder:rbac:groups=*,resources=*,verbs=* +//+kubebuilder:rbac:groups=composition.google.com,resources=compositions;contexts;expanderversions;facades;getterconfigurations;plans,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=composition.google.com,resources=compositions/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=composition.google.com,resources=compositions/finalizers,verbs=update +//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=create;get;list;watch +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch +//+kubebuilder:rbac:groups=facade.facade,resources=*,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;create;patch;delete +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;create;patch;delete +//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;get;patch;list;delete +//+kubebuilder:rbac:groups="batch",resources=jobs,verbs=create;get;patch;list;delete +//+kubebuilder:rbac:groups=facade.compositions.google.com,resources=*,verbs=get;list;patch;update;watch;create;delete +//+kubebuilder:rbac:groups=facade.compositions.google.com,resources=*/status,verbs=get;update + +// / +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile +func (r *CompositionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("Got a new request!", "request", req) + + var composition compositionv1alpha1.Composition + if err := r.Client.Get(ctx, req.NamespacedName, &composition); err != nil { + logger.Error(err, "unable to fetch Composition") + // 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. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Grab status for comparison later + oldStatus := composition.Status.DeepCopy() + + // Try updating status before returning + defer func() { + if !reflect.DeepEqual(oldStatus, composition.Status) { + newStatus := composition.Status.DeepCopy() + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + nn := types.NamespacedName{Namespace: composition.Namespace, Name: composition.Name} + err := r.Client.Get(ctx, nn, &composition) + if err != nil { + return err + } + composition.Status = *newStatus.DeepCopy() + return r.Client.Status().Update(ctx, &composition) + }) + if err != nil { + logger.Error(err, "unable to update Composition status") + } + } + }() + + logger = logger.WithName(composition.Name).WithName(fmt.Sprintf("%d", composition.Generation)) + + composition.Status.ClearCondition(compositionv1alpha1.Error) + composition.Status.ClearCondition(compositionv1alpha1.ValidationFailed) + + logger.Info("Validating expander configs") + if err := r.validateExpanders(ctx, logger, &composition); err != nil { + logger.Info("expander config validation failed") + return ctrl.Result{}, err + } + + logger.Info("Processing Composition object") + if err := r.processComposition(ctx, &composition, logger); err != nil { + // Don't return an error to avoid requeuing a request that we know can't succeed. + if errors.Is(err, errDuplicate) { + return ctrl.Result{}, nil + } + logger.Info("Error processing Composition") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *CompositionReconciler) validateExpanders( + ctx context.Context, logger logr.Logger, c *compositionv1alpha1.Composition, +) error { + errorStages := []string{} + if c.Status.Stages == nil { + c.Status.Stages = make(map[string]compositionv1alpha1.StageValidationStatus) + } + for _, expander := range c.Spec.Expanders { + uri, ev, reason, err := r.getExpanderValue(ctx, expander.Version, expander.Type) + if err != nil { + logger.Error(err, "Error getting ExpanderVersion", "type", expander.Type, "version", expander.Version) + errorStages = append(errorStages, expander.Name) + c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{ + ValidationStatus: compositionv1alpha1.ValidationStatusError, + Reason: reason, + Message: err.Error(), + } + // Try the next expander + continue + } + logger.Info("Got valid expander uri", "uri", uri) + + // We dont have validate for Job type expander + if ev.Spec.Type != compositionv1alpha1.ExpanderTypeGRPC { + c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{ + ValidationStatus: compositionv1alpha1.ValidationStatusUnknown, + Message: "expander type does not implement validation", + Reason: "NoValidationSupport", + } + } else { + reason, err := r.validateExpanderConfig(ctx, logger, expander, ev, uri) + if err != nil { + logger.Error(err, "Validating config failed", "type", expander.Type, "version", expander.Version) + errorStages = append(errorStages, expander.Name) + c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{ + ValidationStatus: compositionv1alpha1.ValidationStatusFailed, + Reason: reason, + Message: err.Error(), + } + // Try the next expander + continue + } + + c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{ + ValidationStatus: compositionv1alpha1.ValidationStatusSuccess, + Reason: "ValidationPassed", + Message: "", + } + } + } + + if len(errorStages) != 0 { + message := fmt.Sprintf("Validating failed for stages: %s", strings.Join(errorStages, ", ")) + c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: message, + Reason: "ValidationFailed", + Type: string(compositionv1alpha1.Error), + Status: metav1.ConditionTrue, + }) + r.Recorder.Event(c, "Warning", "ValidationFailed", message) + return fmt.Errorf("Validation Failed") + } + + return nil +} +func (r *CompositionReconciler) validateExpanderConfig(ctx context.Context, logger logr.Logger, + expander compositionv1alpha1.Expander, ev *compositionv1alpha1.ExpanderVersion, grpcService string) (string, error) { + // Set up a connection to the server. + conn, err := grpc.NewClient(grpcService, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + logger.Error(err, "grpc dial failed: "+grpcService) + return "GRPCConnError", err + } + + // marshall expander config + // read Expander config from in cr.namespace + var configBytes []byte + if expander.ConfigRef != nil { + expanderConfigcr := unstructured.Unstructured{} + expanderConfigcr.SetGroupVersionKind(schema.GroupVersionKind{ + Group: ev.Spec.Config.Group, + Version: ev.Spec.Config.Version, + Kind: ev.Spec.Config.Kind, + }) + expanderConfigNN := types.NamespacedName{Namespace: expander.ConfigRef.Namespace, Name: expander.ConfigRef.Name} + if err := r.Get(ctx, expanderConfigNN, &expanderConfigcr); err != nil { + logger.Error(err, "unable to fetch ExpanderConfig CR", "expander config", expanderConfigNN) + return "GetExpanderConfigFailed", err + } + configBytes, err = json.Marshal(expanderConfigcr.Object) + if err != nil { + logger.Error(err, "failed to marshal ExpanderConfig Object") + return "MarshallExpanderConfigFailed", err + } + } else { + // TODO check if json.Marshall is escaping quotes + // Also causes > to be replaced unicode 'if loop.index \u003e 1' + err = nil + //configBytes, err = json.Marshal(expander.Template) + configBytes = []byte(expander.Template) + if err != nil { + logger.Error(err, "failed to marshall Expander template") + return "MarshallExpanderTemplateFailed", err + } + } + + expanderClient := pb.NewExpanderClient(conn) + result, err := expanderClient.Validate(ctx, + &pb.ValidateRequest{ + Config: configBytes, + }) + if err != nil { + logger.Error(err, "expander.Validate() Failed", "expander", expander.Name) + return "ValidateError", err + } + if result.Status != pb.Status_SUCCESS { + logger.Error(nil, "expander.Validate() Status is not Success", "expander", + expander.Name, "status", result.Status, "message", result.Error.Message) + err = fmt.Errorf(result.Error.Message) + return "ValidateStatusFailed", err + } + return "", nil +} + +func (r *CompositionReconciler) getExpanderValue( + ctx context.Context, inputExpanderVersion string, expanderType string, +) (string, *compositionv1alpha1.ExpanderVersion, string, error) { + logger := log.FromContext(ctx) + + value := "" + var ev compositionv1alpha1.ExpanderVersion + err := r.Client.Get(ctx, + types.NamespacedName{ + Name: "composition-" + expanderType, + Namespace: "composition-system"}, + &ev) + + if err != nil { + // The CR should be created before the specified expander can be used. + logger.Error(err, "Failed to get the ExpanderVersionCR") + if apierrors.IsNotFound(err) { + return value, nil, "MissingExpanderCR", err + } else { + return value, nil, "ErrorGettingExpanderVersionCR", err + } + } + + if ev.Status.VersionMap == nil { + return value, nil, "ErrorEmptyVersionMap", fmt.Errorf("ExpanderVersion .status.versionMap is empty") + } + + logger.Info("input expander version", "current", inputExpanderVersion) + value, ok := ev.Status.VersionMap[inputExpanderVersion] + if !ok { + return value, nil, "VersionNotFound", fmt.Errorf("%s version not found", inputExpanderVersion) + } + return value, &ev, "", nil +} + +func (r *CompositionReconciler) processComposition( + ctx context.Context, c *compositionv1alpha1.Composition, logger logr.Logger, +) error { + var crd extv1.CustomResourceDefinition + logger = logger.WithName(c.Spec.InputAPIGroup) + + crdName := c.Spec.InputAPIGroup + + err := r.Client.Get(ctx, types.NamespacedName{Name: crdName, Namespace: ""}, &crd) + if err != nil { + reason := "FailedGettingFacadeCRD" + if apierrors.IsNotFound(err) { + reason = "MissingFacadeCRD" + } + c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: err.Error(), + Reason: reason, + Type: string(compositionv1alpha1.Error), + Status: metav1.ConditionTrue, + }) + logger.Error(err, "failed to get an Facade CRD object") + r.Recorder.Event(c, "Warning", "MissingFacadeCRD", + fmt.Sprintf("Failed to get Facade CRD: %s", c.Spec.InputAPIGroup)) + return err + } + + logger.Info("Found InputAPI CRD", "Group", crd.Spec.Group, + "Version", crd.Spec.Versions[0].Name, "Kind", crd.Spec.Names.Kind) + + gvk := schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: crd.Spec.Versions[0].Name, + Kind: crd.Spec.Names.Kind, + } + cr := &unstructured.Unstructured{} + cr.SetGroupVersionKind(gvk) + + // TODO(barni@) Stop existing reconciler and start a new one + logger.Info("Checking if Reconciler already exists for InputAPI CRD") + cNN := types.NamespacedName{Namespace: c.Namespace, Name: c.Name} + nn, loaded := FacadeControllers.LoadOrStore(gvk, cNN) + if loaded { + existingNN := nn.(types.NamespacedName) + if existingNN.Namespace != c.Namespace || existingNN.Name != c.Name { + msg := fmt.Sprintf("Failed to apply composition %v over existing composition named %v for GVK %v", cNN, nn, gvk) + logger.Error(errDuplicate, msg) + c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: msg, + Reason: "DuplicateForGVK", + Type: string(compositionv1alpha1.Error), + Status: metav1.ConditionTrue, + }) + return errDuplicate + } + logger.Info("Sending event to handoff channel") + r.handoffChannels[gvk] <- event.GenericEvent{ + Object: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.Name, + Namespace: c.Namespace, + }, + }, + } + + // Reconciler already exists nothing to be done + logger.Info("Reconciler already exists for InputAPI CRD") + return nil + } + + logger.Info("Starting Reconciler for InputAPI CRD") + r.handoffChannels[gvk] = make(chan event.GenericEvent) + expanderController := &ExpanderReconciler{ + Client: r.Client, + Recorder: r.mgr.GetEventRecorderFor(crd.Spec.Names.Plural + "-expander"), + Scheme: r.Scheme, + InputGVK: gvk, + Composition: types.NamespacedName{Name: c.Name, Namespace: c.Namespace}, + InputGVR: gvk.GroupVersion().WithResource(crd.Spec.Names.Plural), + RESTMapper: r.mgr.GetRESTMapper(), + Config: r.mgr.GetConfig(), + CompositionChangedWatcher: r.handoffChannels[gvk], + } + + if err := expanderController.SetupWithManager(r.mgr, cr); err != nil { + c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: err.Error(), + Reason: "InternalError", + Type: string(compositionv1alpha1.Error), + Status: metav1.ConditionTrue, + }) + logger.Error(err, "Failed to start reconciler for InputAPI CRD") + return err + } + r.Recorder.Event(c, "Normal", "InputReconcilerStarted", + fmt.Sprintf("Reconciler started for Facade CR: %s", c.Spec.InputAPIGroup)) + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *CompositionReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.mgr = mgr + r.handoffChannels = make(map[schema.GroupVersionKind]chan event.GenericEvent) + return ctrl.NewControllerManagedBy(mgr). + For(&compositionv1alpha1.Composition{}). + Complete(r) +} diff --git a/composition/internal/controller/context_controller.go b/composition/internal/controller/context_controller.go new file mode 100644 index 0000000..3f90c37 --- /dev/null +++ b/composition/internal/controller/context_controller.go @@ -0,0 +1,56 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +// ContextReconciler reconciles a Context object +type ContextReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=composition.google.com,resources=contexts,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=composition.google.com,resources=contexts/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=composition.google.com,resources=contexts/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile +func (r *ContextReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + // No Op + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ContextReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&compositionv1alpha1.Context{}). + Complete(r) +} diff --git a/composition/internal/controller/context_controller_test.go b/composition/internal/controller/context_controller_test.go new file mode 100644 index 0000000..b38d784 --- /dev/null +++ b/composition/internal/controller/context_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +var _ = Describe("Context Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + context := &compositionv1alpha1.Context{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Context") + err := k8sClient.Get(ctx, typeNamespacedName, context) + if err != nil && errors.IsNotFound(err) { + resource := &compositionv1alpha1.Context{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &compositionv1alpha1.Context{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Context") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ContextReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/composition/internal/controller/expander_reconciler.go b/composition/internal/controller/expander_reconciler.go new file mode 100644 index 0000000..07d6862 --- /dev/null +++ b/composition/internal/controller/expander_reconciler.go @@ -0,0 +1,889 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/composition/pkg/applier" + "github.com/cloud-native-compositions/compositions/composition/pkg/containerexecutor/jobcontainerexecutor" + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "github.com/go-logr/logr" + "golang.org/x/time/rate" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/kubebuilder-declarative-pattern/applylib/forked/github.com/kubernetes/kubectl/pkg/cmd/apply" +) + +// ExpanderReconciler reconciles a expander object +type ExpanderReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + RESTMapper meta.RESTMapper + Config *rest.Config + Dynamic *dynamic.DynamicClient + InputGVK schema.GroupVersionKind + InputGVR schema.GroupVersionResource + Composition types.NamespacedName + CompositionChangedWatcher chan event.GenericEvent +} + +type EvaluateWaitError struct { + msg string +} + +const ( + finalizerName = "compositions.google.com/expander-finalizer" + stagesAnnotation = "compositions.google.com/expander-stages" +) + +func (e *EvaluateWaitError) Error() string { return e.msg } + +var contextGVK schema.GroupVersionKind = schema.GroupVersionKind{ + Group: "composition.google.com", + Version: "v1alpha1", + Kind: "Context", +} + +func (r *ExpanderReconciler) updatePlanStatus(ctx context.Context, plancr *compositionv1alpha1.Plan, newStatus *compositionv1alpha1.PlanStatus) { + logger := log.FromContext(ctx) + logger = logger.WithName(r.Composition.Name).WithName(r.InputGVK.Group) + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + nn := types.NamespacedName{Namespace: plancr.Namespace, Name: plancr.Name} + err := r.Client.Get(ctx, nn, plancr) + if err != nil { + return err + } + plancr.Status = *newStatus.DeepCopy() + return r.Client.Status().Update(ctx, plancr) + }) + if err != nil { + logger.Error(err, "unable to update Plan status", "name", plancr.Name, "namespace", plancr.Namespace) + } +} + +func (r *ExpanderReconciler) getPlanForInputCR(ctx context.Context, inputcr *unstructured.Unstructured) (types.NamespacedName, + *compositionv1alpha1.Plan, error) { + var plancr compositionv1alpha1.Plan + logger := log.FromContext(ctx) + logger = logger.WithName(r.Composition.Name).WithName(r.InputGVK.Group).WithName(inputcr.GetName()) + + planNN := types.NamespacedName{ + Name: r.InputGVR.Resource + "-" + inputcr.GetName(), + Namespace: inputcr.GetNamespace(), + } + if err := r.Client.Get(ctx, planNN, &plancr); err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error(err, "Unable to fetch Plan Object") + return planNN, &plancr, err + } + // create a plan object + plancr = compositionv1alpha1.Plan{ + ObjectMeta: metav1.ObjectMeta{ + Name: planNN.Name, + Namespace: planNN.Namespace, + }, + Spec: compositionv1alpha1.PlanSpec{ + Stages: map[string]compositionv1alpha1.Stage{}, + }, + Status: compositionv1alpha1.PlanStatus{ + Stages: map[string]*compositionv1alpha1.StageStatus{}, + }, + } + if err := ctrl.SetControllerReference(inputcr, &plancr, r.Scheme); err != nil { + logger.Error(err, "Unable to set controller reference for Plan Object", + "name", plancr.Name, "namespace", plancr.Namespace) + return planNN, &plancr, err + } + if err := r.Client.Create(ctx, &plancr); err != nil { + logger.Error(err, "Unable to create Plan Object") + return planNN, &plancr, err + } + + // Read after create to get the UID + if err := r.Client.Get(ctx, planNN, &plancr); err != nil { + logger.Error(err, "Unable to fetch Plan Object") + return planNN, &plancr, err + } + } + + // Add a finalizer to the Plan to discourage out of band deletions. Stage information is + // stored in the Plan to handle proper cleanup of resources when a Facade is deleted. + if !controllerutil.ContainsFinalizer(&plancr, finalizerName) { + controllerutil.AddFinalizer(&plancr, finalizerName) + if err := r.Update(ctx, &plancr); err != nil { + logger.Error(err, "Unable to add finalizer to Plan Object") + return planNN, &plancr, err + } + } + return planNN, &plancr, nil +} + +func (r *ExpanderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger = logger.WithName(r.Composition.Name).WithName(r.InputGVK.Group) + + logger.Info("Got Input API for expansion", "request", req) + + inputcr := unstructured.Unstructured{} + inputcr.SetGroupVersionKind(r.InputGVK) + logger = logger.WithName(req.NamespacedName.Name).WithName(req.NamespacedName.Namespace) + if err := r.Get(ctx, req.NamespacedName, &inputcr); err != nil { + logger.Error(err, "unable to fetch Input API Object") + // 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. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !inputcr.GetDeletionTimestamp().IsZero() { + return r.reconcileDelete(ctx, logger, inputcr) + } + // Add a finalizer to prevent removal of facade before all applied objects are cleaned up. + if !controllerutil.ContainsFinalizer(&inputcr, finalizerName) { + controllerutil.AddFinalizer(&inputcr, finalizerName) + if err := r.Update(ctx, &inputcr); err != nil { + logger.Error(err, "Unable to add finalizer to input CR") + return ctrl.Result{}, err + } + } + + // Grab the latest composition + // TODO(barni@) - Decide how we want the latest composition changes are to be applied. + var compositionCR compositionv1alpha1.Composition + if err := r.Client.Get(ctx, r.Composition, &compositionCR); err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error(err, "Unable to fetch Composition Object") + return ctrl.Result{}, err + } + } + + // Associate a plan object with this input CR + planNN, plancr, err := r.getPlanForInputCR(ctx, &inputcr) + if err != nil { + logger.Error(err, "Unable to get Plan Object") + return ctrl.Result{}, err + } + + // Since applylib doesnt support multiple apply batches we are + // tracking all old objects and accumulating them each apply. + oldAppliers := []*applier.Applier{} + + // Create a new status for comparison later + newStatus := compositionv1alpha1.PlanStatus{ + Stages: map[string]*compositionv1alpha1.StageStatus{}, + // TODO: Accumulates LastPruned. + // Ideally we need to reset if input/composition gen changes + LastPruned: plancr.Status.LastPruned, + } + + // Try updating status before returning + defer r.updatePlanStatus(ctx, plancr, &newStatus) + + expanderDebugLogsEnabled := false + _, exist := inputcr.GetAnnotations()["composition-expander-debug-logs"] + if exist { + expanderDebugLogsEnabled = true + logger.Info("annotation'composition-expander-debug-logs' is turned on.") + } + logger.Info("annotation'composition-expander-debug-logs' is turned off.") + + // Write out the (in-order) stages to the plan as a reference for later when we need to delete resources. + stagesEvaluated := []string{} + for index := range compositionCR.Spec.Expanders { + stagesEvaluated = append(stagesEvaluated, compositionCR.Spec.Expanders[index].Name) + } + metav1.SetMetaDataAnnotation(&plancr.ObjectMeta, stagesAnnotation, strings.Join(stagesEvaluated, ",")) + err = r.Client.Update(ctx, plancr) + if err != nil { + logger.Error(err, "error setting annotation in plan", "plan", planNN, "annotation", stagesAnnotation) + return ctrl.Result{}, err + } + + // Grab a top level logger so we can add expander name in the eval and apply sections + loggerCR := logger + stagesApplied := []string{} + values := map[string]interface{}{} + requeueAgain := false + + // ---------- Evaluate and Apply expanders in order --------------------- + for index, expander := range compositionCR.Spec.Expanders { + planUpdated := false + reason := "" + + // ------------------- EVALUATION SECTION ----------------------- + + values, planUpdated, reason, err = r.evaluate(ctx, logger, &inputcr, planNN, expander, values, expanderDebugLogsEnabled) + _, iswaitErr := err.(*EvaluateWaitError) + if iswaitErr { + newStatus.AppendWaitingCondition(expander.Name, err.Error(), reason) + // Subsume the error + requeueAgain = true + break + } + + if err != nil { + // Skip apply phase and return + newStatus.AppendErrorCondition(expander.Name, err.Error(), reason) + return ctrl.Result{}, err + } + + // Inject plan.Ready Condition with list of expanders + newStatus.ClearCondition(compositionv1alpha1.Ready) + message := fmt.Sprintf("Evaluated stage: %s", expander.Name) + newStatus.AppendCondition(compositionv1alpha1.Ready, metav1.ConditionFalse, message, "EvaluationPending") + + // ------------------- APPLIER SECTION ----------------------- + + logger = loggerCR.WithName(expander.Name).WithName("Apply") + // Re-read the Plan CR to load the expanded manifests + oldGeneration := plancr.GetGeneration() + if err := r.Client.Get(ctx, planNN, plancr); err != nil { + logger.Error(err, "unable to read Plan CR") + return ctrl.Result{}, err + } + + if planUpdated && oldGeneration == plancr.GetGeneration() { + logger.Info("Did not get the latest Plan CR. Will retry.", "generation", oldGeneration) + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + stage, ok := plancr.Spec.Stages[expander.Name] + if !ok { + err := fmt.Errorf("plancr.spec.stages[%s] not found !!", stage) + logger.Error(err, "error applying stage", "stage", stage) + // This is not expected since we just processed the stage above + // We dont want to return error. Lets retry again in 20 secs. + return ctrl.Result{RequeueAfter: 20 * time.Second}, nil + } + if stage.Values != "" { + // This looks like an Getter stage. skip it + continue + } + + // Lets not make empty manifests from a stage an error + // We may have conditional code that generates no manifests in a stage + // We will log it though + if stage.Manifest == "" { + logger.Info("Empty manifests returned for stage", "stage", stage) + } + + // Create Applier and wait for the Applier to complete + ac := applier.ApplierClient{ + Client: r.Client, + Dynamic: r.Dynamic, + RESTMapper: r.RESTMapper, + } + namespace := "" + if compositionCR.Spec.NamespaceMode != compositionv1alpha1.NamespaceModeExplicit { + namespace = inputcr.GetNamespace() + } + applier := applier.NewApplier(ctx, logger, ac, expander.Name, namespace, r.InputGVR.Resource, plancr, compositionCR.Spec.Readiness) + err = applier.Load() // Load Manifests + if err != nil { + r.Recorder.Event(&inputcr, "Warning", "ApplyFailed", fmt.Sprintf("error loading manifests for expander, name: %s", expander.Name)) + logger.Error(err, "Unable to Load manifests for applying") + // Inject plan.ERROR Condition + newStatus.AppendErrorCondition(expander.Name, err.Error(), "FailedLoadingManifestsFromPlan") + return ctrl.Result{}, err + } + logger.Info("Successfully loaded manifests for applying") + if newStatus.Stages == nil { + newStatus.Stages = map[string]*compositionv1alpha1.StageStatus{} + } + newStatus.Stages[expander.Name] = &compositionv1alpha1.StageStatus{ResourceCount: applier.Count()} + + // Prune only for the last expander section + prune := false + if index == len(compositionCR.Spec.Expanders)-1 { + prune = true + } + + err = applier.Apply(oldAppliers, prune) // Apply Manifests + applier.UpdateStageStatus(&newStatus) + if err != nil { + r.Recorder.Event(&inputcr, "Warning", "ApplyFailed", + fmt.Sprintf("error applying manifests for expander, name: %s", expander.Name)) + logger.Error(err, "Unable to apply manifests") + // Inject plan.ERROR Condition + newStatus.AppendErrorCondition(expander.Name, err.Error(), "FailedApplyingManifests") + return ctrl.Result{}, err + } + + logger.Info("Successfully applied manifests") + r.Recorder.Event(&inputcr, "Normal", "ResourcesApplied", fmt.Sprintf("All expanded resources were applied. name: %s", expander.Name)) + + ready, err := applier.AreResourcesReady() + applier.UpdateStageStatus(&newStatus) + if err != nil { + r.Recorder.Event(&inputcr, "Warning", "ReconcileFailed", fmt.Sprintf("Failed waiting for resources to be reconciled. name: %s", expander.Name)) + logger.Error(err, "Failed waiting for applied resources to reconcile") + // Inject plan.ERROR Condition + newStatus.AppendErrorCondition(expander.Name, err.Error(), "FailedWaitingForAppliedResources") + return ctrl.Result{}, err + } + + oldAppliers = append(oldAppliers, applier) + + if !ready { + r.Recorder.Event(&inputcr, "Warning", "ReconcileFailed", fmt.Sprintf("Some resources are not healthy. name: %s", expander.Name)) + logger.Info("Applied successfully but some resources did not become healthy") + // Inject plan.Waiting Condition + newStatus.AppendWaitingCondition(expander.Name, "Not all resources are healthy", "WaitingForAppliedResources") + + // Request a re-reconcile + requeueAgain = true + break + } + logger.Info("Applied resources successfully.") + + // Implicit getter: Make the applied objects available in the values passed to subsequent stages + values = applier.AddAppliedObjectsIntoValues(values) + + stagesApplied = append(stagesApplied, expander.Name) + r.Recorder.Event(&inputcr, "Normal", "ResourcesReconciled", fmt.Sprintf("All applied resources were reconciled. name: %s", expander.Name)) + // Inject plan.Ready Condition with list of expanders + newStatus.ClearCondition(compositionv1alpha1.Ready) + message = fmt.Sprintf("Applied stages: %s", strings.Join(stagesApplied, ", ")) + newStatus.AppendCondition(compositionv1alpha1.Ready, metav1.ConditionFalse, message, "PendingStages") + + if expanderDebugLogsEnabled { + r.Recorder.Event(&inputcr, "Normal", fmt.Sprintf("Finished expander stage %d: %s", index, expander.Name), expanderDebugLog(&inputcr)+fmt.Sprintf("---resource count: %d", applier.Count())) + for i, resourceStatus := range newStatus.Stages[expander.Name].LastApplied { + logger.Info("Expander debug logs", "Resource", i, "Name", resourceStatus.Name, "Namespace", resourceStatus.Namespace, "Group", + resourceStatus.Group, "Version", resourceStatus.Version, "Kind", resourceStatus.Kind, "Status", resourceStatus.Health) + } + } + } + + // Inject plan.Ready Condition with list of expanders + newStatus.ClearCondition(compositionv1alpha1.Ready) + message := fmt.Sprintf("Evaluated and Applied stages: %s", strings.Join(stagesApplied, ", ")) + newStatus.AppendCondition(compositionv1alpha1.Ready, metav1.ConditionTrue, message, "ProcessedAllStages") + newStatus.InputGeneration = inputcr.GetGeneration() + newStatus.Generation = plancr.GetGeneration() + newStatus.CompositionGeneration = compositionCR.GetGeneration() + newStatus.CompositionUID = compositionCR.GetUID() + if requeueAgain { + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + return ctrl.Result{}, nil +} + +func (r *ExpanderReconciler) evaluate(ctx context.Context, logger logr.Logger, + cr *unstructured.Unstructured, planNN types.NamespacedName, + expander compositionv1alpha1.Expander, values map[string]interface{}, + expanderDebugLogsEnabled bool) (map[string]interface{}, bool, string, error) { + + planUpdated := false + + logger = logger.WithName(expander.Name).WithName("Expand") + + uri, ev, reason, err := r.getExpanderConfig(ctx, expander.Version, expander.Type) + if err != nil { + logger.Error(err, "Error getting expander version", "expander", expander.Type, + "version", expander.Version, "reason", reason) + return values, planUpdated, reason, err + } + + logger.Info("Got valid expander uri", "uri", uri) + + if expanderDebugLogsEnabled { + r.Recorder.Event(cr, "Normal", fmt.Sprintf("Running expander stage: %s", expander.Name), expanderDebugLog(cr)) + } + + if ev.Spec.Type == compositionv1alpha1.ExpanderTypeJob { + reason, err = r.runJob(ctx, logger, cr, expander.Name, planNN.Name, uri, ev.Spec.ImageRegistry) + } else { + values, planUpdated, reason, err = r.evaluateAndSavePlan(ctx, logger, cr, values, expander, planNN, ev, uri, expanderDebugLogsEnabled) + } + + if err == nil && expanderDebugLogsEnabled { + r.Recorder.Event(cr, "Normal", fmt.Sprintf("Evaluated expander stage: %s", expander.Name), expanderDebugLog(cr)) + } + + return values, planUpdated, reason, err + +} + +func (r *ExpanderReconciler) getExpanderConfig( + ctx context.Context, inputExpanderVersion string, expanderType string, +) (string, *compositionv1alpha1.ExpanderVersion, string, error) { + logger := log.FromContext(ctx) + + value := "" + var ev compositionv1alpha1.ExpanderVersion + err := r.Client.Get(ctx, + types.NamespacedName{ + Name: "composition-" + expanderType, + Namespace: "composition-system"}, + &ev) + + if err != nil { + // The CR should be created before the specified expander can be used. + logger.Error(err, "Failed to get the ExpanderVersionCR") + if apierrors.IsNotFound(err) { + return value, nil, "MissingExpanderCR", err + } else { + return value, nil, "ErrorGettingExpanderVersionCR", err + } + } + + if ev.Status.VersionMap == nil { + return value, nil, "ErrorEmptyVersionMap", fmt.Errorf("ExpanderVersion .status.versionMap is empty") + } + + logger.Info("input expander version", "current", inputExpanderVersion) + value, ok := ev.Status.VersionMap[inputExpanderVersion] + if !ok { + return value, nil, "VersionNotFound", fmt.Errorf("%s version not found", inputExpanderVersion) + } + return value, &ev, "", nil +} + +func (r *ExpanderReconciler) runJob(ctx context.Context, logger logr.Logger, + cr *unstructured.Unstructured, expanderName, planName, image, registry string) (string, error) { + jf := jobcontainerexecutor.NewJobFactory(ctx, logger, r.Client, r.InputGVK, r.InputGVR, + r.Composition.Name, r.Composition.Namespace, + cr, expanderName, image, planName, registry) + + // Create Expander Job and wait for the Job to complete + logger.Info("Creating expander job") + err := jf.Create() + defer jf.CleanUp() + if err != nil { + // Reference for event API: Event(object runtime.Object, eventtype, reason, message string) + r.Recorder.Event(cr, "Warning", "ExpansionFailed", fmt.Sprintf("Error creating job for name: %s", expanderName)) + logger.Error(err, "Unable to create expander job") + return "ExpanderJobCreationFailed", err + } + r.Recorder.Event(cr, "Normal", "ExpansionStarted", fmt.Sprintf("Job Created for name: %s", expanderName)) + logger.Info("Successfully created expander job") + + success, err := jf.Wait() + if err != nil { + r.Recorder.Event(cr, "Warning", "ExpansionFailed", fmt.Sprintf("Error waiting for Job for name: %s", expanderName)) + logger.Error(err, "Failed waiting for expander job") + return "WaitingForExpanderFailed", err + } + + if !success { + r.Recorder.Event(cr, "Warning", "ExpansionFailed", fmt.Sprintf("Job failed for name: %s", expanderName)) + logger.Info("Expander job completed but Failed") + return "ExpansionFailed", fmt.Errorf("Expander Job Failed") + } + r.Recorder.Event(cr, "Normal", "ExpansionSucceeded", fmt.Sprintf("Job succeeded for name: %s", expanderName)) + logger.Info("Expander job Completed successfully") + return "", nil +} + +func (r *ExpanderReconciler) evaluateAndSavePlan(ctx context.Context, logger logr.Logger, + cr *unstructured.Unstructured, values map[string]interface{}, expander compositionv1alpha1.Expander, + planNN types.NamespacedName, ev *compositionv1alpha1.ExpanderVersion, grpcService string, expanderDebugLogEnabled bool) (map[string]interface{}, bool, string, error) { + // Set up a connection to the server. + updated := false + + conn, err := grpc.NewClient(grpcService, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + logger.Error(err, "grpc dial failed: "+grpcService) + return values, updated, "GRPCConnError", err + } + + // read context in cr.namespace + var contextBytes []byte + contextcr := unstructured.Unstructured{} + contextcr.SetGroupVersionKind(contextGVK) + contextNN := types.NamespacedName{Namespace: cr.GetNamespace(), Name: "context"} + if err := r.Get(ctx, contextNN, &contextcr); err != nil { + logger.Error(err, "unable to fetch Context CR", "context", contextNN) + if !apierrors.IsNotFound(err) { + return values, updated, "ErrorGettingContext", err + } + // If context doesnt exist ignore it. If a composition uses context, + // it will fail evaluation + } else { + contextBytes, err = json.Marshal(contextcr.Object) + if err != nil { + logger.Error(err, "failed to marshal Context Object") + return values, updated, "MarshallContextFailed", err + } + } + + // marshall facade cr + facadeBytes, err := json.Marshal(cr.Object) + if err != nil { + logger.Error(err, "failed to marshall Facade Object") + return values, updated, "MarshallFacadeFailed", err + } + + // marshall expander config + // read Expander config from in cr.namespace + var configBytes []byte + if expander.ConfigRef != nil { + expanderconfigcr := unstructured.Unstructured{} + expanderconfigcr.SetGroupVersionKind(schema.GroupVersionKind{ + Group: ev.Spec.Config.Group, + Version: ev.Spec.Config.Version, + Kind: ev.Spec.Config.Kind, + }) + expanderconfigNN := types.NamespacedName{Namespace: expander.ConfigRef.Namespace, Name: expander.ConfigRef.Name} + if err := r.Get(ctx, expanderconfigNN, &expanderconfigcr); err != nil { + logger.Error(err, "unable to fetch ExpanderConfig CR", "expander config", expanderconfigNN) + return values, updated, "GetExpanderConfigFailed", err + } + configBytes, err = json.Marshal(expanderconfigcr.Object) + if err != nil { + logger.Error(err, "failed to marshal ExpanderConfig Object") + return values, updated, "MarshallExpanderConfigFailed", err + } + } else { + // TODO check if json.Marshall is escaping quotes + // Also causes > to be replaced unicode 'if loop.index \u003e 1' + err = nil + //configBytes, err = json.Marshal(expander.Template) + configBytes = []byte(expander.Template) + if err != nil { + logger.Error(err, "failed to marshall Expander template") + return values, updated, "MarshallExpanderTemplateFailed", err + } + } + + // marshall getter values + valuesBytes, err := json.Marshal(values) + if err != nil { + logger.Error(err, "failed to marshall Getter Values") + return values, updated, "MarshallValuesFailed", err + } + evaluateRequest := &pb.EvaluateRequest{ + Config: configBytes, + Context: contextBytes, + Facade: facadeBytes, + Resource: r.InputGVR.Resource, + Value: valuesBytes, + } + expanderClient := pb.NewExpanderClient(conn) + if expanderDebugLogEnabled { + logger.Info(expanderDebugLog(cr) + fmt.Sprintf("---sending expander request: %v", evaluateRequest)) + } + result, err := expanderClient.Evaluate(ctx, evaluateRequest) + if err != nil { + logger.Error(err, "expander.Evaluate() Failed", "expander", expander.Name) + return values, updated, "EvaluateError", err + } + if result.Status == pb.Status_EVALUATE_WAIT { + logger.Error(nil, "expander.Evaluate() returned WAIT", "expander", expander.Name, "status", result.Status, "msg", result.Error.Message) + err = &EvaluateWaitError{msg: fmt.Sprintf("Expander returned WAIT: %s", result.Error.Message)} + return values, updated, "EvaluateStatusWait", err + } + if result.Status != pb.Status_SUCCESS { + logger.Error(nil, "expander.Evaluate() Status is not Success", "expander", expander.Name, "status", result.Status) + err = fmt.Errorf("Evaluate Failed: %s", result.Error.Message) + return values, updated, "EvaluateStatusFailed", err + } + if expanderDebugLogEnabled { + logger.Info(expanderDebugLog(cr) + fmt.Sprintf("---sent expander request: %v, received results: %v", evaluateRequest, result)) + r.Recorder.Event(cr, "Normal", fmt.Sprintf("Expander stage %s evaluation completed", expander.Name), expanderDebugLog(cr)+fmt.Sprintf("---request: %v, result: %v", evaluateRequest, result)) + } + + // Write to Plan object + // Re-read the Plan CR to load the expanded manifests + plancr := compositionv1alpha1.Plan{} + if err := r.Client.Get(ctx, planNN, &plancr); err != nil { + logger.Error(err, "unable to read Plan CR", "plan", planNN) + return values, updated, "GetPlanFailed", err + } + + if plancr.Spec.Stages == nil { + plancr.Spec.Stages = map[string]compositionv1alpha1.Stage{} + updated = true + } + if _, ok := plancr.Spec.Stages[expander.Name]; !ok { + updated = true + } + + if result.Type == pb.ResultType_MANIFESTS { + err = nil + //s, err := strconv.Unquote(string(result.Manifests)) + s := string(result.Manifests) + if err != nil { + logger.Error(err, "unable to unquote grpc response") + return values, updated, "UnquoteResponseFailed", err + } + if s != plancr.Spec.Stages[expander.Name].Manifest { + plancr.Spec.Stages[expander.Name] = compositionv1alpha1.Stage{ + Manifest: s, + } + updated = true + } + } else { + //s, err := strconv.Unquote(string(result.Values)) + //if err != nil { + // logger.Error(err, "unable to unquote grpc response") + // return values, updated, "UnquoteResponseFailed", err + //} + s := string(result.Values) + if s != plancr.Spec.Stages[expander.Name].Values { + plancr.Spec.Stages[expander.Name] = compositionv1alpha1.Stage{ + Values: s, + } + updated = true + } + + // Grab values for donwstream stages + stageValues := map[string]any{} + err = json.Unmarshal([]byte(plancr.Spec.Stages[expander.Name].Values), &stageValues) + if err != nil { + logger.Error(err, "Failed unmarshalling response.Values field") + return values, updated, "UnmarshallValuesFailed", err + } + for k := range stageValues { + _, ok := values[k] + if ok { + err := fmt.Errorf("values[%s] already exists from one of the previous stages.", k) + logger.Error(err, "Duplicate Value Key") + return values, updated, "DuplicateValueKey", err + } + values[k] = stageValues[k] + } + } + + err = r.Client.Update(ctx, &plancr) + if err != nil { + logger.Error(err, "error updating plan", "plan", planNN) + return values, updated, "UpdatePlanFailed", err + } + + return values, updated, "", nil +} + +func expanderDebugLog(cr *unstructured.Unstructured) string { + // TODO(@xiaoweim): add the UID in the future if possible + return fmt.Sprintf("expanderDebugLog---%s/%s/%s---version: %d", cr.GetKind(), cr.GetNamespace(), cr.GetName(), cr.GetGeneration()) +} + +func (r *ExpanderReconciler) enqueueAllFromGVK(ctx context.Context, _ client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + logger.Info("Got notification of changed CRD") + inputcrList := &unstructured.UnstructuredList{} + inputcrList.SetGroupVersionKind(r.InputGVK) + if err := r.List(ctx, inputcrList); err != nil { + logger.Error(err, "unable to fetch Input API Objects") + return nil + } + if len(inputcrList.Items) == 0 { + return nil + } + + reqs := []reconcile.Request{} + // TODO: If there are lots of objects, this will result in very many reconciles. Have not tested to see how the + // queue copes with this. If it becomes a problem, this will need a rethink. + for _, inputcr := range inputcrList.Items { + nn := types.NamespacedName{Name: inputcr.GetName(), Namespace: inputcr.GetNamespace()} + reqs = append(reqs, reconcile.Request{NamespacedName: nn}) + } + return reqs +} + +func (r *ExpanderReconciler) reconcileDelete(ctx context.Context, logger logr.Logger, inputcr unstructured.Unstructured) (ctrl.Result, error) { + logger = logger.WithName("Delete") + if !controllerutil.ContainsFinalizer(&inputcr, finalizerName) { + return ctrl.Result{}, nil + } + planNN := types.NamespacedName{ + Name: r.InputGVR.Resource + "-" + inputcr.GetName(), + Namespace: inputcr.GetNamespace(), + } + plancr := compositionv1alpha1.Plan{} + if err := r.Client.Get(ctx, planNN, &plancr); err != nil { + logger.Error(err, "Unable to fetch Plan Object", "plan", planNN) + return ctrl.Result{}, err + } + annotations := plancr.GetAnnotations() + planLabels := plancr.GetLabels() + stageList, ok := annotations[stagesAnnotation] + if !ok { + err := fmt.Errorf("Plan is missing stage order annotation") + logger.Error(err, "Unable to fetch stage order from Plan", "Plan", planNN) + return ctrl.Result{}, err + } + stages := strings.Split(stageList, ",") + numFound := 0 + for i := len(stages) - 1; i >= 0; i-- { + r.Recorder.Eventf(&inputcr, corev1.EventTypeNormal, "Delete", "Deleting objects for stage %s", stages[i]) + nsList, ok := annotations[apply.ApplySetAdditionalNamespacesAnnotation] + if !ok { + err := fmt.Errorf("Plan is missing Namespace annotation") + logger.Error(err, "Unable to fetch Namespaces from Plan", "Plan", planNN) + return ctrl.Result{}, err + } + namespaces := strings.Split(nsList, ",") + namespaces = append(namespaces, inputcr.GetNamespace()) + opts, err := deleteListOpts(stages[i], planLabels[apply.ApplySetParentIDLabel]) + if err != nil { + logger.Error(err, "Error creating list options") + return ctrl.Result{}, err + } + gkList, ok := annotations[apply.ApplySetGKsAnnotation] + if !ok { + err := fmt.Errorf("Plan is missing GroupKind annotation") + logger.Error(err, "Unable to fetch GroupKinds from Plan", "Plan", planNN) + return ctrl.Result{}, err + } + gks := strings.Split(gkList, ",") + for _, gk := range gks { + parsedGK := schema.ParseGroupKind(gk) + mapping, err := r.RESTMapper.RESTMapping(parsedGK) + if err != nil { + return ctrl.Result{}, err + } + n := 0 + if mapping.Scope.Name() == meta.RESTScopeNameNamespace { + n, err = r.deleteNamespacedResources(ctx, logger, stages[i], mapping.Resource, namespaces, opts) + } else if mapping.Scope.Name() == meta.RESTScopeNameRoot { + n, err = r.deleteClusterResources(ctx, logger, stages[i], mapping.Resource, opts) + } + if err != nil { + logger.Error(err, "Error deleting resources", "GroupKind", gk) + r.Recorder.Eventf(&inputcr, corev1.EventTypeWarning, "Delete", "Failed deleting objects of GroupKind %q for stage %q: %v", gk, stages[i], err) + return ctrl.Result{}, err + } + numFound += n + } + if numFound > 0 { + break + } + } + if numFound > 0 { + return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, nil + } + + // Remove the finalizers to allow the Plan and Facade to be deleted. + controllerutil.RemoveFinalizer(&plancr, finalizerName) + if err := r.Update(ctx, &plancr); err != nil { + logger.Error(err, "Unable to remove finalizer from Plan") + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(&inputcr, finalizerName) + if err := r.Update(ctx, &inputcr); err != nil { + logger.Error(err, "Unable to remove finalizer from input CR") + return ctrl.Result{}, err + } + // Expanded resources are all deleted, stop reconciliation. + return ctrl.Result{}, nil +} + +func deleteListOpts(stage, applysetId string) (metav1.ListOptions, error) { + stageReq, err := labels.NewRequirement(applier.StageLabel, selection.Equals, []string{stage}) + if err != nil { + return metav1.ListOptions{}, fmt.Errorf("failed making stage label selector: %v", err) + } + asReq, err := labels.NewRequirement(apply.ApplysetPartOfLabel, selection.Equals, []string{applysetId}) + if err != nil { + return metav1.ListOptions{}, fmt.Errorf("failed making applysetId label selector: %v", err) + } + opts := metav1.ListOptions{ + LabelSelector: labels.NewSelector().Add(*stageReq).Add(*asReq).String(), + } + return opts, nil +} + +func (r *ExpanderReconciler) deleteNamespacedResources(ctx context.Context, logger logr.Logger, stage string, endpoint schema.GroupVersionResource, namespaces []string, opts metav1.ListOptions) (int, error) { + numFound := 0 + for _, ns := range namespaces { + resources, err := r.Dynamic.Resource(endpoint).Namespace(ns).List(ctx, opts) + if err != nil { + return numFound, fmt.Errorf("error listing resources in Namespace %q for stage %q: %v", ns, stage, err) + } + for _, res := range resources.Items { + logger.Info("Attempting to delete resource", "Resource", res, "Namespace", ns) + err := r.Delete(ctx, &res) + if err == nil { + numFound++ + } else if err != nil && !apierrors.IsNotFound(err) { + return numFound, fmt.Errorf("failed deleting object %v in Namespace %q for stage %q: %v", res, ns, stage, err) + } + } + } + return numFound, nil +} + +func (r *ExpanderReconciler) deleteClusterResources(ctx context.Context, logger logr.Logger, stage string, endpoint schema.GroupVersionResource, opts metav1.ListOptions) (int, error) { + numFound := 0 + resources, err := r.Dynamic.Resource(endpoint).List(ctx, opts) + if err != nil { + return numFound, fmt.Errorf("error listing resources for stage %q: %v", stage, err) + } + for _, res := range resources.Items { + logger.Info("Attempting to delete resource", "Resource", res) + err := r.Delete(ctx, &res) + if err == nil { + numFound++ + } else if err != nil && !apierrors.IsNotFound(err) { + return numFound, fmt.Errorf("failed deleting object %v for stage %q: %v", res, stage, err) + } + } + return numFound, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ExpanderReconciler) SetupWithManager(mgr ctrl.Manager, cr *unstructured.Unstructured) error { + var err error + // TODO(barni@): Can we setup dynamic controller at main.go for CompositionReconciler instead of 1 per ExpanderReconciler + r.Dynamic, err = dynamic.NewForConfig(r.Config) + if err != nil { + return fmt.Errorf("error building dynamic client: %w", err) + } + + ratelimiter := workqueue.NewMaxOfRateLimiter( + workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 120*time.Second), + // 40 qps, 400 bucket size. This is only for retry speed and its only the overall factor (not per item) + &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(40), 400)}, + ) + + return ctrl.NewControllerManagedBy(mgr). + For(cr). + WatchesRawSource(&source.Channel{Source: r.CompositionChangedWatcher}, handler.EnqueueRequestsFromMapFunc(r.enqueueAllFromGVK)). + WithOptions(controller.Options{RateLimiter: ratelimiter}). + Complete(r) +} diff --git a/composition/internal/controller/expanderversion_controller.go b/composition/internal/controller/expanderversion_controller.go new file mode 100644 index 0000000..efffb4b --- /dev/null +++ b/composition/internal/controller/expanderversion_controller.go @@ -0,0 +1,150 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "reflect" + "sort" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +// ExpanderVersionReconciler reconciles a ExpanderVersion object +type ExpanderVersionReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=composition.google.com,resources=expanderversions,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=composition.google.com,resources=expanderversions/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=composition.google.com,resources=expanderversions/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the ExpanderVersion object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile +func (r *ExpanderVersionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("Got a new request!", "request", req) + + var ev compositionv1alpha1.ExpanderVersion + if err := r.Client.Get(ctx, req.NamespacedName, &ev); err != nil { + logger.Error(err, "unable to fetch ExpanderVersion") + // 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. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Grab status for comparison later + oldStatus := ev.Status.DeepCopy() + + // Try updating status before returning + defer func() { + if !reflect.DeepEqual(oldStatus, ev.Status) { + newStatus := ev.Status.DeepCopy() + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + nn := types.NamespacedName{Namespace: ev.Namespace, Name: ev.Name} + err := r.Client.Get(ctx, nn, &ev) + if err != nil { + return err + } + ev.Status = *newStatus.DeepCopy() + return r.Client.Status().Update(ctx, &ev) + }) + if err != nil { + logger.Error(err, "unable to update Composition status") + } + } + }() + + logger = logger.WithName(ev.Name).WithName(fmt.Sprintf("%d", ev.Generation)) + + logger.Info("Validating ExpanderVersion object") + if !ev.Validate() { + logger.Info("Validation Failed") + return ctrl.Result{}, fmt.Errorf("Validation failed") + } + + ev.Status.ClearCondition(compositionv1alpha1.Error) + logger.Info("Processing ExpanderVersion object") + r.processExpanderVersion(&ev, logger) + return ctrl.Result{}, nil +} + +func (r *ExpanderVersionReconciler) processExpanderVersion( + ev *compositionv1alpha1.ExpanderVersion, logger logr.Logger, +) { + if ev.Status.VersionMap == nil { + ev.Status.VersionMap = make(map[string]string) + } + + expander := strings.TrimPrefix(ev.Name, "composition-") + image := ev.Spec.Image + if image == "" { + image = fmt.Sprintf("expander-%s", expander) + } + semVerVersions := []*semver.Version{} + for _, r := range ev.Spec.ValidVersions { + v, err := semver.NewVersion(r) + if err != nil { + logger.Info("Error parsing version: %s", err) + continue + } + + semVerVersions = append(semVerVersions, v) + key := "v" + strings.TrimPrefix(r, "v") + value := "" + + if ev.Spec.Type == compositionv1alpha1.ExpanderTypeJob { + value = fmt.Sprintf("%s/%s:%s", ev.Spec.ImageRegistry, image, key) + } else { + svcVersion := strings.Replace(key, ".", "-", -1) + value = fmt.Sprintf("composition-%s-%s:8443", expander, svcVersion) + } + ev.Status.VersionMap[key] = value + } + + sort.Sort(semver.Collection(semVerVersions)) + latest := "v" + semVerVersions[len(semVerVersions)-1].String() + ev.Status.VersionMap["latest"] = ev.Status.VersionMap[latest] +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ExpanderVersionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&compositionv1alpha1.ExpanderVersion{}). + Complete(r) +} diff --git a/composition/internal/controller/expanderversion_controller_test.go b/composition/internal/controller/expanderversion_controller_test.go new file mode 100644 index 0000000..64b8270 --- /dev/null +++ b/composition/internal/controller/expanderversion_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +var _ = Describe("ExpanderVersion Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + expanderversion := &compositionv1alpha1.ExpanderVersion{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ExpanderVersion") + err := k8sClient.Get(ctx, typeNamespacedName, expanderversion) + if err != nil && errors.IsNotFound(err) { + resource := &compositionv1alpha1.ExpanderVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &compositionv1alpha1.ExpanderVersion{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ExpanderVersion") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ExpanderVersionReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/composition/internal/controller/facade_controller.go b/composition/internal/controller/facade_controller.go new file mode 100644 index 0000000..10c68d7 --- /dev/null +++ b/composition/internal/controller/facade_controller.go @@ -0,0 +1,186 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/go-logr/logr" + "github.com/gobuffalo/flect" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/composition/pkg/crds" +) + +// FacadeReconciler reconciles a Facade object +type FacadeReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=composition.google.com,resources=facades,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=composition.google.com,resources=facades/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=composition.google.com,resources=facades/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Facade object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile +func (r *FacadeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("Got a new request!", "request", req) + + var facade compositionv1alpha1.Facade + if err := r.Client.Get(ctx, req.NamespacedName, &facade); err != nil { + logger.Error(err, "unable to fetch Facade") + // 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. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Grab status for comparison later + oldStatus := facade.Status.DeepCopy() + + // Try updating status before returning + defer func() { + if !reflect.DeepEqual(oldStatus, facade.Status) { + newStatus := facade.Status.DeepCopy() + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + nn := types.NamespacedName{Namespace: facade.Namespace, Name: facade.Name} + err := r.Client.Get(ctx, nn, &facade) + if err != nil { + return err + } + facade.Status = *newStatus.DeepCopy() + return r.Client.Status().Update(ctx, &facade) + }) + if err != nil { + logger.Error(err, "unable to update Composition status") + } + } + }() + + logger = logger.WithName(facade.Name).WithName(fmt.Sprintf("%d", facade.Generation)) + + logger.Info("Validating Facade object") + if !facade.Validate() { + logger.Info("Validation Failed") + return ctrl.Result{}, fmt.Errorf("Validation failed") + } + + logger.Info("Processing Facade object") + facade.Status.ClearCondition(compositionv1alpha1.Error) + if err := r.createCRD(ctx, &facade, logger); err != nil { + logger.Info("Error creating CRD") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *FacadeReconciler) createCRD(ctx context.Context, + c *compositionv1alpha1.Facade, logger logr.Logger) error { + + var crd extv1.CustomResourceDefinition + logger = logger.WithName(c.Spec.FacadeKind) + + plural := strings.ToLower(flect.Pluralize(c.Spec.FacadeKind)) + crdName := fmt.Sprintf("%s.%s", plural, crds.FacadeGroup) + + logger.Info("Checking if CRD exists", "crd", crdName) + err := r.Client.Get(ctx, types.NamespacedName{Name: crdName, Namespace: ""}, &crd) + // CRD exists. Nothing to be done. + if err == nil { + logger.Info("CRD exists. Not creating.", "crd", crdName) + return nil + } + + // If we are unable to get it for some reason other than not found return + if !apierrors.IsNotFound(err) { + c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: err.Error(), + Reason: "FailedGettingFacadeCRD", + Type: string(compositionv1alpha1.Error), + Status: metav1.ConditionTrue, + }) + logger.Error(err, "failed to get an Facade CRD object") + return err + } + + // Construct Facade CRD from the openAPI Schema + crdInfo := crds.NewFacadeCRDInfo(c.Spec.FacadeKind, plural, nil, "v1", nil, nil) + err = crdInfo.SetSpec(c.Spec.OpenAPIV3Schema) + if err == nil { + unversionedFacadeCRD, err := crdInfo.CRD() + if err == nil { + facadeCRD := &apiextensionsv1.CustomResourceDefinition{} + err = r.Scheme.Convert(unversionedFacadeCRD, facadeCRD, nil) + if err == nil { + err = r.Client.Create(ctx, facadeCRD) + if err != nil { + logger.Error(err, "failed to Create Facade CRD") + } + } else { + logger.Error(err, "CRD conversion error") + } + } else { + logger.Error(err, "Error getting unversioned CRD") + } + } else { + logger.Error(err, "Unable to set CRD Spec from Schema") + } + + if err != nil { + c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{ + LastTransitionTime: metav1.Now(), + Message: err.Error(), + Reason: "CreateFacadeCRDFailed", + Type: string(compositionv1alpha1.Error), + Status: metav1.ConditionTrue, + }) + } + logger.Info("Created Facade CRD", "crd", crdName) + return err +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FacadeReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&compositionv1alpha1.Facade{}). + Complete(r) +} diff --git a/composition/internal/controller/facade_controller_test.go b/composition/internal/controller/facade_controller_test.go new file mode 100644 index 0000000..a699bac --- /dev/null +++ b/composition/internal/controller/facade_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +var _ = Describe("Facade Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + facade := &compositionv1alpha1.Facade{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Facade") + err := k8sClient.Get(ctx, typeNamespacedName, facade) + if err != nil && errors.IsNotFound(err) { + resource := &compositionv1alpha1.Facade{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &compositionv1alpha1.Facade{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Facade") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &FacadeReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/composition/internal/controller/getter_controller.go b/composition/internal/controller/getter_controller.go new file mode 100644 index 0000000..3593223 --- /dev/null +++ b/composition/internal/controller/getter_controller.go @@ -0,0 +1,105 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +// GetterConfigurationReconciler reconciles a GetterConfiguration object +type GetterConfigurationReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=composition.google.com,resources=getterconfigurations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=composition.google.com,resources=getterconfigurations/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=composition.google.com,resources=getterconfigurations/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the GetterConfiguration object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile +func (r *GetterConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("Got a new request!", "request", req) + + var obj compositionv1alpha1.GetterConfiguration + if err := r.Client.Get(ctx, req.NamespacedName, &obj); err != nil { + logger.Error(err, "unable to fetch GetterConfiguration") + // 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. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Grab status for comparison later + oldStatus := obj.Status.DeepCopy() + + // Try updating status before returning + defer func() { + if !reflect.DeepEqual(oldStatus, obj.Status) { + newStatus := obj.Status.DeepCopy() + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + nn := types.NamespacedName{Namespace: obj.Namespace, Name: obj.Name} + err := r.Client.Get(ctx, nn, &obj) + if err != nil { + return err + } + obj.Status = *newStatus.DeepCopy() + return r.Client.Status().Update(ctx, &obj) + }) + if err != nil { + logger.Error(err, "unable to update Composition status") + } + } + }() + + logger = logger.WithName(obj.Name).WithName(fmt.Sprintf("%d", obj.Generation)) + + logger.Info("Validating ExpanderVersion object") + if !obj.Validate() { + logger.Info("Validation Failed") + return ctrl.Result{}, fmt.Errorf("Validation failed") + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GetterConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&compositionv1alpha1.GetterConfiguration{}). + Complete(r) +} diff --git a/composition/internal/controller/getter_controller_test.go b/composition/internal/controller/getter_controller_test.go new file mode 100644 index 0000000..21887bd --- /dev/null +++ b/composition/internal/controller/getter_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +var _ = Describe("GetterConfiguration Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + getter := &compositionv1alpha1.GetterConfiguration{} + + BeforeEach(func() { + By("creating the custom resource for the Kind GetterConfiguration") + err := k8sClient.Get(ctx, typeNamespacedName, getter) + if err != nil && errors.IsNotFound(err) { + resource := &compositionv1alpha1.GetterConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &compositionv1alpha1.GetterConfiguration{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance GetterConfiguration") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &GetterConfigurationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/composition/internal/controller/plan_controller.go b/composition/internal/controller/plan_controller.go new file mode 100644 index 0000000..6453489 --- /dev/null +++ b/composition/internal/controller/plan_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +// PlanReconciler reconciles a Plan object +type PlanReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=composition.google.com,resources=plans,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=composition.google.com,resources=plans/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=composition.google.com,resources=plans/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Plan object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile +func (r *PlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PlanReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&compositionv1alpha1.Plan{}). + Complete(r) +} diff --git a/composition/internal/controller/plan_controller_test.go b/composition/internal/controller/plan_controller_test.go new file mode 100644 index 0000000..3315715 --- /dev/null +++ b/composition/internal/controller/plan_controller_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "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/reconcile" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" +) + +var _ = Describe("Plan Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + plan := &compositionv1alpha1.Plan{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Plan") + err := k8sClient.Get(ctx, typeNamespacedName, plan) + if err != nil && errors.IsNotFound(err) { + resource := &compositionv1alpha1.Plan{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &compositionv1alpha1.Plan{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Plan") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &PlanReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/composition/internal/controller/suite_test.go b/composition/internal/controller/suite_test.go new file mode 100644 index 0000000..7c93994 --- /dev/null +++ b/composition/internal/controller/suite_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/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 cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = compositionv1alpha1.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()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/composition/pkg/applier/applier.go b/composition/pkg/applier/applier.go new file mode 100644 index 0000000..091ab82 --- /dev/null +++ b/composition/pkg/applier/applier.go @@ -0,0 +1,464 @@ +// Copyright 2024 Google LLC +// +// 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 applier + +import ( + "context" + "fmt" + "strings" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/composition/pkg/cel" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kubebuilder-declarative-pattern/applylib/applyset" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" +) + +const StageLabel = "compositions.google.com/applier-stage" + +type ApplierClient struct { + RESTMapper meta.RESTMapper + Dynamic *dynamic.DynamicClient + Client client.Client +} + +type Applier struct { + client ApplierClient + planCR *compositionv1alpha1.Plan + Dryrun bool + stageName string + namespace string + resource string + logger logr.Logger + ctx context.Context + objects []applyset.ApplyableObject + results *applyset.ApplyResults + readiness []compositionv1alpha1.ReadyOn +} + +func NewApplier( + ctx context.Context, logger logr.Logger, + ac ApplierClient, + stage string, namespace string, resource string, + plan *compositionv1alpha1.Plan, + readiness []compositionv1alpha1.ReadyOn, +) *Applier { + return &Applier{ + client: ac, + resource: resource, + planCR: plan, + namespace: namespace, + stageName: stage, + Dryrun: false, + logger: logger, + ctx: ctx, + readiness: readiness, + } +} + +func (a *Applier) Count() int { + return len(a.objects) +} + +func (a *Applier) UpdatePruneStatus(status *compositionv1alpha1.PlanStatus) { + for _, resultObj := range a.results.Objects { + if !resultObj.Apply.IsPruned { + continue + } + + rs := compositionv1alpha1.ResourceStatus{ + Group: resultObj.GVK.Group, + Version: resultObj.GVK.Version, + Kind: resultObj.GVK.Kind, + Namespace: resultObj.NameNamespace.Namespace, + Name: resultObj.NameNamespace.Name, + Status: "Pruned", + Health: compositionv1alpha1.Healthy, // Is it ? + } + if resultObj.Apply.Error != nil { + rs.Status = fmt.Sprintf("Prune Error: %s", resultObj.Apply.Error) + } + status.LastPruned = append(status.LastPruned, rs) + } +} + +func (a *Applier) UpdateStageStatus(status *compositionv1alpha1.PlanStatus) { + applyCount := 0 + if status.Stages[a.stageName] == nil { + status.Stages[a.stageName] = &compositionv1alpha1.StageStatus{} + } + status.Stages[a.stageName].LastApplied = []compositionv1alpha1.ResourceStatus{} + + for _, resultObj := range a.results.Objects { + // Match objects from this applier only. + fromCurrentApplier := false + for _, applierObj := range a.objects { + if applierObj.GroupVersionKind() == resultObj.GVK && + applierObj.GetNamespace() == resultObj.NameNamespace.Namespace && + applierObj.GetName() == resultObj.NameNamespace.Name { + fromCurrentApplier = true + } + } + if fromCurrentApplier { + if status.Stages == nil { + status.Stages = map[string]*compositionv1alpha1.StageStatus{} + } + rs := compositionv1alpha1.ResourceStatus{ + Group: resultObj.GVK.Group, + Version: resultObj.GVK.Version, + Kind: resultObj.GVK.Kind, + Namespace: resultObj.NameNamespace.Namespace, + Name: resultObj.NameNamespace.Name, + Status: "", + Health: compositionv1alpha1.Unhealthy, + } + if resultObj.Apply.IsPruned { + rs.Status = "Unexpected Prune" + } else { + if resultObj.Apply.Error != nil { + rs.Status = fmt.Sprintf("Apply Error: %s", resultObj.Apply.Error) + } else { + applyCount++ + rs.Status = resultObj.Apply.Message + } + if resultObj.Health.IsHealthy { + rs.Health = compositionv1alpha1.Healthy + } + } + status.Stages[a.stageName].LastApplied = append(status.Stages[a.stageName].LastApplied, rs) + } + } + status.Stages[a.stageName].AppliedCount = applyCount +} + +func (a *Applier) Load() error { + var stage compositionv1alpha1.Stage + stage, ok := a.planCR.Spec.Stages[a.stageName] + if !ok { + a.logger.Info(".spec.stages did not have a matching expander name") + return fmt.Errorf(".spec.stages did not have a matching expander name") + } + + a.objects = []applyset.ApplyableObject{} + + // We dont error out on empty manifests + if stage.Manifest == "" { + a.logger.Info(".spec.stages[name] has empty manifests. Nothing to apply") + return nil + } + + objects, err := manifest.ParseObjects(a.ctx, stage.Manifest) + if err != nil { + a.logger.Error(err, "Error parsing manifest") + return err + } + + err = a.injectOwnerRef(objects) + if err != nil { + a.logger.Error(err, "Error injecting ownerRefs") + return err + } + a.addStageLabel(objects) + + // loop over objects and extract unstructured + for _, item := range objects.Items { + + // If namespace is passed it is namespace mode composition + if a.namespace != "" { + err := item.SetNamespace(a.namespace) + if err != nil { + a.logger.Error(err, "Error setting namespace") + return err + } + } + a.objects = append(a.objects, item.UnstructuredObject()) + } + + return nil +} + +func (a *Applier) injectOwnerRef(objects *manifest.Objects) error { + for _, o := range objects.Items { + // TODO (barney-s): This would result in some objects not being cleaned up. + // objects not in the plan namespace (cross namespace composition) would be skipped + // may be it is ok if we also create the namespace. + if o.GetNamespace() != a.planCR.GetNamespace() { + continue + } + gvk := a.planCR.GroupVersionKind() + + ownerRefs := []interface{}{ + map[string]interface{}{ + "apiVersion": gvk.Group + "/" + gvk.Version, + "blockOwnerDeletion": true, + "controller": true, + "kind": gvk.Kind, + "name": a.planCR.GetName(), + "uid": string(a.planCR.GetUID()), + }, + } + + if err := o.SetNestedField(ownerRefs, "metadata", "ownerReferences"); err != nil { + return err + } + } + return nil +} + +func (a *Applier) addStageLabel(objects *manifest.Objects) { + labels := map[string]string{StageLabel: a.stageName} + for _, o := range objects.Items { + o.AddLabels(labels) + } +} + +func (a *Applier) getApplyOptions(prune bool) (applyset.Options, error) { + var options applyset.Options + force := true + patchOptions := metav1.PatchOptions{ + FieldManager: a.resource + "-controller", + Force: &force, + } + if a.Dryrun { + patchOptions.DryRun = []string{metav1.DryRunAll} + } + + parentGVK := a.planCR.GroupVersionKind() + restMapping, err := a.client.RESTMapper.RESTMapping(parentGVK.GroupKind(), parentGVK.Version) + if err != nil { + return options, err + } + + parent := applyset.NewParentRef(a.planCR, a.planCR.GetName(), a.planCR.GetNamespace(), restMapping) + options = applyset.Options{ + RESTMapper: a.client.RESTMapper, + Client: a.client.Dynamic, + Prune: prune, + PatchOptions: patchOptions, + Parent: parent, + ParentClient: a.client.Client, + Tooling: "compositions", + ComputeHealth: a.isObjectReady, + } + return options, nil +} + +func flattenObjects(appliers ...*Applier) []applyset.ApplyableObject { + objects := []applyset.ApplyableObject{} + for _, applier := range appliers { + objects = append(objects, applier.objects...) + } + return objects +} + +func (a *Applier) Apply(oldAppliers []*Applier, prune bool) error { + options, err := a.getApplyOptions(prune) + if err != nil { + return err + } + applySet, err := applyset.New(options) + if err != nil { + return err + } + + appliers := append(oldAppliers, a) + objects := flattenObjects(appliers...) + + if err = applySet.SetDesiredObjects(objects); err != nil { + return err + } + a.results, err = applySet.ApplyOnce(a.ctx) + if err != nil { + return err + } + + /* + if containsCRDObject(objects) { + // Reset the REST Mapper's cache so we can discover any new CRDs immediately without + // needing to wait for the cache to expire. This clears any cached 'no matches'. + c.RestMapper.Reset() + } + */ + + if !a.results.AllApplied() { + err = fmt.Errorf("Unable to apply all objects") + } + return err +} + +func (a *Applier) getReadinessRule(u *unstructured.Unstructured) string { + gvk := u.GetObjectKind().GroupVersionKind() + match := false + for i := range a.readiness { + if a.readiness[i].Group == gvk.Group && + a.readiness[i].Version == gvk.Version && + a.readiness[i].Kind == gvk.Kind { + match = true + } + if !match { + continue + } + if a.readiness[i].Name != "" { + match = a.readiness[i].Name == u.GetName() + } + if !match { + continue + } + if a.readiness[i].Namespace != "" { + match = a.readiness[i].Namespace == u.GetNamespace() + } + if !match { + continue + } + return a.readiness[i].Ready + } + return "" +} + +// isObjectReady - is the object ready +func (a *Applier) isObjectReady(u *unstructured.Unstructured) (bool, string, error) { + ready := false + var err error + message := "" + + rule := a.getReadinessRule(u) + if rule != "" { + // Create a CEL engine + celEngine, err := cel.NewEngine(u) + if err != nil { + return ready, message, fmt.Errorf("error creating CEL engine: %w", err) + } + result, err := celEngine.Eval(rule) + if err != nil { + return ready, message, fmt.Errorf("error Evaluating expression: %s, %w", rule, err) + } + resultBool, ok := result.Value().(bool) + if !ok { + message := fmt.Sprintf("Readiness Rule [%s] not evaluating to bool. result: %v", rule, result.Value()) + return ready, message, fmt.Errorf("%s, %w", message, err) + } + message = fmt.Sprintf("Readiness Rule evaluated to %v", resultBool) + ready = resultBool + } else { + // Fall back to kstatus + result, err := status.Compute(u) + if err == nil { + ready = result.Status == status.CurrentStatus + message = result.Message + } + } + return ready, message, err +} + +func (a *Applier) AreResourcesReady() (bool, error) { + // Check for readiness or progress CEL rules + allReady := true + for i, resultObj := range a.results.Objects { + // Match objects from this applier only. + fromCurrentApplier := false + for _, applierObj := range a.objects { + if applierObj.GroupVersionKind() == resultObj.GVK && + applierObj.GetNamespace() == resultObj.NameNamespace.Namespace && + applierObj.GetName() == resultObj.NameNamespace.Name { + fromCurrentApplier = true + } + } + if fromCurrentApplier { + if resultObj.Apply.IsPruned { + continue + } + a.results.Objects[i].Health.Message = resultObj.Health.Message + a.results.Objects[i].Health.IsHealthy = resultObj.Health.IsHealthy + if !resultObj.Health.IsHealthy { + allReady = false + } + } + } + + return allReady, nil +} + +func (a *Applier) AddAppliedObjectsIntoValues(values map[string]interface{}) map[string]interface{} { + for _, resultObj := range a.results.Objects { + if resultObj.Apply.IsPruned { + continue + } + obj := resultObj.LastApplied + name := obj.GetName() + gvk := obj.GroupVersionKind() + kind := strings.ToLower(gvk.Kind) + + // short path: values... May clash + _, ok := values[kind] + if !ok { + values[kind] = map[string]interface{}{} + } + ref := values[kind].(map[string]interface{}) + + _, ok = ref[name] + if ok { + // Clash !! We will ignore + a.logger.Info("Clash when adding applied objects to values.", "kind", kind, "name", name) + } else { + ref[name] = obj.Object + } + + // long path: values.... will not clash + // Long path may not be practical since the namespace is not part of the composition and + // most templating languages dont support nested templatable variable. + // ex: {{ values.deployment.teampage.status.something }} will work in jinja2 + // but this {{ values.apps.deployment.{{teampage.metadata.namespace}}.status.something }} wont work + // So leaving this code commented for now. + /* + group := strings.ReplaceAll(strings.ToLower(gvk.Group), ".", "_") + namespace := obj.GetNamespace() + + if group == "" { + group = "core" + } + _, ok = values[group] + if !ok { + values[group] = map[string]interface{}{} + } + ref = values[group].(map[string]interface{}) + + _, ok = ref[kind] + if !ok { + ref[kind] = map[string]interface{}{} + } + ref = ref[kind].(map[string]interface{}) + + _, ok = ref[namespace] + if !ok { + ref[namespace] = map[string]interface{}{} + } + ref = ref[namespace].(map[string]interface{}) + + _, ok = ref[name] + if !ok { + ref[name] = map[string]interface{}{} + } + ref[name] = obj.Object + */ + + } + return values +} diff --git a/composition/pkg/cel/engine.go b/composition/pkg/cel/engine.go new file mode 100644 index 0000000..820ed5a --- /dev/null +++ b/composition/pkg/cel/engine.go @@ -0,0 +1,72 @@ +// Copyright 2024 Google LLC +// +// 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 cel + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" + "github.com/wzshiming/easycel" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type Engine struct { + u *unstructured.Unstructured + registry *easycel.Registry + env *easycel.Environment +} + +func NewEngine(u *unstructured.Unstructured) (*Engine, error) { + // TODO: what withtagname ? can we pass yaml ? + registry := easycel.NewRegistry("cel-engine", easycel.WithTagName("json")) + + // Register variables + err := registry.RegisterVariable("spec", map[string]interface{}{}) + if err != nil { + return nil, err + } + err = registry.RegisterVariable("status", map[string]interface{}{}) + if err != nil { + return nil, err + } + err = registry.RegisterVariable("metadata", map[string]interface{}{}) + if err != nil { + return nil, err + } + + // Environment + env, err := easycel.NewEnvironment(cel.Lib(registry)) + if err != nil { + return nil, err + } + + return &Engine{ + u: u, + registry: registry, + env: env, + }, nil +} + +func (e *Engine) Eval(expression string) (ref.Val, error) { + prog, err := e.env.Program(expression) + if err != nil { + return nil, err + } + val, _, err := prog.Eval(e.u.Object) + if err != nil { + return nil, err + } + + return val, nil +} diff --git a/composition/pkg/containerexecutor/jobcontainerexecutor/jobcontainerrunner.go b/composition/pkg/containerexecutor/jobcontainerexecutor/jobcontainerrunner.go new file mode 100644 index 0000000..d3f8c24 --- /dev/null +++ b/composition/pkg/containerexecutor/jobcontainerexecutor/jobcontainerrunner.go @@ -0,0 +1,454 @@ +// Copyright 2024 Google LLC +// +// 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 jobcontainerexecutor + +import ( + "bytes" + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "github.com/google/safetext/yamltemplate" + corev1 "k8s.io/api/core/v1" + apierrors "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/schema" + "k8s.io/apimachinery/pkg/types" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ConditionTypeComplete string = "Complete" + ConditionTypeFailed string = "Failed" +) + +var jobTemplate string = `apiVersion: batch/v1 +kind: Job +metadata: + name: {{.Name}} + namespace: {{.CompositionNamespace}} + labels: + compositionname: {{.CompositionName}} + name: {{.InputAPIName}} + namespace: {{.InputAPINamespace}} +spec: + backoffLimit: 2 # retry twice at most + ttlSecondsAfterFinished: 300 # delete after 60 seconds + template: + metadata: + name: expander + labels: + compositionname: {{.CompositionName}} + name: {{.InputAPIName}} + namespace: {{.InputAPINamespace}} + spec: + restartPolicy: Never + serviceAccountName: {{.Name}} + terminationGracePeriodSeconds: 10 + volumes: + - name: inputs + emptyDir: {} + - name: expanded + emptyDir: {} + containers: + - name: copyout + image: {{.ImageRegistry}}/manifests-inline:v0.0.1 + args: + - "--template" + - "{{.CompositionName}}" + - "--plan" + - "{{.PlanName}}" + - "--expander" + - "{{.ExpanderName}}" + - "--group" + - "{{.InputAPIGroup}}" + - "--version" + - "{{.InputAPIVersion}}" + - "--resource" + - "{{.InputAPIResource}}" + - "--name" + - "{{.InputAPIName}}" + - "--namespace" + - "{{.InputAPINamespace}}" + - "--path" + - "/expanded" + - "--stage" + - "afterExpansion" + volumeMounts: + - name: expanded + mountPath: /expanded + - name: expand + image: {{.ExpanderImage}} + args: ["/inputs/template", "/inputs/values", "--format=json", "-o", "/expanded/expanded"] + volumeMounts: + - name: inputs + mountPath: /inputs + - name: expanded + mountPath: /expanded + initContainers: + - name: copyin + image: {{.ImageRegistry}}/manifests-inline:v0.0.1 + args: + - "--template" + - "{{.CompositionName}}" + - "--plan" + - "{{.PlanName}}" + - "--expander" + - "{{.ExpanderName}}" + - "--group" + - "{{.InputAPIGroup}}" + - "--version" + - "{{.InputAPIVersion}}" + - "--resource" + - "{{.InputAPIResource}}" + - "--name" + - "{{.InputAPIName}}" + - "--namespace" + - "{{.InputAPINamespace}}" + - "--path" + - "/inputs" + - "--stage" + - "beforeExpansion" + volumeMounts: + - name: inputs + mountPath: /inputs` + +type JobFactory struct { + InputAPIGroup string + InputAPIResource string + InputAPIName string + InputAPINamespace string + InputAPIVersion string + CompositionName string + CompositionNamespace string + ExpanderImage string + ExpanderName string + ImageRegistry string + PlanName string + Name string + logger logr.Logger + ctx context.Context + client client.Client + objects []unstructured.Unstructured + timeout time.Duration +} + +func NewJobFactory(ctx context.Context, logger logr.Logger, client client.Client, + inputGVK schema.GroupVersionKind, inputGVR schema.GroupVersionResource, + compositionName string, compositionNamespace string, + cr *unstructured.Unstructured, expanderName string, expanderImage string, + planName string, imageRegistry string) *JobFactory { + return &JobFactory{ + InputAPIGroup: inputGVK.Group, + InputAPIVersion: inputGVK.Version, + InputAPIResource: inputGVR.Resource, + InputAPIName: cr.GetName(), + InputAPINamespace: cr.GetNamespace(), + CompositionName: compositionName, + CompositionNamespace: compositionNamespace, + ExpanderName: expanderName, + ExpanderImage: expanderImage, + PlanName: planName, + ImageRegistry: imageRegistry, + Name: compositionName + "-" + cr.GetName() + "-" + expanderName, + logger: logger, + ctx: ctx, + client: client, + } +} + +func (f *JobFactory) parseObjectTemplate(name string, objTemplate string) ([]byte, error) { + var manifests bytes.Buffer + + tmpl, err := yamltemplate.New(name).Parse(objTemplate) + if err != nil { + return nil, err + } + // Execute yamltemplate tmpl and write to a string + err = tmpl.Execute(&manifests, *f) + if err != nil { + return nil, err + } + return manifests.Bytes(), nil +} + +func (j *JobFactory) getUnstructuredFromTemplate(name, objTemplate string) (unstructured.Unstructured, error) { + out := unstructured.Unstructured{} + logger := j.logger.WithName(name) + raw, err := j.parseObjectTemplate(name, objTemplate) + if err != nil { + logger.Error(err, "Unable to parse manifest") + return out, err + } + raw = bytes.TrimSpace(raw) + if err := k8syaml.Unmarshal(raw, &out); err != nil { + logger.Error(err, "Unable to marshall") + return out, err + } + + return out, nil +} + +func (f *JobFactory) GetManifests() ([]byte, error) { + return f.parseObjectTemplate("all", jobTemplate) +} + +// Get a Service account kubernetes object for the Job +func (j *JobFactory) getServiceAccount() unstructured.Unstructured { + return unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": map[string]interface{}{ + "name": j.Name, + "namespace": j.CompositionNamespace, + }, + }, + } +} + +func (j *JobFactory) getCompositionRole() unstructured.Unstructured { + return unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": map[string]interface{}{ + "name": "composition-" + j.Name, + "namespace": j.CompositionNamespace, + }, + "rules": []interface{}{ + map[string]interface{}{ + "apiGroups": []interface{}{"composition.google.com"}, + "resources": []interface{}{"compositions"}, + "verbs": []interface{}{"get"}, + }, + }, + }, + } +} + +func (j *JobFactory) getInputAPIRule() unstructured.Unstructured { + return unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": map[string]interface{}{ + "name": "inputapi-" + j.Name, + "namespace": j.InputAPINamespace, + }, + "rules": []interface{}{ + map[string]interface{}{ + "apiGroups": []interface{}{"composition.google.com"}, + "resources": []interface{}{"contexts"}, + "verbs": []interface{}{"get"}, + }, + map[string]interface{}{ + "apiGroups": []interface{}{j.InputAPIGroup}, + "resources": []interface{}{j.InputAPIResource, j.InputAPIResource + "/status"}, + "verbs": []interface{}{"get", "patch", "update"}, + }, + // TODO (barney-s@) : Move this to composition namespace + map[string]interface{}{ + "apiGroups": []interface{}{"composition.google.com"}, + "resources": []interface{}{"plans"}, + "verbs": []interface{}{"get", "patch", "update"}, + }, + }, + }, + } +} + +func (j *JobFactory) getCompositionRoleBinding() unstructured.Unstructured { + return unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": map[string]interface{}{ + "name": "composition-" + j.Name, + "namespace": j.CompositionNamespace, + }, + "roleRef": map[string]interface{}{ + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "composition-" + j.Name, + "namespace": j.CompositionNamespace, + }, + "subjects": []interface{}{ + map[string]interface{}{ + "kind": "ServiceAccount", + "name": j.Name, + "namespace": j.CompositionNamespace, + }, + }, + }, + } +} + +func (j *JobFactory) getInputAPIRoleBinding() unstructured.Unstructured { + return unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": map[string]interface{}{ + "name": "inputapi-" + j.Name, + "namespace": j.InputAPINamespace, + }, + "roleRef": map[string]interface{}{ + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "inputapi-" + j.Name, + "namespace": j.InputAPINamespace, + }, + "subjects": []interface{}{ + map[string]interface{}{ + "kind": "ServiceAccount", + "name": j.Name, + "namespace": j.CompositionNamespace, + }, + }, + }, + } +} + +func (j *JobFactory) getJob() (unstructured.Unstructured, error) { + return j.getUnstructuredFromTemplate("job", jobTemplate) +} + +func (j *JobFactory) Create() error { + j.objects = []unstructured.Unstructured{ + j.getServiceAccount(), + j.getCompositionRole(), + j.getInputAPIRule(), + j.getCompositionRoleBinding(), + j.getInputAPIRoleBinding(), + } + job, err := j.getJob() + if err != nil { + j.logger.Error(err, "failed to marshall a job object") + return err + } + j.objects = append(j.objects, job) + + for _, obj := range j.objects { + objStr := fmt.Sprintf("%s/%s", obj.GroupVersionKind().Group, obj.GetName()) + if err := j.client.Create(j.ctx, &obj); err != nil { + if apierrors.IsAlreadyExists(err) { + j.logger.Info("Object already exists: " + objStr) + continue + } + j.logger.Error(err, "Failed to create Object: "+objStr) + return err + } + j.logger.Info("Created Object: " + objStr) + } + return nil +} + +func (j *JobFactory) CleanUp() { + for _, obj := range j.objects { + objStr := fmt.Sprintf("%s/%s", obj.GroupVersionKind().Kind, obj.GetName()) + // delete with propagation true + propagationPolicy := metav1.DeletePropagationBackground + if err := j.client.Delete(j.ctx, &obj, + &client.DeleteOptions{ + PropagationPolicy: &propagationPolicy, + }); err != nil { + j.logger.Error(err, "Failed to delete object: "+objStr) + } + j.logger.Info("Deleted Object: " + objStr) + } +} + +func (j *JobFactory) Wait() (bool, error) { + // Not elegant !! + job := j.objects[len(j.objects)-1] + j.timeout = time.Second * 60 + + logger := j.logger.WithName("Job").WithName(job.GetName()) + nn := types.NamespacedName{Name: job.GetName(), Namespace: job.GetNamespace()} + + ctx, cancel := context.WithTimeout(j.ctx, j.timeout) + defer cancel() + for { + select { + case <-ctx.Done(): + return false, ctx.Err() + default: + if err := j.client.Get(j.ctx, nn, &job); err != nil { + logger.Error(err, "failed to get object") + time.Sleep(5 * time.Second) + continue + } + health, err := j.jobStatus(&job) + if err != nil { + logger.Error(err, "failed computing job status") + time.Sleep(5 * time.Second) + continue + } + if health == ConditionTypeComplete { + return true, nil + } + if health == ConditionTypeFailed { + return false, nil + } + time.Sleep(5 * time.Second) + } + } +} + +func (j *JobFactory) jobStatus(u *unstructured.Unstructured) (string, error) { + logger := j.logger.WithName("Job").WithName(u.GetName()) + obj := u.UnstructuredContent() + + succeeded := status.GetIntField(obj, ".status.succeeded", 0) + active := status.GetIntField(obj, ".status.active", 0) + failed := status.GetIntField(obj, ".status.failed", 0) + starttime := status.GetStringField(obj, ".status.startTime", "") + + // Conditions + // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24 + objc, err := status.GetObjectWithConditions(obj) + if err != nil { + return "", err + } + for _, c := range objc.Status.Conditions { + switch c.Type { + case ConditionTypeComplete: + if c.Status == corev1.ConditionTrue { + logger.Info("Completed") + return ConditionTypeComplete, nil + } + case ConditionTypeFailed: + if c.Status == corev1.ConditionTrue { + logger.Info("Failed") + return ConditionTypeFailed, nil + } + } + } + + // replicas + if starttime == "" { + logger.Info("Not Started") + return "NotStarted", nil + } + + logstr := fmt.Sprintf("In Progress. success:%d, active: %d, failed: %d", succeeded, active, failed) + logger.Info(logstr) + return "InProgress", nil +} diff --git a/composition/pkg/crds/crds.go b/composition/pkg/crds/crds.go new file mode 100644 index 0000000..c0429dd --- /dev/null +++ b/composition/pkg/crds/crds.go @@ -0,0 +1,229 @@ +// Copyright 2024 Google LLC +// +// 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 crds + +import ( + "context" + "fmt" + "strings" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsvalidation "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +const ( + FacadeGroup = "facade.compositions.google.com" +) + +var scheme = runtime.NewScheme() + +func init() { + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + panic(err) + } + if err := apiextensions.AddToScheme(scheme); err != nil { + panic(err) + } +} + +type CRDInfo struct { + Group string // Should the group be hardcoded to something like facade.compositions.google.com ? + Kind string + Plural string + ShortNames []string + Categories []string + Version string + PrinterColumns []apiextensions.CustomResourceColumnDefinition + Labels map[string]string + schema *apiextensions.JSONSchemaProps +} + +func NewFacadeCRDInfo(kind string, plural string, + shortNames []string, version string, + printerCols []apiextensions.CustomResourceColumnDefinition, + labels map[string]string) *CRDInfo { + + crd := CRDInfo{ + Group: FacadeGroup, + Kind: kind, + Plural: plural, + ShortNames: shortNames, + Categories: []string{"facade", "facades"}, + Version: version, + PrinterColumns: printerCols, + schema: nil, + } + + crd.Labels = map[string]string{ + "compositions.google.com/facade": "yes", + } + for k, v := range labels { + crd.Labels[k] = v + } + return &crd +} + +func (c *CRDInfo) SetCRDSchema(schema *apiextensions.JSONSchemaProps) { + c.schema = schema +} + +func (c *CRDInfo) SetSpec(specProperties *apiextensionsv1.JSONSchemaProps) error { + specUnversionedProperties := &apiextensions.JSONSchemaProps{} + // Risk ? nil conversion.Scope passed + if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps( + specProperties, specUnversionedProperties, nil); err != nil { + return err + } + statusProperties := map[string]apiextensions.JSONSchemaProps{ + "conditions": { + Type: "array", + Items: &apiextensions.JSONSchemaPropsOrArray{ + Schema: &apiextensions.JSONSchemaProps{ + Description: "", + Required: []string{"lastTransitionTime", "message", "reason", "status", "type"}, + Type: "object", + Properties: map[string]apiextensions.JSONSchemaProps{ + "lastTransitionTime": { + Description: "", + Format: "date-time", + Type: "string", + }, + "message": { + Description: "human readable message", + MaxLength: ptr.To[int64](1024), + Type: "string", + }, + "observedGeneration": { + Description: "", + Format: "int64", + Minimum: ptr.To[float64](0), + Type: "integer", + }, + "reason": { + Description: "", + MaxLength: ptr.To[int64](256), + MinLength: ptr.To[int64](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: []apiextensions.JSON{"True", "False", "Unknown"}, + Type: "string", + }, + "type": { + Description: "type of condition in CamelCase or in foo.example.com/CamelCase." + + " The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)", + MaxLength: ptr.To[int64](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", + }, + }, + }, + }, + }, + } + + crdSchema := &apiextensions.JSONSchemaProps{ + Type: "object", + Description: "TODO", + Properties: map[string]apiextensions.JSONSchemaProps{ + "apiVersion": {Type: "string"}, + "kind": {Type: "string"}, + "metadata": {Type: "object"}, + "spec": *specUnversionedProperties.DeepCopy(), + "status": { + Type: "object", + Properties: statusProperties, + XPreserveUnknownFields: ptr.To[bool](true), + }, + }, + } + + c.schema = crdSchema + return nil +} + +// CRD takes a schema and converts it to a CRD. +func (c *CRDInfo) CRD() (*apiextensions.CustomResourceDefinition, error) { + if c.schema == nil { + return nil, fmt.Errorf("Schema is nil. Use SetSpec or SetCRDSchema first.") + } + crd := &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + PreserveUnknownFields: ptr.To[bool](false), + Group: c.Group, + Names: apiextensions.CustomResourceDefinitionNames{ + Kind: c.Kind, + ListKind: c.Kind + "List", + Plural: strings.ToLower(c.Plural), + Singular: strings.ToLower(c.Kind), + ShortNames: c.ShortNames, + Categories: c.Categories, + }, + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: c.schema, + }, + Scope: apiextensions.NamespaceScoped, + Version: c.Version, + Subresources: &apiextensions.CustomResourceSubresources{ + Status: &apiextensions.CustomResourceSubresourceStatus{}, + Scale: nil, + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: c.Version, + Storage: true, + Served: true, + }, + }, + AdditionalPrinterColumns: c.PrinterColumns, + }, + } + + // Defaulting functions are not found in versionless CRD package + crdv1 := &apiextensionsv1.CustomResourceDefinition{} + if err := scheme.Convert(crd, crdv1, nil); err != nil { + return nil, err + } + scheme.Default(crdv1) + + crd2 := &apiextensions.CustomResourceDefinition{} + if err := scheme.Convert(crdv1, crd2, nil); err != nil { + return nil, err + } + crd2.ObjectMeta.Name = fmt.Sprintf("%s.%s", crd.Spec.Names.Plural, c.Group) + + labels := c.Labels + if labels == nil { + labels = make(map[string]string) + } + crd2.ObjectMeta.Labels = labels + + return crd2, nil +} + +// ValidateCRD calls the CRD package's validation on an internal representation of the CRD. +func ValidateCRD(ctx context.Context, crd *apiextensions.CustomResourceDefinition) error { + errs := apiextensionsvalidation.ValidateCustomResourceDefinition(ctx, crd) + if len(errs) > 0 { + return errs.ToAggregate() + } + return nil +} diff --git a/composition/proto/expander.pb.go b/composition/proto/expander.pb.go new file mode 100644 index 0000000..c65eddc --- /dev/null +++ b/composition/proto/expander.pb.go @@ -0,0 +1,685 @@ +// Copyright 2024 Google LLC +// +// 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. + +// The definition of a Expander service. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.27.1 +// source: proto/expander.proto + +package expander + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Status int32 + +const ( + Status_SUCCESS Status = 0 + Status_VALIDATE_FAILED Status = 1 + Status_EVALUATE_FAILED Status = 2 + Status_EVALUATE_WAIT Status = 3 + Status_UNEXPECTED_ERROR Status = 4 +) + +// Enum value maps for Status. +var ( + Status_name = map[int32]string{ + 0: "SUCCESS", + 1: "VALIDATE_FAILED", + 2: "EVALUATE_FAILED", + 3: "EVALUATE_WAIT", + 4: "UNEXPECTED_ERROR", + } + Status_value = map[string]int32{ + "SUCCESS": 0, + "VALIDATE_FAILED": 1, + "EVALUATE_FAILED": 2, + "EVALUATE_WAIT": 3, + "UNEXPECTED_ERROR": 4, + } +) + +func (x Status) Enum() *Status { + p := new(Status) + *p = x + return p +} + +func (x Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Status) Descriptor() protoreflect.EnumDescriptor { + return file_proto_expander_proto_enumTypes[0].Descriptor() +} + +func (Status) Type() protoreflect.EnumType { + return &file_proto_expander_proto_enumTypes[0] +} + +func (x Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Status.Descriptor instead. +func (Status) EnumDescriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{0} +} + +type ResultType int32 + +const ( + ResultType_UNKNOWN ResultType = 0 + ResultType_MANIFESTS ResultType = 1 + ResultType_VALUES ResultType = 2 +) + +// Enum value maps for ResultType. +var ( + ResultType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "MANIFESTS", + 2: "VALUES", + } + ResultType_value = map[string]int32{ + "UNKNOWN": 0, + "MANIFESTS": 1, + "VALUES": 2, + } +) + +func (x ResultType) Enum() *ResultType { + p := new(ResultType) + *p = x + return p +} + +func (x ResultType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResultType) Descriptor() protoreflect.EnumDescriptor { + return file_proto_expander_proto_enumTypes[1].Descriptor() +} + +func (ResultType) Type() protoreflect.EnumType { + return &file_proto_expander_proto_enumTypes[1] +} + +func (x ResultType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResultType.Descriptor instead. +func (ResultType) EnumDescriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{1} +} + +type Error struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *Error) Reset() { + *x = Error{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_expander_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_proto_expander_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{0} +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type ValidateResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status Status `protobuf:"varint,1,opt,name=status,proto3,enum=expander_grpc.Status" json:"status,omitempty"` + Error *Error `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *ValidateResult) Reset() { + *x = ValidateResult{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_expander_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ValidateResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateResult) ProtoMessage() {} + +func (x *ValidateResult) ProtoReflect() protoreflect.Message { + mi := &file_proto_expander_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateResult.ProtoReflect.Descriptor instead. +func (*ValidateResult) Descriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{1} +} + +func (x *ValidateResult) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_SUCCESS +} + +func (x *ValidateResult) GetError() *Error { + if x != nil { + return x.Error + } + return nil +} + +type EvaluateResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status Status `protobuf:"varint,1,opt,name=status,proto3,enum=expander_grpc.Status" json:"status,omitempty"` + Error *Error `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + Type ResultType `protobuf:"varint,3,opt,name=type,proto3,enum=expander_grpc.ResultType" json:"type,omitempty"` + Manifests []byte `protobuf:"bytes,4,opt,name=manifests,proto3" json:"manifests,omitempty"` + Values []byte `protobuf:"bytes,5,opt,name=values,proto3" json:"values,omitempty"` +} + +func (x *EvaluateResult) Reset() { + *x = EvaluateResult{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_expander_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EvaluateResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EvaluateResult) ProtoMessage() {} + +func (x *EvaluateResult) ProtoReflect() protoreflect.Message { + mi := &file_proto_expander_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EvaluateResult.ProtoReflect.Descriptor instead. +func (*EvaluateResult) Descriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{2} +} + +func (x *EvaluateResult) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_SUCCESS +} + +func (x *EvaluateResult) GetError() *Error { + if x != nil { + return x.Error + } + return nil +} + +func (x *EvaluateResult) GetType() ResultType { + if x != nil { + return x.Type + } + return ResultType_UNKNOWN +} + +func (x *EvaluateResult) GetManifests() []byte { + if x != nil { + return x.Manifests + } + return nil +} + +func (x *EvaluateResult) GetValues() []byte { + if x != nil { + return x.Values + } + return nil +} + +type EvaluateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Config []byte `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Context []byte `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` + Facade []byte `protobuf:"bytes,3,opt,name=facade,proto3" json:"facade,omitempty"` + Value []byte `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + Resource string `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty"` +} + +func (x *EvaluateRequest) Reset() { + *x = EvaluateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_expander_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EvaluateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EvaluateRequest) ProtoMessage() {} + +func (x *EvaluateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_expander_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EvaluateRequest.ProtoReflect.Descriptor instead. +func (*EvaluateRequest) Descriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{3} +} + +func (x *EvaluateRequest) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +func (x *EvaluateRequest) GetContext() []byte { + if x != nil { + return x.Context + } + return nil +} + +func (x *EvaluateRequest) GetFacade() []byte { + if x != nil { + return x.Facade + } + return nil +} + +func (x *EvaluateRequest) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *EvaluateRequest) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +type ValidateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Config []byte `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Context []byte `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` + Facade []byte `protobuf:"bytes,3,opt,name=facade,proto3" json:"facade,omitempty"` + Value []byte `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + Resource string `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty"` +} + +func (x *ValidateRequest) Reset() { + *x = ValidateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_expander_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ValidateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateRequest) ProtoMessage() {} + +func (x *ValidateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_expander_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateRequest.ProtoReflect.Descriptor instead. +func (*ValidateRequest) Descriptor() ([]byte, []int) { + return file_proto_expander_proto_rawDescGZIP(), []int{4} +} + +func (x *ValidateRequest) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +func (x *ValidateRequest) GetContext() []byte { + if x != nil { + return x.Context + } + return nil +} + +func (x *ValidateRequest) GetFacade() []byte { + if x != nil { + return x.Facade + } + return nil +} + +func (x *ValidateRequest) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *ValidateRequest) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +var File_proto_expander_proto protoreflect.FileDescriptor + +var file_proto_expander_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, + 0x5f, 0x67, 0x72, 0x70, 0x63, 0x22, 0x21, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x6b, 0x0a, 0x0e, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, 0x78, 0x70, + 0x61, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, + 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xd0, 0x01, 0x0a, 0x0e, 0x45, 0x76, 0x61, 0x6c, 0x75, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, + 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, + 0x72, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x19, 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, + 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x45, 0x76, 0x61, + 0x6c, 0x75, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x66, 0x61, 0x63, 0x61, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, + 0x66, 0x61, 0x63, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x66, 0x61, 0x63, 0x61, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, + 0x66, 0x61, 0x63, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2a, 0x68, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x00, 0x12, + 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x56, 0x41, 0x4c, 0x55, 0x41, 0x54, 0x45, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x41, + 0x4c, 0x55, 0x41, 0x54, 0x45, 0x5f, 0x57, 0x41, 0x49, 0x54, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, + 0x55, 0x4e, 0x45, 0x58, 0x50, 0x45, 0x43, 0x54, 0x45, 0x44, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x04, 0x2a, 0x34, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, + 0x09, 0x4d, 0x41, 0x4e, 0x49, 0x46, 0x45, 0x53, 0x54, 0x53, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, + 0x56, 0x41, 0x4c, 0x55, 0x45, 0x53, 0x10, 0x02, 0x32, 0xa4, 0x01, 0x0a, 0x08, 0x45, 0x78, 0x70, + 0x61, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x4b, 0x0a, 0x08, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x12, 0x1e, 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x08, 0x45, 0x76, 0x61, 0x6c, 0x75, 0x61, 0x74, 0x65, 0x12, 0x1e, + 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, + 0x76, 0x61, 0x6c, 0x75, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x2e, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, + 0x76, 0x61, 0x6c, 0x75, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x42, + 0x69, 0x5a, 0x67, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x47, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, + 0x6d, 0x2f, 0x6b, 0x38, 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2d, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_proto_expander_proto_rawDescOnce sync.Once + file_proto_expander_proto_rawDescData = file_proto_expander_proto_rawDesc +) + +func file_proto_expander_proto_rawDescGZIP() []byte { + file_proto_expander_proto_rawDescOnce.Do(func() { + file_proto_expander_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_expander_proto_rawDescData) + }) + return file_proto_expander_proto_rawDescData +} + +var file_proto_expander_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_proto_expander_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proto_expander_proto_goTypes = []any{ + (Status)(0), // 0: expander_grpc.Status + (ResultType)(0), // 1: expander_grpc.ResultType + (*Error)(nil), // 2: expander_grpc.Error + (*ValidateResult)(nil), // 3: expander_grpc.ValidateResult + (*EvaluateResult)(nil), // 4: expander_grpc.EvaluateResult + (*EvaluateRequest)(nil), // 5: expander_grpc.EvaluateRequest + (*ValidateRequest)(nil), // 6: expander_grpc.ValidateRequest +} +var file_proto_expander_proto_depIdxs = []int32{ + 0, // 0: expander_grpc.ValidateResult.status:type_name -> expander_grpc.Status + 2, // 1: expander_grpc.ValidateResult.error:type_name -> expander_grpc.Error + 0, // 2: expander_grpc.EvaluateResult.status:type_name -> expander_grpc.Status + 2, // 3: expander_grpc.EvaluateResult.error:type_name -> expander_grpc.Error + 1, // 4: expander_grpc.EvaluateResult.type:type_name -> expander_grpc.ResultType + 6, // 5: expander_grpc.Expander.Validate:input_type -> expander_grpc.ValidateRequest + 5, // 6: expander_grpc.Expander.Evaluate:input_type -> expander_grpc.EvaluateRequest + 3, // 7: expander_grpc.Expander.Validate:output_type -> expander_grpc.ValidateResult + 4, // 8: expander_grpc.Expander.Evaluate:output_type -> expander_grpc.EvaluateResult + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_proto_expander_proto_init() } +func file_proto_expander_proto_init() { + if File_proto_expander_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_expander_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Error); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_expander_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*ValidateResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_expander_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*EvaluateResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_expander_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*EvaluateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_expander_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*ValidateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_expander_proto_rawDesc, + NumEnums: 2, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_expander_proto_goTypes, + DependencyIndexes: file_proto_expander_proto_depIdxs, + EnumInfos: file_proto_expander_proto_enumTypes, + MessageInfos: file_proto_expander_proto_msgTypes, + }.Build() + File_proto_expander_proto = out.File + file_proto_expander_proto_rawDesc = nil + file_proto_expander_proto_goTypes = nil + file_proto_expander_proto_depIdxs = nil +} diff --git a/composition/proto/expander.proto b/composition/proto/expander.proto new file mode 100644 index 0000000..2d7456d --- /dev/null +++ b/composition/proto/expander.proto @@ -0,0 +1,80 @@ +// Copyright 2024 Google LLC +// +// 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. + +// The definition of a Expander service. + +syntax = "proto3"; + +package expander_grpc; + +option go_package = "github.com/cloud-native-compositions/compositions/composition/proto/expander"; + + +enum Status { + SUCCESS = 0; + + VALIDATE_FAILED = 1; + EVALUATE_FAILED = 2; + EVALUATE_WAIT = 3; + UNEXPECTED_ERROR = 4; +} + +enum ResultType { + UNKNOWN = 0; + MANIFESTS = 1; + VALUES = 2; +} + +message Error { + string message = 1; +} + +message ValidateResult { + Status status = 1; + Error error = 2; +} + +message EvaluateResult { + Status status = 1; + Error error = 2; + ResultType type = 3; + bytes manifests = 4; + bytes values = 5; +} + +message EvaluateRequest { + bytes config = 1; + bytes context = 2; + bytes facade = 3; + bytes value = 4; + string resource = 5; +} + +message ValidateRequest { + bytes config = 1; + bytes context = 2; + bytes facade = 3; + bytes value = 4; + string resource = 5; +} + +service Expander { + // Verify the expander config/template + rpc Validate(ValidateRequest) returns (ValidateResult) { + } + + // Evaluate the expander config in context of inputs and return manifests + rpc Evaluate(EvaluateRequest) returns (EvaluateResult) { + } +} \ No newline at end of file diff --git a/composition/proto/expander_grpc.pb.go b/composition/proto/expander_grpc.pb.go new file mode 100644 index 0000000..8654625 --- /dev/null +++ b/composition/proto/expander_grpc.pb.go @@ -0,0 +1,169 @@ +// Copyright 2024 Google LLC +// +// 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. + +// The definition of a Expander service. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.4.0 +// - protoc v5.27.1 +// source: proto/expander.proto + +package expander + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 + +const ( + Expander_Validate_FullMethodName = "/expander_grpc.Expander/Validate" + Expander_Evaluate_FullMethodName = "/expander_grpc.Expander/Evaluate" +) + +// ExpanderClient is the client API for Expander service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ExpanderClient interface { + // Verify the expander config/template + Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateResult, error) + // Evaluate the expander config in context of inputs and return manifests + Evaluate(ctx context.Context, in *EvaluateRequest, opts ...grpc.CallOption) (*EvaluateResult, error) +} + +type expanderClient struct { + cc grpc.ClientConnInterface +} + +func NewExpanderClient(cc grpc.ClientConnInterface) ExpanderClient { + return &expanderClient{cc} +} + +func (c *expanderClient) Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateResult) + err := c.cc.Invoke(ctx, Expander_Validate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *expanderClient) Evaluate(ctx context.Context, in *EvaluateRequest, opts ...grpc.CallOption) (*EvaluateResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EvaluateResult) + err := c.cc.Invoke(ctx, Expander_Evaluate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ExpanderServer is the server API for Expander service. +// All implementations must embed UnimplementedExpanderServer +// for forward compatibility +type ExpanderServer interface { + // Verify the expander config/template + Validate(context.Context, *ValidateRequest) (*ValidateResult, error) + // Evaluate the expander config in context of inputs and return manifests + Evaluate(context.Context, *EvaluateRequest) (*EvaluateResult, error) + mustEmbedUnimplementedExpanderServer() +} + +// UnimplementedExpanderServer must be embedded to have forward compatible implementations. +type UnimplementedExpanderServer struct { +} + +func (UnimplementedExpanderServer) Validate(context.Context, *ValidateRequest) (*ValidateResult, error) { + return nil, status.Errorf(codes.Unimplemented, "method Validate not implemented") +} +func (UnimplementedExpanderServer) Evaluate(context.Context, *EvaluateRequest) (*EvaluateResult, error) { + return nil, status.Errorf(codes.Unimplemented, "method Evaluate not implemented") +} +func (UnimplementedExpanderServer) mustEmbedUnimplementedExpanderServer() {} + +// UnsafeExpanderServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ExpanderServer will +// result in compilation errors. +type UnsafeExpanderServer interface { + mustEmbedUnimplementedExpanderServer() +} + +func RegisterExpanderServer(s grpc.ServiceRegistrar, srv ExpanderServer) { + s.RegisterService(&Expander_ServiceDesc, srv) +} + +func _Expander_Validate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExpanderServer).Validate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Expander_Validate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExpanderServer).Validate(ctx, req.(*ValidateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Expander_Evaluate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EvaluateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExpanderServer).Evaluate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Expander_Evaluate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExpanderServer).Evaluate(ctx, req.(*EvaluateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Expander_ServiceDesc is the grpc.ServiceDesc for Expander service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Expander_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "expander_grpc.Expander", + HandlerType: (*ExpanderServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Validate", + Handler: _Expander_Validate_Handler, + }, + { + MethodName: "Evaluate", + Handler: _Expander_Evaluate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/expander.proto", +} diff --git a/composition/release/crds.yaml b/composition/release/crds.yaml new file mode 100644 index 0000000..dc8d781 --- /dev/null +++ b/composition/release/crds.yaml @@ -0,0 +1,921 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: compositions.composition.google.com +spec: + group: composition.google.com + names: + kind: Composition + listKind: CompositionList + plural: compositions + singular: composition + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Composition is the Schema for the compositions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: CompositionSpec defines the desired state of Composition + properties: + description: + type: string + expanders: + items: + properties: + configref: + description: ConfigReference - For BYO Expanders, we can extend + it + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + jinja2: + description: Built in expanders + properties: + template: + type: string + required: + - template + type: object + name: + type: string + template: + description: For BYO Expanders use generic template or ref for + external config + type: string + type: + default: jinja2 + description: |- + Type indicates what expander to use + jinja - jinja2 expander + ... + type: string + version: + default: latest + type: string + required: + - type + type: object + minItems: 1 + type: array + inputAPIGroup: + description: Use existing KRM API + type: string + namespaceMode: + description: |- + Namespace mode indicates how compositions set the namespace of the objects from expanders. + ""|inherit implies inherit the facade api's namespace. Only namespaced objects are allowed. + explicit implies the objects in the template must have the namespace set. + enum: + - inherit + - explicit + type: string + required: + - expanders + type: object + status: + description: CompositionStatus defines the observed state of Composition + properties: + conditions: + 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 + generation: + format: int64 + type: integer + stages: + additionalProperties: + description: StageStatus captures the status of a stage + properties: + message: + type: string + reason: + type: string + validationStatus: + type: string + type: object + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: contexts.composition.google.com +spec: + group: composition.google.com + names: + kind: Context + listKind: ContextList + plural: contexts + singular: context + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Context is the Schema for the contexts API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ContextSpec defines the desired state of Context + properties: + project: + description: Project is passed to the expander. + type: string + type: object + status: + description: ContextStatus defines the observed state of Context + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: expanderversions.composition.google.com +spec: + group: composition.google.com + names: + kind: ExpanderVersion + listKind: ExpanderVersionList + plural: expanderversions + singular: expanderversion + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ExpanderVersion is the Schema for the expanderversions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExpanderVersionSpec defines the desired state of ExpanderVersion + properties: + config: + description: ExpanderConfig GVK + properties: + group: + type: string + kind: + type: string + version: + type: string + required: + - group + - kind + - version + type: object + image: + description: Image if different from removePrefix(expanderversion.name + , "composition-") + type: string + imageRegistry: + description: ImageRegistry is the designated registry for where to + pull the named expander image + type: string + type: + default: job + description: |- + Type indicates what sort of expander: + job - job based expander. ephemeral + grpc - grpc service expander. persistent + enum: + - job + - grpc + type: string + validVersions: + description: ValidVersions is a list of valid versions of the named + expander + items: + type: string + type: array + required: + - type + - validVersions + type: object + status: + description: ExpanderVersionStatus defines the observed state of ExpanderVersion + properties: + conditions: + 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 + versionMap: + additionalProperties: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: facades.composition.google.com +spec: + group: composition.google.com + names: + kind: Facade + listKind: FacadeList + plural: facades + singular: facade + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Facade is the Schema for the facades API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FacadeSpec defines the desired state of Facade + properties: + facadeKind: + type: string + openAPIV3Schema: + description: Bring your own open API spec + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - facadeKind + - openAPIV3Schema + type: object + status: + description: FacadeStatus defines the observed state of Facade + properties: + conditions: + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: getterconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: GetterConfiguration + listKind: GetterConfigurationList + plural: getterconfigurations + singular: getterconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GetterConfiguration is the Schema for the getters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: GetterConfigurationSpec defines the desired state of GetterConfiguration + properties: + valuesFrom: + items: + properties: + fieldRef: + items: + properties: + as: + type: string + path: + type: string + required: + - as + - path + type: object + type: array + name: + type: string + resourceRef: + properties: + group: + description: OPTION 2 + type: string + kind: + type: string + name: + description: OneOf validation needed for Name and NameSuffix + in CRD Definition + type: string + nameSuffix: + type: string + resource: + type: string + version: + type: string + required: + - kind + - resource + type: object + required: + - fieldRef + - name + - resourceRef + type: object + type: array + type: object + status: + description: GetterConfigurationStatus defines the observed state of GetterConfiguration + properties: + conditions: + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: plans.composition.google.com +spec: + group: composition.google.com + names: + kind: Plan + listKind: PlanList + plural: plans + singular: plan + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Plan is the Schema for the plans API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PlanSpec defines the desired state of Plan + properties: + stages: + additionalProperties: + properties: + manifest: + type: string + values: + type: string + type: object + type: object + type: object + status: + description: PlanStatus defines the observed state of Plan + properties: + compositionGeneration: + description: Composition generation last succesfully reconciled + format: int64 + type: integer + compositionUID: + description: Composition UID + type: string + conditions: + 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 + generation: + description: Plan generation we last successfully reconciled + format: int64 + type: integer + inputGeneration: + description: Facade's generation last succesfully reconciled + format: int64 + type: integer + lastPruned: + items: + properties: + group: + type: string + health: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + status: + type: string + version: + type: string + required: + - health + - kind + type: object + type: array + stages: + additionalProperties: + description: StageStatus captures the status of a stage + properties: + appliedCount: + type: integer + lastApplied: + items: + properties: + group: + type: string + health: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + status: + type: string + version: + type: string + required: + - health + - kind + type: object + type: array + resourceCount: + type: integer + required: + - resourceCount + type: object + type: object + required: + - compositionGeneration + - inputGeneration + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/composition/release/manifest.yaml b/composition/release/manifest.yaml new file mode 100644 index 0000000..f48f4a8 --- /dev/null +++ b/composition/release/manifest.yaml @@ -0,0 +1,1778 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/component: composition-controller-manager + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: composition-controller-manager-system + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: composition-controller-manager-namespace + app.kubernetes.io/part-of: composition + control-plane: controller-manager + name: composition-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: compositions.composition.google.com +spec: + group: composition.google.com + names: + kind: Composition + listKind: CompositionList + plural: compositions + singular: composition + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Composition is the Schema for the compositions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: CompositionSpec defines the desired state of Composition + properties: + description: + type: string + expanders: + items: + properties: + configref: + description: ConfigReference - For BYO Expanders, we can extend + it + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + jinja2: + description: Built in expanders + properties: + template: + type: string + required: + - template + type: object + name: + type: string + template: + description: For BYO Expanders use generic template or ref for + external config + type: string + type: + default: jinja2 + description: |- + Type indicates what expander to use + jinja - jinja2 expander + ... + type: string + version: + default: latest + type: string + required: + - type + type: object + minItems: 1 + type: array + inputAPIGroup: + description: Use existing KRM API + type: string + namespaceMode: + description: |- + Namespace mode indicates how compositions set the namespace of the objects from expanders. + ""|inherit implies inherit the facade api's namespace. Only namespaced objects are allowed. + explicit implies the objects in the template must have the namespace set. + enum: + - inherit + - explicit + type: string + required: + - expanders + type: object + status: + description: CompositionStatus defines the observed state of Composition + properties: + conditions: + 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 + generation: + format: int64 + type: integer + stages: + additionalProperties: + description: StageStatus captures the status of a stage + properties: + message: + type: string + reason: + type: string + validationStatus: + type: string + type: object + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: contexts.composition.google.com +spec: + group: composition.google.com + names: + kind: Context + listKind: ContextList + plural: contexts + singular: context + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Context is the Schema for the contexts API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ContextSpec defines the desired state of Context + properties: + project: + description: Project is passed to the expander. + type: string + type: object + status: + description: ContextStatus defines the observed state of Context + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: expanderversions.composition.google.com +spec: + group: composition.google.com + names: + kind: ExpanderVersion + listKind: ExpanderVersionList + plural: expanderversions + singular: expanderversion + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ExpanderVersion is the Schema for the expanderversions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExpanderVersionSpec defines the desired state of ExpanderVersion + properties: + config: + description: ExpanderConfig GVK + properties: + group: + type: string + kind: + type: string + version: + type: string + required: + - group + - kind + - version + type: object + image: + description: Image if different from removePrefix(expanderversion.name + , "composition-") + type: string + imageRegistry: + description: ImageRegistry is the designated registry for where to + pull the named expander image + type: string + type: + default: job + description: |- + Type indicates what sort of expander: + job - job based expander. ephemeral + grpc - grpc service expander. persistent + enum: + - job + - grpc + type: string + validVersions: + description: ValidVersions is a list of valid versions of the named + expander + items: + type: string + type: array + required: + - type + - validVersions + type: object + status: + description: ExpanderVersionStatus defines the observed state of ExpanderVersion + properties: + conditions: + 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 + versionMap: + additionalProperties: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: facades.composition.google.com +spec: + group: composition.google.com + names: + kind: Facade + listKind: FacadeList + plural: facades + singular: facade + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Facade is the Schema for the facades API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FacadeSpec defines the desired state of Facade + properties: + facadeKind: + type: string + openAPIV3Schema: + description: Bring your own open API spec + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - facadeKind + - openAPIV3Schema + type: object + status: + description: FacadeStatus defines the observed state of Facade + properties: + conditions: + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: getterconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: GetterConfiguration + listKind: GetterConfigurationList + plural: getterconfigurations + singular: getterconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GetterConfiguration is the Schema for the getters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: GetterConfigurationSpec defines the desired state of GetterConfiguration + properties: + valuesFrom: + items: + properties: + fieldRef: + items: + properties: + as: + type: string + path: + type: string + required: + - as + - path + type: object + type: array + name: + type: string + resourceRef: + properties: + group: + description: OPTION 2 + type: string + kind: + type: string + name: + description: OneOf validation needed for Name and NameSuffix + in CRD Definition + type: string + nameSuffix: + type: string + resource: + type: string + version: + type: string + required: + - kind + - resource + type: object + required: + - fieldRef + - name + - resourceRef + type: object + type: array + type: object + status: + description: GetterConfigurationStatus defines the observed state of GetterConfiguration + properties: + conditions: + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: plans.composition.google.com +spec: + group: composition.google.com + names: + kind: Plan + listKind: PlanList + plural: plans + singular: plan + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Plan is the Schema for the plans API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PlanSpec defines the desired state of Plan + properties: + stages: + additionalProperties: + properties: + manifest: + type: string + values: + type: string + type: object + type: object + type: object + status: + description: PlanStatus defines the observed state of Plan + properties: + compositionGeneration: + description: Composition generation last succesfully reconciled + format: int64 + type: integer + compositionUID: + description: Composition UID + type: string + conditions: + 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 + generation: + description: Plan generation we last successfully reconciled + format: int64 + type: integer + inputGeneration: + description: Facade's generation last succesfully reconciled + format: int64 + type: integer + lastPruned: + items: + properties: + group: + type: string + health: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + status: + type: string + version: + type: string + required: + - health + - kind + type: object + type: array + stages: + additionalProperties: + description: StageStatus captures the status of a stage + properties: + appliedCount: + type: integer + lastApplied: + items: + properties: + group: + type: string + health: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + status: + type: string + version: + type: string + required: + - health + - kind + type: object + type: array + resourceCount: + type: integer + required: + - resourceCount + type: object + type: object + required: + - compositionGeneration + - inputGeneration + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: composition + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: getter-expander-sa + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: composition + name: composition-getter-expander + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: role + app.kubernetes.io/part-of: composition + name: composition-leader-election-role + namespace: composition-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: facade + app.kubernetes.io/instance: cloudsql-editor-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: facade + name: composition-cloudsql-editor-role +rules: +- apiGroups: + - facade.facade + resources: + - cloudsqls + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - facade.facade + resources: + - cloudsqls/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: composition + name: composition-expanderversion-editor-role +rules: +- apiGroups: + - composition.google.com + resources: + - expanderversions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - expanderversions/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: composition + name: composition-expanderversion-viewer-role +rules: +- apiGroups: + - composition.google.com + resources: + - expanderversions + verbs: + - get + - list + - watch +- apiGroups: + - composition.google.com + resources: + - expanderversions/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-getter-expander +rules: +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch +- apiGroups: + - '*' + resources: + - '*' + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - patch +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - get + - list + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch +- apiGroups: + - composition.google.com + resources: + - compositions + - contexts + - expanderversions + - facades + - getterconfigurations + - plans + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - compositions/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - compositions/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - contexts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - contexts/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - contexts/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - expanderversions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - expanderversions/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - expanderversions/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - facades + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - facades/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - facades/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - getterconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - getterconfigurations/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - getterconfigurations/status + verbs: + - get + - patch + - update +- apiGroups: + - composition.google.com + resources: + - plans + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - composition.google.com + resources: + - plans/finalizers + verbs: + - update +- apiGroups: + - composition.google.com + resources: + - plans/status + verbs: + - get + - patch + - update +- apiGroups: + - facade.compositions.google.com + resources: + - '*' + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - facade.compositions.google.com + resources: + - '*/status' + verbs: + - get + - update +- apiGroups: + - facade.facade + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: composition + name: composition-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: composition + name: composition-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: composition + name: composition-leader-election-rolebinding + namespace: composition-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: composition-leader-election-role +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: getter-expander-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: composition + name: composition-getter-expander-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-getter-expander +subjects: +- kind: ServiceAccount + name: composition-getter-expander + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: composition + name: composition-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-manager-role +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: composition + name: composition-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-proxy-role +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + control-plane: controller-manager + name: composition-controller-manager-metrics-service + namespace: composition-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: getter-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + control-plane: expander-getter + name: composition-getter-v0-0-1 + namespace: composition-system +spec: + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-getter-v0.0.1 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: jinja2-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + control-plane: expander-jinja2 + name: composition-jinja2-v0-0-1 + namespace: composition-system +spec: + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-jinja2-v0.0.1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: composition-controller-manager + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: composition-controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: composition-controller-manager + app.kubernetes.io/part-of: composition + control-plane: controller-manager + name: composition-controller-manager + namespace: composition-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 5m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: gcr.io/krmapihosting-release/composition:0.0.406 + imagePullPolicy: Always + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: composition-controller-manager + terminationGracePeriodSeconds: 10 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: getter-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: composition + control-plane: expander-getter-v0.0.1 + name: composition-getter-v0.0.1 + namespace: composition-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: expander-getter-v0.0.1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-getter-v0.0.1 + spec: + containers: + - args: + - --port=8443 + command: + - /expander + image: gcr.io/krmapihosting-release/expander-getter:v0.0.1 + name: getter + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + serviceAccountName: composition-getter-expander + terminationGracePeriodSeconds: 10 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: jinja2-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: composition + control-plane: expander-jinja2-v0.0.1 + name: composition-jinja2-v0.0.1 + namespace: composition-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: expander-jinja2-v0.0.1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-jinja2-v0.0.1 + spec: + containers: + - args: + - --port=8443 + command: + - /expander + image: gcr.io/krmapihosting-release/expander-jinja2:v0.0.1 + name: jinja2 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + terminationGracePeriodSeconds: 10 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: composition-getter + namespace: composition-system +spec: + config: + group: composition.google.com + kind: GetterConfiguration + version: v1alpha1 + type: grpc + validVersions: + - v0.0.1 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: composition-jinja2 + namespace: composition-system +spec: + type: grpc + validVersions: + - v0.0.1 + - v0.0.0 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: composition-jinja2-job + namespace: composition-system +spec: + image: expander-jinja2 + imageRegistry: gcr.io/krmapihosting-release + type: job + validVersions: + - v0.0.115 + - v0.0.114 + - v0.0.113 + - v0.0.112 + - v0.0.111 + - v0.0.110 + - v0.0.1 + - v0.0.0 diff --git a/composition/tests/cluster/cluster.go b/composition/tests/cluster/cluster.go new file mode 100644 index 0000000..14a8782 --- /dev/null +++ b/composition/tests/cluster/cluster.go @@ -0,0 +1,170 @@ +// Copyright 2024 Google LLC +// +// 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 cluster + +import ( + "fmt" + "log" + "strings" + "sync" + "testing" + "time" + + "github.com/cloud-native-compositions/compositions/composition/tests/cluster/configcontroller" + "github.com/cloud-native-compositions/compositions/composition/tests/cluster/kind" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" +) + +const ( + CRDManifests = "../../release/test/crds.yaml" + FacadeCRDManifests = "../../release/test/facade_crds.yaml" + KindOperatorManifests = "../../release/test/kind-operator.yaml" + CCOperatorManifests = "../../release/test/cc-operator.yaml" +) + +type ClusterUser interface { + Config() *rest.Config + Name() string + RestartWorkloads() error + WaitForWorkloads() error + KCCInstalled() bool + Context() map[string]string +} + +type ClusterSet struct { + available sync.Map +} + +var clusterSet ClusterSet + +func AddCluster(c ClusterUser) { + clusterSet.available.Store(c.Name(), c) +} + +func RemoveCluster(c ClusterUser) { + clusterSet.available.Delete(c.Name()) +} + +func ReserveCluster(t *testing.T) ClusterUser { + var cluster ClusterUser + found := false + for !found { + clusterSet.available.Range(func(k, v any) bool { + c, loaded := clusterSet.available.LoadAndDelete(k) + if !loaded { + return true + } + found = true + cluster = c.(ClusterUser) + return false + }) + + if found { + continue + } + + t.Logf("\nWaiting for a cluster to become available...") + time.Sleep(5 * time.Second) + } + + t.Logf("Reserved cluster %s", cluster.Name()) + return cluster +} + +func ReleaseCluster(t *testing.T, cluster ClusterUser) { + clusterSet.available.Store(cluster.Name(), cluster) + t.Logf("Released cluster %s", cluster.Name()) +} + +// Create Kind Clusters +func CreateKindClusters(clusterCount int, images string) { + var wg sync.WaitGroup + wg.Add(clusterCount) + // Start with 1 e2e cluster + for i := 0; i < clusterCount; i++ { + go func(index int) { + name := fmt.Sprintf("composition-e2e-%d", index) + // kind cluster + kc := kind.NewCluster(name, + // that adds these images + strings.Split(images, ","), + // and installs these manifests + []string{CRDManifests, KindOperatorManifests}, + // and waits for these deployments to be ready + []types.NamespacedName{ + {Namespace: "composition-system", Name: "composition-controller-manager"}, + }, + ) + + // Bringup the cluster and install the operator + err := kc.ClusterUp() + if err != nil { + log.Fatalf("Error creating kind cluster: %s, %v", name, err) + } + // TODO ADD CLUSTER + AddCluster(kc) + wg.Done() + }(i) + } + wg.Wait() +} + +// Create CC Cluster +func CreateCCClusters(clusterCount int, images string) { + var wg sync.WaitGroup + if clusterCount > 1 { + log.Fatalf("clusterCount must be 1 for CC clusters") + return + } + wg.Add(clusterCount) + // Start with 1 e2e cluster + for i := 0; i < clusterCount; i++ { + go func(index int) { + name := fmt.Sprintf("composition-e2e-%d", index) + // cc cluster + + // HACK for master CIDR + // https://stackoverflow.com/questions/55399928/cidr-range-for-master-ipv4-cidr-in-gke-private-cluster + // You can use any of the following CIDRs + // 10.0.0.0 – 10.255.255.255, + // 172.16.0.0 – 172.31.255.255, + // 192.168.0.0 – 192.168.255.255 + // except 172.16 and 172.17. + // Also this must be /28 + masterCidr := fmt.Sprintf("172.18.%d.0/28", index) + + cc := configcontroller.NewCluster(name, masterCidr, + // and installs these manifests + []string{CRDManifests, CCOperatorManifests}, + // and waits for these deployments to be ready + []types.NamespacedName{ + {Namespace: "composition-system", Name: "composition-controller-manager"}, + }, + ) + + // Bringup the cluster and install the operator + err := cc.ClusterUp() + if err != nil { + log.Fatalf("Error creating CC cluster: %s, %v", name, err) + } + + // TODO ADD CLUSTER + AddCluster(cc) + wg.Done() + }(i) + } + wg.Wait() +} diff --git a/composition/tests/cluster/configcontroller/cluster.go b/composition/tests/cluster/configcontroller/cluster.go new file mode 100644 index 0000000..f6a66c5 --- /dev/null +++ b/composition/tests/cluster/configcontroller/cluster.go @@ -0,0 +1,365 @@ +// Copyright 2024 Google LLC +// +// 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 configcontroller + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/composition/tests/utils" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" +) + +const ( + Region = "us-west1" +) + +var ( + scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + //TODO (barney-s) remove compositionv1alpha1 from test package + utilruntime.Must(compositionv1alpha1.AddToScheme(scheme)) +} + +type CCCluster interface { + ClusterUp() error + //Delete() error + //Exists() (bool, error) + + // ClusterUser + Config() *rest.Config + Name() string + RestartWorkloads() error + WaitForWorkloads() error + KCCInstalled() bool + Context() map[string]string +} + +type ccCluster struct { + name string + masterCIDR string + config *rest.Config + manifestPaths []string + deployments []types.NamespacedName + ctx context.Context + gcpProject string + gcpProjectNumber string + client.Client +} + +func NewCluster(name string, masterCidr string, manifestPaths []string, + deployments []types.NamespacedName) CCCluster { + return &ccCluster{ + name: name, + masterCIDR: masterCidr, + manifestPaths: manifestPaths, + deployments: deployments, + ctx: context.Background(), + } +} + +func (c *ccCluster) KCCInstalled() bool { return true } + +func (c *ccCluster) Config() *rest.Config { + return c.config +} + +func (c *ccCluster) Context() map[string]string { + return map[string]string{ + "clusterName": c.name, + "clusterLocation": Region, + "gcpProject": c.gcpProject, + "gcpProjectNumber": c.gcpProjectNumber, + } +} + +func (c *ccCluster) Name() string { + return c.name +} + +func (c *ccCluster) RestartWorkloads() error { + return nil +} + +func (c *ccCluster) exec(cmd string, args ...string) string { + commandString := fmt.Sprintf("%s %s", cmd, strings.Join(args, " ")) + op, err := exec.Command(cmd, args...).CombinedOutput() + if err != nil { + log.Fatalf("Failed running cmd: %s \noutput: %s\nerr: %v", commandString, string(op), err) + } + return strings.TrimSuffix(string(op), "\n") +} + +func (c *ccCluster) create() error { + // reuse existing project + // gcloud config list --format 'value(core.project)' + project := c.exec("gcloud", "config", "list", "--format", "value(core.project)") + c.gcpProject = project + + //gcloud projects describe $PROJECT_ID --format="value(projectNumber)" + projectNumber := c.exec("gcloud", "projects", "describe", + project, "--format", "value(projectNumber)") + c.gcpProjectNumber = projectNumber + + // create or reuse cc if it exists + getcc, err := exec.Command("gcloud", "anthos", "config", "controller", + "describe", c.name, "--location", Region).CombinedOutput() + if err != nil { + // If exiterror (failed to get CC), ignore + if _, ok := err.(*exec.ExitError); !ok { + return err + } + } + if strings.Contains(string(getcc), "not found") { + // create CC instance + c.exec("gcloud", "anthos", "config", "controller", + "create", c.name, "--master-ipv4-cidr-block", c.masterCIDR, "--location", Region) + } + + // point kubectl to cc + c.exec("gcloud", "anthos", "config", "controller", + "get-credentials", c.name, "--location", Region) + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + // read from default kubectl context + kubectlContext := "gke_" + project + "_" + Region + "_krmapihost-" + c.name + c.config, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: homeDir + "/.kube/config"}, + &clientcmd.ConfigOverrides{ + ClusterInfo: clientcmdapi.Cluster{ + Server: "", + }, + CurrentContext: kubectlContext, + }).ClientConfig() + if err != nil { + return err + } + + // grant cc permissions + sa := fmt.Sprintf("serviceAccount:service-%s@gcp-sa-yakima.iam.gserviceaccount.com", projectNumber) + //gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ + // --member "serviceAccount:${SA_EMAIL}" --role "roles/owner" \ + // --project "${PROJECT_ID}" + c.exec( + "gcloud", "projects", "add-iam-policy-binding", project, + "--member", sa, + "--role", "roles/owner", + "--project", project, + ) + + // allow custom workloads + + //kubectl patch k8sallowedresources.constraints.gatekeeper.sh block-workloads \ + // --patch '{"spec":{"enforcementAction":"dryrun"}}' --type merge + // Not needed anymore: https://source.corp.google.com/h/acp/acp/+/51a8ebb6fbb9de6637dd0eda72cf6c16937fd107 + /* + c.exec("kubectl", "patch", + "k8sallowedresources.constraints.gatekeeper.sh", + "block-workloads", "--patch", + "{\"spec\":{\"enforcementAction\":\"dryrun\"}}", "--type", "merge") + */ + + //kubectl label validatingwebhookconfigurations.admissionregistration.k8s.io \ + // gatekeeper-validating-webhook-configuration policycontroller.configmanagement.gke.io/managed-by-operator- + c.exec("kubectl", "label", + "validatingwebhookconfigurations.admissionregistration.k8s.io", + "gatekeeper-validating-webhook-configuration", + "policycontroller.configmanagement.gke.io/managed-by-operator-") + + //kubectl patch validatingwebhookconfigurations.admissionregistration.k8s.io \ + // gatekeeper-validating-webhook-configuration \ + // --type=json -p '[ {"op":"remove","path":"/webhooks"} ]' + c.exec("kubectl", "patch", + "validatingwebhookconfigurations.admissionregistration.k8s.io", + "gatekeeper-validating-webhook-configuration", "--type", "json", + "-p", "[ {\"op\":\"remove\",\"path\":\"/webhooks\"} ]") + + // this is reverted: + // kubectl patch K8sAllowedResources forbidden-namespaces --type=json --patch \ + // '[ {"op":"add","path":"/spec/parameters/exemptedNamespaces/-", "value": "composition-system"} ]' + // so do this instead: + // kubectl patch K8sAllowedResources forbidden-namespaces \ + // --patch '{"spec":{"enforcementAction":"dryrun"}}' --type merge + c.exec("kubectl", "patch", + "K8sAllowedResources", + "forbidden-namespaces", + "--patch", "{\"spec\":{\"enforcementAction\":\"dryrun\"}}", + "--type", "merge", + ) + /* + // Enable 3p workloads + c.exec("gcloud", "container", "clusters", "update", c.name, + "--enable-autoprovisioning", + "--min-cpu", "1", + "--min-memory", "1", + "--max-cpu", "10", + "--max-memory", "64", + "--autoprovisioning-scopes", + "https://www.googleapis.com/auth/logging.write," + + https://www.googleapis.com/auth/monitoring," + + https://www.googleapis.com/auth/devstorage.read_only", + ) + */ + + // allow image pulls + //export defaultGCESA="$(gcloud iam service-accounts list --format=json | \ + // jq '.[] | select(.displayName == "Compute Engine default service account") | .email' | xargs echo)" + defaultGCESA := fmt.Sprintf("%s-compute@developer.gserviceaccount.com", projectNumber) + registryBucket := fmt.Sprintf("gs://artifacts.%s.appspot.com/", project) + iamPermissions := fmt.Sprintf("serviceAccount:%s:roles/storage.objectViewer", defaultGCESA) + c.exec("gsutil", "iam", "ch", iamPermissions, registryBucket) + + c.Client, err = client.New(c.Config(), client.Options{Scheme: scheme}) + if err != nil { + return err + } + return nil +} + +func (c *ccCluster) installManifests() error { + for _, path := range c.manifestPaths { + manifests, err := os.ReadFile(path) + if err != nil { + return err + } + objects, err := manifest.ParseObjects(context.Background(), string(manifests)) + if err != nil { + return err + } + for _, item := range objects.Items { + err := c.Client.Create(context.Background(), item.UnstructuredObject()) + if err != nil { + exists := apierrors.IsAlreadyExists(err) + if exists { + continue + } + return err + } + } + } + return nil +} + +func (c *ccCluster) ClusterUp() error { + err := c.create() + if err != nil { + return fmt.Errorf("Error Creating Cluster. err: %v", err) + } + + err = c.installManifests() + if err != nil { + return fmt.Errorf("Error Installing Manifests. err: %v", err) + } + + err = c.WaitForWorkloads() + if err != nil { + return fmt.Errorf("Error Waiting for Deloyments. err: %v", err) + } + + // HACK - Ties this lib to Composition. Shall figure out callback lateron. + // May be parameterize manifests with context + contextObj := utils.GetContextObj(c.Context()) + err = c.Client.Create(context.Background(), contextObj) + if err != nil { + if !apierrors.IsAlreadyExists(err) { + return fmt.Errorf("Error creating config-control/context. err: %v", err) + } + } + + return nil +} + +// isReady - is the object ready +func isReady(ctx context.Context, c client.Client, u *unstructured.Unstructured) (bool, error) { + key := types.NamespacedName{ + Name: u.GetName(), + Namespace: u.GetNamespace(), + } + err := c.Get(ctx, key, u) + result := &kstatus.Result{} + if err != nil { + if !apierrors.IsNotFound(err) { + return false, err + } + return false, nil + } else { + result, err = kstatus.Compute(u) + if err != nil { + return false, err + } + } + if result.Status != kstatus.CurrentStatus { + return false, nil + } + return true, nil +} + +func isDeploymentReady(ctx context.Context, c client.Client, nn types.NamespacedName) (bool, error) { + u := unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}) + u.SetName(nn.Name) + u.SetNamespace(nn.Namespace) + + return isReady(ctx, c, &u) +} + +func (c *ccCluster) WaitForWorkloads() error { + start := time.Now() + for { + allReady := true + for _, workload := range c.deployments { + ready, err := isDeploymentReady(c.ctx, c.Client, workload) + if err != nil { + continue + } + if !ready { + allReady = false + break + } + } + if allReady { + break + } + if time.Since(start).Seconds() > 40 { + return fmt.Errorf("timed out waiting for operator to be ready") + } + time.Sleep(2 * time.Second) + } + return nil +} diff --git a/composition/tests/cluster/kind/cluster.go b/composition/tests/cluster/kind/cluster.go new file mode 100644 index 0000000..a1acbcf --- /dev/null +++ b/composition/tests/cluster/kind/cluster.go @@ -0,0 +1,379 @@ +// Copyright 2024 Google LLC +// +// 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 kind + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" +) + +const ( + ReuseIfPresent bool = true + RecreateIfPresent bool = false +) + +var ( + scheme = runtime.NewScheme() +) + +type kindCluster struct { + name string + config *rest.Config + manifestPaths []string + images []string + deployments []types.NamespacedName + ctx context.Context + client.Client +} + +type KindCluster interface { + ClusterUp() error + Delete() error + Exists() (bool, error) + + // ClusterUser + Config() *rest.Config + Name() string + RestartWorkloads() error + WaitForWorkloads() error + KCCInstalled() bool + Context() map[string]string +} + +// NewCluster - return a cluster setup object +func NewCluster(name string, images []string, + manifestPaths []string, deployments []types.NamespacedName) KindCluster { + return &kindCluster{ + name: name, + manifestPaths: manifestPaths, + images: images, + deployments: deployments, + ctx: context.Background(), + } +} + +func VerifyKindIsInstalled() error { + _, err := exec.LookPath("kind") + if err != nil { + return err + } + return nil +} + +// Wait for all clusters to become ready +func (c *kindCluster) create() error { + err := c.Delete() + if err != nil { + return fmt.Errorf("Delete existing cluster if present. err: %v", err) + } + clusterConfig, err := c.kindClusterDefinition() + if err != nil { + return fmt.Errorf("kindClusterDefinition failed. err: %v", err) + } + defer func() { + err := os.Remove(clusterConfig) + if err != nil { + panic("Error removing file: " + clusterConfig) + } + }() + c.config, err = c.createCluster(clusterConfig) + if err != nil { + return fmt.Errorf("createCluster() failed. err: %v", err) + } + + c.Client, err = client.New(c.Config(), client.Options{Scheme: scheme}) + if err != nil { + return fmt.Errorf("getting client for kind cluster failed. err: %v", err) + } + + return nil +} + +func (c *kindCluster) registerImages() error { + for _, image := range c.images { + err := c.LoadImage(image) + if err != nil { + return err + } + } + return nil +} + +func (c *kindCluster) installManifests() error { + for _, path := range c.manifestPaths { + manifests, err := os.ReadFile(path) + if err != nil { + return err + } + objects, err := manifest.ParseObjects(context.Background(), string(manifests)) + if err != nil { + return err + } + for _, item := range objects.Items { + err := c.Client.Create(context.Background(), item.UnstructuredObject()) + if err != nil { + exists := apierrors.IsAlreadyExists(err) + if exists { + continue + } + return err + } + } + } + return nil +} + +// isReady - is the object ready +func isReady(ctx context.Context, c client.Client, u *unstructured.Unstructured) (bool, error) { + key := types.NamespacedName{ + Name: u.GetName(), + Namespace: u.GetNamespace(), + } + err := c.Get(ctx, key, u) + result := &kstatus.Result{} + if err != nil { + if !apierrors.IsNotFound(err) { + return false, err + } + return false, nil + } else { + result, err = kstatus.Compute(u) + if err != nil { + return false, err + } + } + if result.Status != kstatus.CurrentStatus { + return false, nil + } + return true, nil +} + +func isDeploymentReady(ctx context.Context, c client.Client, nn types.NamespacedName) (bool, error) { + u := unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}) + u.SetName(nn.Name) + u.SetNamespace(nn.Namespace) + + return isReady(ctx, c, &u) +} + +func (c *kindCluster) WaitForWorkloads() error { + start := time.Now() + for { + allReady := true + for _, workload := range c.deployments { + ready, err := isDeploymentReady(c.ctx, c.Client, workload) + if err != nil { + continue + } + if !ready { + allReady = false + break + } + } + if allReady { + break + } + if time.Since(start).Seconds() > 40 { + return fmt.Errorf("timed out waiting for operator to be ready") + } + time.Sleep(2 * time.Second) + } + return nil +} + +func (c *kindCluster) RestartWorkloads() error { + return nil +} + +// ClusterUp: Create() + registerImages() + installManifests() + WaitForWorkloads() +func (c *kindCluster) ClusterUp() error { + err := c.create() + if err != nil { + return fmt.Errorf("Error Creating Cluster. err: %v", err) + } + + err = c.registerImages() + if err != nil { + return fmt.Errorf("Error Registering Images. err: %v", err) + } + + err = c.installManifests() + if err != nil { + return fmt.Errorf("Error Installing Manifests. err: %v", err) + } + + err = c.WaitForWorkloads() + if err != nil { + return fmt.Errorf("Error Waiting for Deloyments. err: %v", err) + } + + return nil +} + +// Config return rest.Config +func (c *kindCluster) Config() *rest.Config { + return c.config +} + +func (c *kindCluster) KCCInstalled() bool { return false } + +func (c *kindCluster) Context() map[string]string { + return map[string]string{ + "clusterName": c.name, + } +} + +// Name return name +func (c *kindCluster) Name() string { + return c.name +} + +func (c *kindCluster) String() string { + return c.name +} + +// LoadImage loads a docker image into the cluster +func (c *kindCluster) LoadImage(image string) error { + err := exec.Command("kind", "load", "docker-image", image, "--name", c.name).Run() + if err != nil { + return err + } + return nil +} + +// Delete deletes all clusters +func (c *kindCluster) Delete() error { + err := exec.Command("kind", "delete", "cluster", "--name", c.name).Run() + if err != nil { + return err + } + return nil +} + +// Exists checks if the cluster exists +func (c *kindCluster) Exists() (bool, error) { + output, err := exec.Command("kind", "get", "clusters").CombinedOutput() + if err != nil { + return false, err + } + + for _, cluster := range strings.Split(string(output), "\n") { + if cluster == c.name { + return true, nil + } + } + return false, nil +} + +func (c *kindCluster) kindClusterDefinition() (string, error) { + ipAddress, err := c.getHostIPAddress() + if err != nil { + return "", fmt.Errorf("getHostIPAddress failed. err: %v", err) + } + clusterConfigFile, err := os.CreateTemp("", "kind-cluster.yaml") + if err != nil { + return "", err + } + kindClusterConfig := `kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +networking: + # Allow connections to the API Sever with the host IP address + apiServerAddress: "` + ipAddress + `"` + bytes := []byte(kindClusterConfig) + if _, err = clusterConfigFile.Write(bytes); err != nil { + return "", err + } + if err = clusterConfigFile.Close(); err != nil { + return "", err + } + return clusterConfigFile.Name(), err +} + +func (c *kindCluster) createCluster(clusterConfig string) (*rest.Config, error) { + op, err := exec.Command("kind", "create", "cluster", "--name", c.name, "--config", clusterConfig).CombinedOutput() + if err != nil { + return nil, fmt.Errorf("kind create cluster command failed: %v\n output: %s", err, op) + } + + kubeConfigFile, err := os.CreateTemp("", "kubeconfig.yaml") + if err != nil { + return nil, err + } + defer func() { + err := os.Remove(kubeConfigFile.Name()) + if err != nil { + panic("Error removing kubeconfig file") + } + }() + content, err := exec.Command("kind", "get", "kubeconfig", "--name", c.name).CombinedOutput() + if err != nil { + return nil, err + } + bytes := content + if _, err = kubeConfigFile.Write(bytes); err != nil { + return nil, err + } + err = kubeConfigFile.Close() + if err != nil { + return nil, err + } + + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigFile.Name()}, + &clientcmd.ConfigOverrides{ + ClusterInfo: clientcmdapi.Cluster{ + Server: "", + }, + CurrentContext: "", + }).ClientConfig() +} + +func (c *kindCluster) getHostIPAddress() (string, error) { + // Try getting host ip by creating a connection object and reading the localaddr + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + return "", err + } + localAddr := conn.LocalAddr().(*net.UDPAddr) + err = conn.Close() + if err != nil { + return "", err + } + return localAddr.IP.String(), nil +} + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) +} diff --git a/composition/tests/data/TestCustomStatusCELRule/input.yaml b/composition/tests/data/TestCustomStatusCELRule/input.yaml new file mode 100644 index 0000000..fea00d1 --- /dev/null +++ b/composition/tests/data/TestCustomStatusCELRule/input.yaml @@ -0,0 +1,175 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: team-page + namespace: default +spec: + inputAPIGroup: teampages.idp.mycompany.com # Facade API + readiness: + - group: apps + version: v1 + kind: Deployment + readyIf: "status.readyReplicas==status.replicas" + expanders: + - type: jinja2 # pluggable expander + name: server # stage + template: | + apiVersion: apps/v1 + kind: Deployment + metadata: + # Use the teampages Facade's name + name: team-{{ teampages.metadata.name }} + # The namespace is set to the facade's namespace + namespace: default + labels: + # use facade's name in the label + app: nginx-{{ teampages.metadata.name }} + spec: + replicas: 1 + selector: + matchLabels: + app: nginx-{{ teampages.metadata.name }} + template: + metadata: + labels: + # use facade name in the pod's label + app: nginx-{{ teampages.metadata.name }} + spec: + containers: + - name: server + image: nginx:1.16.0 + ports: + - name: http + containerPort: 80 + protocol: TCP + volumeMounts: + - name: index + mountPath: /usr/share/nginx/html/ + volumes: + - name: index + configMap: + # use the configmap created by this composition + name: team-{{ teampages.metadata.name }}-page + --- + apiVersion: v1 + kind: Service + metadata: + # include the facade name in the service name + name: team-{{ teampages.metadata.name }}-landing + namespace: default + labels: + app: nginx-{{ teampages.metadata.name }} + spec: + ports: + - port: 80 + protocol: TCP + selector: + # match the web-server pod + app: nginx-{{ teampages.metadata.name }} + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: team-{{ teampages.metadata.name }}-page + namespace: default + data: + index.html: | + +

{{ teampages.metadata.name }}

+ + + + + + {% for member in teampages.spec.members %} + + + + + {% endfor %} +
NameRole
{{ member.name }}{{ member.role }}
+ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: teampages.idp.mycompany.com +spec: + group: idp.mycompany.com + names: + categories: + - facade + - facades + kind: TeamPage + listKind: TeamPageList + plural: teampages + singular: teampage + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Team Page Creation + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + required: + - members + properties: + members: + type: array + items: + type: object + required: + - role + - name + properties: + role: + type: string + name: + type: string + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: my-team +--- +apiVersion: idp.mycompany.com/v1alpha1 +kind: TeamPage +metadata: + name: landing + namespace: my-team +spec: + members: + - name: Jo + role: Eng Manager + - name: Jane + role: Lead + - name: Bob + role: Developer \ No newline at end of file diff --git a/composition/tests/data/TestCustomStatusCELRule/output.yaml b/composition/tests/data/TestCustomStatusCELRule/output.yaml new file mode 100644 index 0000000..b9f66cf --- /dev/null +++ b/composition/tests/data/TestCustomStatusCELRule/output.yaml @@ -0,0 +1,96 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx-landing + name: team-landing + namespace: my-team +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: nginx-landing + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + creationTimestamp: null + labels: + app: nginx-landing + spec: + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + name: server + ports: + - containerPort: 80 + name: http + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /usr/share/nginx/html/ + name: index + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + name: team-landing-page + name: index +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: team-landing-page + namespace: my-team +data: + index.html: |- + +

landing

+ + + + + + + + + + + + + + + + + + + + + +
NameRole
JoEng Manager
JaneLead
BobDeveloper
+ diff --git a/composition/tests/data/TestDeleteFacade/input.yaml b/composition/tests/data/TestDeleteFacade/input.yaml new file mode 100644 index 0000000..9482a13 --- /dev/null +++ b/composition/tests/data/TestDeleteFacade/input.yaml @@ -0,0 +1,141 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: dconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: DConfig + listKind: DConfigList + plural: dconfigs + singular: dconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: DConfig is the Schema for the dconfigs API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DConfigSpec defines the desired state of DConfig + properties: + namespace: + type: string + projects: + items: + type: string + type: array + required: + - namespace + - projects + type: object + status: + description: DConfigStatus defines the observed state of DConfig + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: dprojectconfigmap + namespace: default +spec: + inputAPIGroup: dconfigs.facade.foocorp.com + namespaceMode: explicit + expanders: + - type: jinja2 + name: createnamespace + template: | + apiVersion: v1 + kind: Namespace + metadata: + name: {{ dconfigs.spec.namespace }} + - type: jinja2 + name: project + template: | + {% for project in dconfigs.spec.projects %} + apiVersion: composition.google.com/v1alpha1 + kind: Context + metadata: + name: context-{{ project }} + namespace: {{ dconfigs.spec.namespace }} + spec: + project: {{ project }} + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: d-{{ project }} + namespace: {{ dconfigs.spec.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ project }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + {% endfor %} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: configs +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: DConfig +metadata: + name: config-team-a + namespace: configs +spec: + projects: + - proj-a + - proj-b + namespace: team-a-ns +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: DConfig +metadata: + name: config-team-b + namespace: configs +spec: + projects: + - proj-a + - proj-b + namespace: team-b-ns \ No newline at end of file diff --git a/composition/tests/data/TestDeleteFacade/output.yaml b/composition/tests/data/TestDeleteFacade/output.yaml new file mode 100644 index 0000000..2534a9c --- /dev/null +++ b/composition/tests/data/TestDeleteFacade/output.yaml @@ -0,0 +1,253 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + kubernetes.io/metadata.name: team-a-ns + name: team-a-ns +spec: + finalizers: + - kubernetes +status: + phase: Active +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + kubernetes.io/metadata.name: team-b-ns + name: team-b-ns +spec: + finalizers: + - kubernetes +status: + phase: Active +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context-proj-a + namespace: team-a-ns +spec: + project: proj-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context-proj-b + namespace: team-a-ns +spec: + project: proj-b +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context-proj-a + namespace: team-b-ns +spec: + project: proj-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context-proj-b + namespace: team-b-ns +spec: + project: proj-b +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: d-proj-a + namespace: team-a-ns +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-b +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: d-proj-b + namespace: team-a-ns +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: d-proj-a + namespace: team-b-ns +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-b +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: d-proj-b + namespace: team-b-ns +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: team-a-ns + applyset.kubernetes.io/contains-group-kinds: ConfigMap,Context.composition.google.com,Namespace + applyset.kubernetes.io/tooling: Plan/ + name: dconfigs-config-team-a + namespace: configs + ownerReferences: + - apiVersion: facade.updatetest.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: DConfig + name: config-team-a +spec: + stages: + createnamespace: + manifest: | + apiVersion: v1 + kind: Namespace + metadata: + name: team-a-ns + project: + manifest: |2+ + + apiVersion: composition.google.com/v1alpha1 + kind: Context + metadata: + name: context-proj-a + namespace: team-a-ns + spec: + project: proj-a + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: d-proj-a + namespace: team-a-ns + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + + apiVersion: composition.google.com/v1alpha1 + kind: Context + metadata: + name: context-proj-b + namespace: team-a-ns + spec: + project: proj-b + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: d-proj-b + namespace: team-a-ns + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-b + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: team-b-ns + applyset.kubernetes.io/contains-group-kinds: ConfigMap,Context.composition.google.com,Namespace + applyset.kubernetes.io/tooling: Plan/ + name: dconfigs-config-team-b + namespace: configs + ownerReferences: + - apiVersion: facade.updatetest.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: DConfig + name: config-team-b +spec: + stages: + createnamespace: + manifest: | + apiVersion: v1 + kind: Namespace + metadata: + name: team-b-ns + project: + manifest: |2+ + + apiVersion: composition.google.com/v1alpha1 + kind: Context + metadata: + name: context-proj-a + namespace: team-b-ns + spec: + project: proj-a + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: d-proj-a + namespace: team-b-ns + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + + apiVersion: composition.google.com/v1alpha1 + kind: Context + metadata: + name: context-proj-b + namespace: team-b-ns + spec: + project: proj-b + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: d-proj-b + namespace: team-b-ns + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-b + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- \ No newline at end of file diff --git a/composition/tests/data/TestImplicitGetter/input.yaml b/composition/tests/data/TestImplicitGetter/input.yaml new file mode 100644 index 0000000..e1cfe24 --- /dev/null +++ b/composition/tests/data/TestImplicitGetter/input.yaml @@ -0,0 +1,186 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: team2-page + namespace: default +spec: + inputAPIGroup: team2pages.idp.mycompany.com # Facade API + readiness: + - group: apps + version: v1 + kind: Deployment + readyIf: "status.readyReplicas==status.replicas" + expanders: + - type: jinja2 # pluggable expander + name: server # stage + template: | + apiVersion: apps/v1 + kind: Deployment + metadata: + # Use the team2pages Facade's name + name: team2deployment + # The namespace is set to the facade's namespace + namespace: default + labels: + # use facade's name in the label + app: nginx-{{ team2pages.metadata.name }} + spec: + replicas: 1 + selector: + matchLabels: + app: nginx-{{ team2pages.metadata.name }} + template: + metadata: + labels: + # use facade name in the pod's label + app: nginx-{{ team2pages.metadata.name }} + spec: + containers: + - name: server + image: nginx:1.16.0 + ports: + - name: http + containerPort: 80 + protocol: TCP + volumeMounts: + - name: index + mountPath: /usr/share/nginx/html/ + volumes: + - name: index + configMap: + # use the configmap created by this composition + name: team2-{{ team2pages.metadata.name }}-page + --- + apiVersion: v1 + kind: Service + metadata: + # include the facade name in the service name + name: team2service + namespace: default + labels: + app: nginx-{{ team2pages.metadata.name }} + spec: + ports: + - port: 80 + protocol: TCP + selector: + # match the web-server pod + app: nginx-{{ team2pages.metadata.name }} + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: team2-{{ team2pages.metadata.name }}-page + namespace: default + data: + index.html: | + +

{{ team2pages.metadata.name }}

+ + + + + + {% for member in team2pages.spec.members %} + + + + + {% endfor %} +
NameRole
{{ member.name }}{{ member.role }}
+ + - type: jinja2 # pluggable expander + name: configmap # stage + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: team2config + namespace: default + data: + ports: "{% for port in values.service.team2service.spec.ports %}{{ port.port }}({{ port.protocol }}),{% endfor %}" + pods: "{{ values.deployment.team2deployment.status.readyReplicas }}" +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: team2pages.idp.mycompany.com +spec: + group: idp.mycompany.com + names: + categories: + - facade + - facades + kind: Team2Page + listKind: Team2PageList + plural: team2pages + singular: team2page + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Team page creation + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + required: + - members + properties: + members: + type: array + items: + type: object + required: + - role + - name + properties: + role: + type: string + name: + type: string + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: my-team2 +--- +apiVersion: idp.mycompany.com/v1alpha1 +kind: Team2Page +metadata: + name: landing + namespace: my-team2 +spec: + members: + - name: Jo + role: Eng Manager + - name: Jane + role: Lead + - name: Bob + role: Developer \ No newline at end of file diff --git a/composition/tests/data/TestImplicitGetter/output.yaml b/composition/tests/data/TestImplicitGetter/output.yaml new file mode 100644 index 0000000..bfd65c1 --- /dev/null +++ b/composition/tests/data/TestImplicitGetter/output.yaml @@ -0,0 +1,105 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx-landing + name: team2deployment + namespace: my-team2 +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: nginx-landing + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + creationTimestamp: null + labels: + app: nginx-landing + spec: + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + name: server + ports: + - containerPort: 80 + name: http + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /usr/share/nginx/html/ + name: index + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + name: team2-landing-page + name: index +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: team2-landing-page + namespace: my-team2 +data: + index.html: |- + +

landing

+ + + + + + + + + + + + + + + + + + + + + +
NameRole
JoEng Manager
JaneLead
BobDeveloper
+ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: team2config + namespace: my-team2 +data: + ports: 80(TCP), + pods: "1" \ No newline at end of file diff --git a/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/composition2.yaml b/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/composition2.yaml new file mode 100644 index 0000000..79ec699 --- /dev/null +++ b/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/composition2.yaml @@ -0,0 +1,38 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: secondprojectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: second-{{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/input.yaml b/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/input.yaml new file mode 100644 index 0000000..2c314c4 --- /dev/null +++ b/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/input.yaml @@ -0,0 +1,102 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/output.yaml b/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/output.yaml new file mode 100644 index 0000000..64da44d --- /dev/null +++ b/composition/tests/data/TestMultipleCompositionsDisallowedForSameGVK/output.yaml @@ -0,0 +1,63 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: "" + applyset.kubernetes.io/contains-group-kinds: ConfigMap + applyset.kubernetes.io/tooling: Plan/ + name: pconfigs-team-a-config + namespace: team-a + ownerReferences: + - apiVersion: facade.foocorp.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: PConfig + name: team-a-config +spec: + stages: + project: + manifest: |2 + + apiVersion: v1 + kind: ConfigMap + metadata: + name: proj-a + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleAddFacadeField/input.yaml b/composition/tests/data/TestSimpleAddFacadeField/input.yaml new file mode 100644 index 0000000..e2db573 --- /dev/null +++ b/composition/tests/data/TestSimpleAddFacadeField/input.yaml @@ -0,0 +1,172 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + projects: + items: + type: string + type: array + required: + - projects + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% for project in pconfigs.spec.projects %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ project }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ project }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + {% endfor %} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + projects: + - proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleAddFacadeField/output.yaml b/composition/tests/data/TestSimpleAddFacadeField/output.yaml new file mode 100644 index 0000000..14043fd --- /dev/null +++ b/composition/tests/data/TestSimpleAddFacadeField/output.yaml @@ -0,0 +1,64 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: "" + applyset.kubernetes.io/contains-group-kinds: ConfigMap + applyset.kubernetes.io/tooling: Plan/ + name: pconfigs-team-a-config + namespace: team-a + ownerReferences: + - apiVersion: facade.foocorp.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: PConfig + name: team-a-config +spec: + stages: + project: + manifest: |2 + + + apiVersion: v1 + kind: ConfigMap + metadata: + name: proj-a + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionExpanderLoggingEnabled/input.yaml b/composition/tests/data/TestSimpleCompositionExpanderLoggingEnabled/input.yaml new file mode 100644 index 0000000..1dfcc6d --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionExpanderLoggingEnabled/input.yaml @@ -0,0 +1,161 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: projectvalues + namespace: default +spec: + valuesFrom: + - name: commondata + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + name: common-config + fieldRef: + - path: ".data" + as: data +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a + annotations: + composition-expander-debug-logs: "true" +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionExpanderLoggingEnabled/output.yaml b/composition/tests/data/TestSimpleCompositionExpanderLoggingEnabled/output.yaml new file mode 100644 index 0000000..bb1f931 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionExpanderLoggingEnabled/output.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/fixed_composition.yaml b/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/fixed_composition.yaml new file mode 100644 index 0000000..0a8977c --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/fixed_composition.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: + external: "010101-ABABCD-BCAB11" + folderRef: + external: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/input.yaml b/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/input.yaml new file mode 100644 index 0000000..5f474e5 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/input.yaml @@ -0,0 +1,88 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + # ERROR: jinja error, % in a variable eval block is an error: {{%}} + name: {{% managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + # ERROR: jinja error, {% fooboo %} is an unknown jinja command + {% fooboo %} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: + external: "010101-ABABCD-BCAB11" + folderRef: + external: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/output.yaml b/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/output.yaml new file mode 100644 index 0000000..fc042a9 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionJinjaValidationFailure/output.yaml @@ -0,0 +1,14 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/facade_crd.yaml b/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/facade_crd.yaml new file mode 100644 index 0000000..bb8d701 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/facade_crd.yaml @@ -0,0 +1,57 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/input.yaml b/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/input.yaml new file mode 100644 index 0000000..3fb37db --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/input.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: + external: "010101-ABABCD-BCAB11" + folderRef: + external: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/output.yaml b/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/output.yaml new file mode 100644 index 0000000..fc042a9 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionStatusFacadeCRDMissing/output.yaml @@ -0,0 +1,14 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/composition/tests/data/TestSimpleCompositionStatusValidation/fixed_composition.yaml b/composition/tests/data/TestSimpleCompositionStatusValidation/fixed_composition.yaml new file mode 100644 index 0000000..0a8977c --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionStatusValidation/fixed_composition.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: + external: "010101-ABABCD-BCAB11" + folderRef: + external: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionStatusValidation/input.yaml b/composition/tests/data/TestSimpleCompositionStatusValidation/input.yaml new file mode 100644 index 0000000..5f49420 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionStatusValidation/input.yaml @@ -0,0 +1,85 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + # Error missing 'name: something'. Would result in validation failure + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: + external: "010101-ABABCD-BCAB11" + folderRef: + external: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionStatusValidation/output.yaml b/composition/tests/data/TestSimpleCompositionStatusValidation/output.yaml new file mode 100644 index 0000000..fc042a9 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionStatusValidation/output.yaml @@ -0,0 +1,14 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/composition/tests/data/TestSimpleCompositionUpdate/input.yaml b/composition/tests/data/TestSimpleCompositionUpdate/input.yaml new file mode 100644 index 0000000..c15c88c --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionUpdate/input.yaml @@ -0,0 +1,259 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.updatetest.com +spec: + group: facade.updatetest.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + projects: + items: + type: string + type: array + required: + - projects + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: sconfigs.facade.updatetest.com +spec: + group: facade.updatetest.com + names: + kind: SConfig + listKind: SConfigList + plural: sconfigs + singular: sconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the sconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: SConfig spec + properties: + projects: + items: + type: string + type: array + required: + - projects + type: object + status: + description: SConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-updatetest +rules: +- apiGroups: + - facade.updatetest.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.updatetest.com + resources: + - "*/status" + verbs: + - get + - update +- apiGroups: [""] + resources: + - configmaps + verbs: + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-updatetest +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-updatetest +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: pprojectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.updatetest.com + expanders: + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% for project in pconfigs.spec.projects %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: p-{{ project }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ project }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + {% endfor %} +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: sprojectconfigmap + namespace: default +spec: + inputAPIGroup: sconfigs.facade.updatetest.com + expanders: + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% for project in sconfigs.spec.projects %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: s-{{ project }} + namespace: {{ sconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ project }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + {% endfor %} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.updatetest.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + projects: + - proj-a + - proj-b +--- +apiVersion: facade.updatetest.com/v1alpha1 +kind: SConfig +metadata: + name: team-a-config + namespace: team-a +spec: + projects: + - proj-a + - proj-b \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionUpdate/modified_composition.yaml b/composition/tests/data/TestSimpleCompositionUpdate/modified_composition.yaml new file mode 100644 index 0000000..7f819dd --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionUpdate/modified_composition.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: pprojectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.updatetest.com + expanders: + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% for project in pconfigs.spec.projects %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: p-{{ project }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ project }}-updated + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + {% endfor %} \ No newline at end of file diff --git a/composition/tests/data/TestSimpleCompositionUpdate/output.yaml b/composition/tests/data/TestSimpleCompositionUpdate/output.yaml new file mode 100644 index 0000000..87933f2 --- /dev/null +++ b/composition/tests/data/TestSimpleCompositionUpdate/output.yaml @@ -0,0 +1,85 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a-updated +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: p-proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-b-updated +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: p-proj-b + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: s-proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: sconfigs-team-a-config +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-b +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: s-proj-b + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: sconfigs-team-a-config \ No newline at end of file diff --git a/composition/tests/data/TestSimpleDeleteFacadeField/input.yaml b/composition/tests/data/TestSimpleDeleteFacadeField/input.yaml new file mode 100644 index 0000000..a09870f --- /dev/null +++ b/composition/tests/data/TestSimpleDeleteFacadeField/input.yaml @@ -0,0 +1,173 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + projects: + items: + type: string + type: array + required: + - projects + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% for project in pconfigs.spec.projects %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ project }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ project }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + --- + {% endfor %} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + projects: + - proj-a + - proj-b \ No newline at end of file diff --git a/composition/tests/data/TestSimpleDeleteFacadeField/output.yaml b/composition/tests/data/TestSimpleDeleteFacadeField/output.yaml new file mode 100644 index 0000000..fcea0b9 --- /dev/null +++ b/composition/tests/data/TestSimpleDeleteFacadeField/output.yaml @@ -0,0 +1,82 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-b +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-b + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: "" + applyset.kubernetes.io/contains-group-kinds: ConfigMap + applyset.kubernetes.io/tooling: Plan/ + name: pconfigs-team-a-config + namespace: team-a + ownerReferences: + - apiVersion: facade.foocorp.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: PConfig + name: team-a-config +spec: + stages: + project: + manifest: |2 + + + apiVersion: v1 + kind: ConfigMap + metadata: + name: proj-a + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleExpanderInvalid/input.yaml b/composition/tests/data/TestSimpleExpanderInvalid/input.yaml new file mode 100644 index 0000000..d319fa3 --- /dev/null +++ b/composition/tests/data/TestSimpleExpanderInvalid/input.yaml @@ -0,0 +1,134 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: fakeExpander + version: v1.10.2 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: fakeExpander + version: v1.2.3 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" diff --git a/composition/tests/data/TestSimpleExpanderInvalid/output.yaml b/composition/tests/data/TestSimpleExpanderInvalid/output.yaml new file mode 100644 index 0000000..633bb7f --- /dev/null +++ b/composition/tests/data/TestSimpleExpanderInvalid/output.yaml @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/composition/tests/data/TestSimpleExpanderJinjaWithQuotes/input.yaml b/composition/tests/data/TestSimpleExpanderJinjaWithQuotes/input.yaml new file mode 100644 index 0000000..4aacaf8 --- /dev/null +++ b/composition/tests/data/TestSimpleExpanderJinjaWithQuotes/input.yaml @@ -0,0 +1,189 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + # Using single and double quotes in jinja for loop + # had issue when we used json.Marshall(): + # - double quotes were escaped + # - > was replaced with unicode string + # so removed json.Marshall(template) + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + {% set services=[ "cloudkms.googleapis.com", 'iam.googleapis.com', "serviceusage.googleapis.com", 'sqladmin.googleapis.com' ] %} + {% for service in services %} + key{{loop.index}}: {{service}} + {% endfor %} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleExpanderJinjaWithQuotes/output.yaml b/composition/tests/data/TestSimpleExpanderJinjaWithQuotes/output.yaml new file mode 100644 index 0000000..10a3982 --- /dev/null +++ b/composition/tests/data/TestSimpleExpanderJinjaWithQuotes/output.yaml @@ -0,0 +1,44 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: cloudkms.googleapis.com + key2: iam.googleapis.com + key3: serviceusage.googleapis.com + key4: sqladmin.googleapis.com \ No newline at end of file diff --git a/composition/tests/data/TestSimpleExpanderVersionInvalid/input.yaml b/composition/tests/data/TestSimpleExpanderVersionInvalid/input.yaml new file mode 100644 index 0000000..f463915 --- /dev/null +++ b/composition/tests/data/TestSimpleExpanderVersionInvalid/input.yaml @@ -0,0 +1,134 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v1.10.2 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + version: v1.2.3 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" diff --git a/composition/tests/data/TestSimpleExpanderVersionInvalid/output.yaml b/composition/tests/data/TestSimpleExpanderVersionInvalid/output.yaml new file mode 100644 index 0000000..633bb7f --- /dev/null +++ b/composition/tests/data/TestSimpleExpanderVersionInvalid/output.yaml @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/composition/tests/data/TestSimpleExpansionGrpc/input.yaml b/composition/tests/data/TestSimpleExpansionGrpc/input.yaml new file mode 100644 index 0000000..f07b683 --- /dev/null +++ b/composition/tests/data/TestSimpleExpansionGrpc/input.yaml @@ -0,0 +1,167 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleExpansionGrpc/output.yaml b/composition/tests/data/TestSimpleExpansionGrpc/output.yaml new file mode 100644 index 0000000..14043fd --- /dev/null +++ b/composition/tests/data/TestSimpleExpansionGrpc/output.yaml @@ -0,0 +1,64 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: "" + applyset.kubernetes.io/contains-group-kinds: ConfigMap + applyset.kubernetes.io/tooling: Plan/ + name: pconfigs-team-a-config + namespace: team-a + ownerReferences: + - apiVersion: facade.foocorp.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: PConfig + name: team-a-config +spec: + stages: + project: + manifest: |2 + + + apiVersion: v1 + kind: ConfigMap + metadata: + name: proj-a + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleExpansionJob/input.yaml b/composition/tests/data/TestSimpleExpansionJob/input.yaml new file mode 100644 index 0000000..98078b6 --- /dev/null +++ b/composition/tests/data/TestSimpleExpansionJob/input.yaml @@ -0,0 +1,168 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2-job + name: project + version: v0.0.1 + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleExpansionJob/output.yaml b/composition/tests/data/TestSimpleExpansionJob/output.yaml new file mode 100644 index 0000000..14043fd --- /dev/null +++ b/composition/tests/data/TestSimpleExpansionJob/output.yaml @@ -0,0 +1,64 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: composition.google.com/v1alpha1 +kind: Plan +metadata: + annotations: + applyset.kubernetes.io/additional-namespaces: "" + applyset.kubernetes.io/contains-group-kinds: ConfigMap + applyset.kubernetes.io/tooling: Plan/ + name: pconfigs-team-a-config + namespace: team-a + ownerReferences: + - apiVersion: facade.foocorp.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: PConfig + name: team-a-config +spec: + stages: + project: + manifest: |2 + + + apiVersion: v1 + kind: ConfigMap + metadata: + name: proj-a + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" + data: + name: proj-a + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimpleFacadeByoSchema/in_pconfig.yaml b/composition/tests/data/TestSimpleFacadeByoSchema/in_pconfig.yaml new file mode 100644 index 0000000..b07f273 --- /dev/null +++ b/composition/tests/data/TestSimpleFacadeByoSchema/in_pconfig.yaml @@ -0,0 +1,21 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: facade.compositions.google.com/v1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleFacadeByoSchema/input.yaml b/composition/tests/data/TestSimpleFacadeByoSchema/input.yaml new file mode 100644 index 0000000..98bbcb5 --- /dev/null +++ b/composition/tests/data/TestSimpleFacadeByoSchema/input.yaml @@ -0,0 +1,146 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade +rules: +- apiGroups: + - facade.compositions.google.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.compositions.google.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Facade +metadata: + name: projectconfigmap + namespace: default +spec: + facadeKind: PConfig + openAPIV3Schema: + # Schema for the `spec` field + type: object + properties: + project: + type: string + required: + - project +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.compositions.google.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleFacadeByoSchema/out_crd_pconfigs.yaml b/composition/tests/data/TestSimpleFacadeByoSchema/out_crd_pconfigs.yaml new file mode 100644 index 0000000..4a2b298 --- /dev/null +++ b/composition/tests/data/TestSimpleFacadeByoSchema/out_crd_pconfigs.yaml @@ -0,0 +1,112 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + compositions.google.com/facade: "yes" + name: pconfigs.facade.compositions.google.com +spec: + conversion: + strategy: None + group: facade.compositions.google.com + names: + categories: + - facade + - facades + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: TODO + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + project: + type: string + required: + - project + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + description: human readable message + maxLength: 1024 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 256 + 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. + 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 + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + categories: + - facade + - facades + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + storedVersions: + - v1 \ No newline at end of file diff --git a/composition/tests/data/TestSimpleFacadeByoSchema/output.yaml b/composition/tests/data/TestSimpleFacadeByoSchema/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestSimpleFacadeByoSchema/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimpleNamespaceExplicit/input.yaml b/composition/tests/data/TestSimpleNamespaceExplicit/input.yaml new file mode 100644 index 0000000..4ee6cca --- /dev/null +++ b/composition/tests/data/TestSimpleNamespaceExplicit/input.yaml @@ -0,0 +1,205 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - create + - delete + - get + - list + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: ["*"] + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + namespaceMode: explicit + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + - type: jinja2 + name: clusterscope + template: | + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: foobar-foocorp + rules: + - apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleNamespaceExplicit/output.yaml b/composition/tests/data/TestSimpleNamespaceExplicit/output.yaml new file mode 100644 index 0000000..15dd9b3 --- /dev/null +++ b/composition/tests/data/TestSimpleNamespaceExplicit/output.yaml @@ -0,0 +1,54 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: foobar-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update \ No newline at end of file diff --git a/composition/tests/data/TestSimpleNamespaceInherit/input.yaml b/composition/tests/data/TestSimpleNamespaceInherit/input.yaml new file mode 100644 index 0000000..daf9cd0 --- /dev/null +++ b/composition/tests/data/TestSimpleNamespaceInherit/input.yaml @@ -0,0 +1,194 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + namespaceMode: inherit + expanders: + - type: jinja2 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" + - type: jinja2 + version: v0.0.1 + name: clusterscope + template: | + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: {{ managedProject }} + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: foobar-foocorp + subjects: + - kind: ServiceAccount + name: foobar + namespace: foobar-system +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimpleNamespaceInherit/output.yaml b/composition/tests/data/TestSimpleNamespaceInherit/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestSimpleNamespaceInherit/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/composition_fixed.yaml b/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/composition_fixed.yaml new file mode 100644 index 0000000..ec87fb0 --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/composition_fixed.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/input.yaml b/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/input.yaml new file mode 100644 index 0000000..56f0b1a --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/input.yaml @@ -0,0 +1,182 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + # This will cause a jinja expansion failure + key: {{ unknown.spec.doesNotExist }} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/output.yaml b/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorExpansionFailed/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/composition_fixed.yaml b/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/composition_fixed.yaml new file mode 100644 index 0000000..ec87fb0 --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/composition_fixed.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/input.yaml b/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/input.yaml new file mode 100644 index 0000000..f54aaac --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/input.yaml @@ -0,0 +1,181 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: MonfigCap ## should fail + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/output.yaml b/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorFailedApplyingManifests/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/composition_fixed.yaml b/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/composition_fixed.yaml new file mode 100644 index 0000000..ec87fb0 --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/composition_fixed.yaml @@ -0,0 +1,53 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key: {{ pconfigs.spec.project }} + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/input.yaml b/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/input.yaml new file mode 100644 index 0000000..b4aaf31 --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/input.yaml @@ -0,0 +1,179 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + --- + stuff: 1 + --- # Make sure termination happen with inline comment + stuff: 2 + --- + stuff: 3 + --- Make sure uncommented content results YAMLSyntaxError + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/output.yaml b/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusWaitingForValues/configmap_with_data.yaml b/composition/tests/data/TestSimplePlanStatusWaitingForValues/configmap_with_data.yaml new file mode 100644 index 0000000..9624375 --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusWaitingForValues/configmap_with_data.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusWaitingForValues/input.yaml b/composition/tests/data/TestSimplePlanStatusWaitingForValues/input.yaml new file mode 100644 index 0000000..49de25e --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusWaitingForValues/input.yaml @@ -0,0 +1,205 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: system:serviceaccount:composition-system:composition-controller-manager + namespace: team-a +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:serviceaccount:composition-system:composition-controller-manager +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: projectvalues + namespace: default +spec: + valuesFrom: + - name: commondata + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + name: common-config + fieldRef: + - path: ".data" + as: data +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + # No data + - type: getter + version: v0.0.1 + name: get-project + template: "" + configref: + name: projectvalues + namespace: default + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestSimplePlanStatusWaitingForValues/output.yaml b/composition/tests/data/TestSimplePlanStatusWaitingForValues/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestSimplePlanStatusWaitingForValues/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestUpdateCompositionCRAddStage/input.yaml b/composition/tests/data/TestUpdateCompositionCRAddStage/input.yaml new file mode 100644 index 0000000..8c7dbbc --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionCRAddStage/input.yaml @@ -0,0 +1,156 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: projectvalues + namespace: default +spec: + valuesFrom: + - name: commondata + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + name: common-config + fieldRef: + - path: ".data" + as: data +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestUpdateCompositionCRAddStage/output.yaml b/composition/tests/data/TestUpdateCompositionCRAddStage/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionCRAddStage/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestUpdateCompositionCRAddStage/updated_composition.yaml b/composition/tests/data/TestUpdateCompositionCRAddStage/updated_composition.yaml new file mode 100644 index 0000000..b7811b3 --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionCRAddStage/updated_composition.yaml @@ -0,0 +1,59 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 + - type: getter + version: v0.0.1 + name: get-project + template: "" + configref: + name: projectvalues + namespace: default + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" diff --git a/composition/tests/data/TestUpdateCompositionCRRemoveStage/input.yaml b/composition/tests/data/TestUpdateCompositionCRRemoveStage/input.yaml new file mode 100644 index 0000000..8359a6d --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionCRRemoveStage/input.yaml @@ -0,0 +1,180 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: projectvalues + namespace: default +spec: + valuesFrom: + - name: commondata + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + name: common-config + fieldRef: + - path: ".data" + as: data +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 + - type: getter + version: v0.0.1 + name: get-project + template: "" + configref: + name: projectvalues + namespace: default + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a \ No newline at end of file diff --git a/composition/tests/data/TestUpdateCompositionCRRemoveStage/output.yaml b/composition/tests/data/TestUpdateCompositionCRRemoveStage/output.yaml new file mode 100644 index 0000000..4f85b6b --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionCRRemoveStage/output.yaml @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: v1 +data: + billingAccountRef: 010101-ABABCD-BCAB11 + folderRef: "000000111100" + name: proj-a +kind: ConfigMap +metadata: + labels: + createdby: composition-namespaceconfigmap + name: proj-a + namespace: team-a + ownerReferences: + - apiVersion: composition.google.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Plan + name: pconfigs-team-a-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: team-a + labels: + createdby: "composition-namespaceconfigmap" +data: + key1: value1 \ No newline at end of file diff --git a/composition/tests/data/TestUpdateCompositionCRRemoveStage/updated_composition.yaml b/composition/tests/data/TestUpdateCompositionCRRemoveStage/updated_composition.yaml new file mode 100644 index 0000000..4cad791 --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionCRRemoveStage/updated_composition.yaml @@ -0,0 +1,35 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 diff --git a/composition/tests/data/TestUpdateCompositionModifyStage/input.yaml b/composition/tests/data/TestUpdateCompositionModifyStage/input.yaml new file mode 100644 index 0000000..04acedd --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionModifyStage/input.yaml @@ -0,0 +1,180 @@ +# Copyright 2024 Google LLC +# +# 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. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pconfigs.facade.foocorp.com +spec: + group: facade.foocorp.com + names: + kind: PConfig + listKind: PConfigList + plural: pconfigs + singular: pconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Schema for the pconfig + properties: + apiVersion: + description: api-version of api + type: string + kind: + description: gvk Kind + type: string + metadata: + type: object + spec: + description: PConfig spec + properties: + project: + type: string + required: + - project + type: object + status: + description: PConfig status + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-foocorp +rules: +- apiGroups: + - facade.foocorp.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.foocorp.com + resources: + - "*/status" + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-foocorp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-foocorp +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: projectvalues + namespace: default +spec: + valuesFrom: + - name: commondata + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + name: common-config + fieldRef: + - path: ".data" + as: data +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 + - type: getter + version: v0.0.1 + name: get-project + template: "" + configref: + name: projectvalues + namespace: default + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: team-a +--- +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: team-a +spec: + project: proj-a +--- +apiVersion: facade.foocorp.com/v1alpha1 +kind: PConfig +metadata: + name: team-a-config + namespace: team-a +spec: + project: proj-a diff --git a/composition/tests/data/TestUpdateCompositionModifyStage/output.yaml b/composition/tests/data/TestUpdateCompositionModifyStage/output.yaml new file mode 100644 index 0000000..fc042a9 --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionModifyStage/output.yaml @@ -0,0 +1,14 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/composition/tests/data/TestUpdateCompositionModifyStage/updated_composition.yaml b/composition/tests/data/TestUpdateCompositionModifyStage/updated_composition.yaml new file mode 100644 index 0000000..cdb98fa --- /dev/null +++ b/composition/tests/data/TestUpdateCompositionModifyStage/updated_composition.yaml @@ -0,0 +1,59 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: projectconfigmap + namespace: default +spec: + inputAPIGroup: pconfigs.facade.foocorp.com + expanders: + - type: jinja2 + version: v0.0.1 + name: common + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: common-config-2 + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + key1: value1 + - type: getter + version: v0.0.1 + name: get-project + template: "" + configref: + name: projectvalues + namespace: default + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set hostProject = 'compositions-foobar' %} + {% set managedProject = pconfigs.spec.project %} + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ managedProject }} + namespace: {{ pconfigs.metadata.namespace }} + labels: + createdby: "composition-namespaceconfigmap" + data: + name: {{ managedProject }} + billingAccountRef: "010101-ABABCD-BCAB11" + folderRef: "000000111100" diff --git a/composition/tests/flags.go b/composition/tests/flags.go new file mode 100644 index 0000000..4fe2227 --- /dev/null +++ b/composition/tests/flags.go @@ -0,0 +1,50 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "flag" +) + +// Constants used in the file +const ( + Local = "local" + None = "none" + Remote = "remote" +) + +// E2E enables running end-to-end tests. +var E2E = flag.Bool("e2e", true, "if true, run end-to-end tests.") + +// KubernetesVersion is the version of Kubernetes to test against. Only has effect +// when testing against test-created Kind clusters. +var KubernetesVersion = flag.String("kubernetes-version", "1.23", + "The version of Kubernetes to create") + +// Operator - Where is the reconciler for the KRM objects running +var Operator = flag.String("operator", "none", "One of [local,none,oncluster].\n"+ + "`local` implies operator is started locally by test code\n"+ + "`none` implies the user has started the operator already\n"+ + "`remote` implies the test installs the operator manifests using .\n"+ + "In all cases, --kubeconfig is used to connect to admin-cluster.") + +// Image - if a custom image is to be used +var Image = flag.String("image", "", " to be used for the operator container") + +// Kubeconfig - if a local kubeconfig is to be used +var Kubeconfig = flag.String("kube-config", "", "location of kubeconfig file") + +// Debug - for logging +var Debug = flag.Bool("debug", false, "If true, enables verbose logging") diff --git a/composition/tests/scenario/scenario.go b/composition/tests/scenario/scenario.go new file mode 100644 index 0000000..8490522 --- /dev/null +++ b/composition/tests/scenario/scenario.go @@ -0,0 +1,372 @@ +// Copyright 2024 Google LLC +// +// 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 scenario + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cloud-native-compositions/compositions/composition/tests/cluster" + "github.com/cloud-native-compositions/compositions/composition/tests/testclient" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/rest" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" +) + +const ( + // ExistTimeout - time in which object should exist after creation; + ExistTimeout = 1 * time.Minute + // InstantTimeout - time used to check properties that should already apply. + // It isn't 0 to give checks time to run. + InstantTimeout = 2 * time.Second + // ReadyTimeout - time after creation that object should be available; + ReadyTimeout = 4 * time.Minute + // Composition Reconcile - bound on time + CompositionReconcileTimeout = 4 * time.Minute + // DeleteTimeout time to delete + DeleteTimeout = 2 * time.Minute + // OutputExistsTimeout - time in which output objects exist after reconcile + OutputExistsTimeout = CompositionReconcileTimeout +) + +// Scenario - context for each scenario +type Scenario struct { + T *testing.T + C *testclient.Client + + // Cluster + cluster cluster.ClusterUser + config *rest.Config + ctx context.Context + + // Scenario + noTestData bool + name string + dataFolder string + inputObjects []*unstructured.Unstructured + outputObjects []*unstructured.Unstructured + manifestObjects map[string][]*unstructured.Unstructured +} + +// NewBasic - return a Scenario object for Basic testcases +// - Use ../../tests/data/Test for data +// - Expect input.yaml, output.yaml (optional) +// - Dont need KCC enabled for tests +func NewBasic(t *testing.T) *Scenario { + // Sub-tests will include "/" in their names, which are not allowed in + // metadata.name. + name := strings.ReplaceAll(t.Name(), "/", "-") + dataFolder := "../../tests/data/" + t.Name() + // TODO (barney-s) parameterize or make it global + logRoot := "../../" + + t.Logf("------------------------------------------------------------------") + t.Logf("%s", name) + t.Logf("------------------------------------------------------------------") + time.Sleep(2 * time.Second) + + ctx := context.Background() + clusterUser := cluster.ReserveCluster(t) + config := clusterUser.Config() + + testClient := testclient.New(ctx, t, config, clusterUser.Name(), name, logRoot) + + s := &Scenario{ + T: t, + C: testClient, + cluster: clusterUser, + name: name, + ctx: ctx, + config: config, + dataFolder: dataFolder, + noTestData: false, + manifestObjects: make(map[string][]*unstructured.Unstructured), + } + + s.inputObjects = s.loadObjects(s.inputData(), "input") + s.outputObjects = s.loadObjects(s.outputData(), "output") + return s +} + +type Sample struct { + Name string + Composition string +} + +// NewFromSample - return a Scenario object for testing Samples/ +func NewFromSample(t *testing.T, sample Sample, dependentSamples []Sample, hasCloudRsrc bool) *Scenario { + // Sub-tests will include "/" in their names, which are not allowed in + // metadata.name. + name := strings.ReplaceAll(t.Name(), "/", "-") + dataFolder := "../../../samples/" + sample.Name + logRoot := "../../" + + ctx := context.Background() + clusterUser := cluster.ReserveCluster(t) + if hasCloudRsrc && !clusterUser.KCCInstalled() { + cluster.ReleaseCluster(t, clusterUser) + t.Logf("This sample has Cloud Resources. KCC is not installed in cluster, skipping test") + t.SkipNow() + } + config := clusterUser.Config() + + testClient := testclient.New(ctx, t, config, clusterUser.Name(), name, logRoot) + + s := &Scenario{ + T: t, + C: testClient, + cluster: clusterUser, + name: name, + ctx: ctx, + config: config, + dataFolder: dataFolder, + noTestData: false, + manifestObjects: make(map[string][]*unstructured.Unstructured), + } + + s.inputObjects = []*unstructured.Unstructured{} + for _, dependent := range dependentSamples { + folder := fmt.Sprintf("../../../samples/%s", dependent.Name) + filePath := filepath.Join(folder, "composition", dependent.Composition) + s.inputObjects = append(s.inputObjects, s.loadObjects(s.dataFromPath(filePath), "composition:"+dependent.Name)...) + } + s.inputObjects = append(s.inputObjects, s.loadObjects(s.testData("composition", sample.Composition), "composition")...) + s.outputObjects = []*unstructured.Unstructured{} + return s +} + +func (s *Scenario) Cleanup() { + defer cluster.ReleaseCluster(s.T, s.cluster) + defer s.GatherLogs() + s.CleanupInput() + s.CleanupIntermediateManifests() + s.CleanupOutput() +} + +func (s *Scenario) Setup() { + s.ApplyInput() + s.VerifyInput() +} + +func (s *Scenario) dataFromPath(filePath string) string { + data, err := os.ReadFile(filePath) + if err != nil { + s.T.Errorf("Failed reading input: %s", filePath) + s.T.FailNow() + } + return string(data) +} + +func (s *Scenario) testData(path ...string) string { + filePath := filepath.Join(append([]string{s.dataFolder}, path...)...) + return s.dataFromPath(filePath) +} + +func (s *Scenario) inputData() string { + return s.testData("input.yaml") +} + +func (s *Scenario) outputData() string { + return s.testData("output.yaml") +} + +func (s *Scenario) loadObjects(manifests string, name string) []*unstructured.Unstructured { + output := []*unstructured.Unstructured{} + if manifests == "" { + return output + } + objects, err := manifest.ParseObjects(s.ctx, manifests) + if err != nil { + s.T.Errorf("Failed parsing manifests: %s", name) + s.T.FailNow() + } + + // loop over objects and extract unstructured + for _, item := range objects.Items { + output = append(output, item.UnstructuredObject()) + } + return output +} + +func (s *Scenario) applyObjects(items []*unstructured.Unstructured, updateAllowed bool) { + // loop over objects and apply CRDs first + crds := false + for _, item := range items { + if item.GroupVersionKind().Kind == "CustomResourceDefinition" { + s.C.MustCreate(item, updateAllowed) + crds = true + } + } + if crds { + // hacky + time.Sleep(time.Second * 2) + } + + namespaces := false + // loop over objects and extract unstructured + for _, item := range items { + if item.GroupVersionKind().Kind == "Namespace" { + s.C.MustCreate(item, updateAllowed) + namespaces = true + } + } + if namespaces { + // hacky + time.Sleep(time.Second) + } + + // loop over objects and extract unstructured + for _, item := range items { + kind := item.GroupVersionKind().Kind + if kind != "CustomResourceDefinition" && kind != "Namespace" { + s.C.MustCreate(item, updateAllowed) + } + } +} + +func (s *Scenario) Apply(name string, objects []*unstructured.Unstructured) { + s.T.Logf("Applying objects: %s", name) + s.manifestObjects[name] = objects + s.applyObjects(s.manifestObjects[name], true) +} + +func (s *Scenario) ApplyManifests(name string, path ...string) { + if s.noTestData { + s.T.Errorf("Scenario configured with 'nodata'. But ApplyManifest(%s) called.", path) + s.T.FailNow() + return + } + + s.T.Logf("Loading manifests from: %s", name) + s.manifestObjects[name] = s.loadObjects(s.testData(path...), name) + + s.applyObjects(s.manifestObjects[name], true) +} + +func (s *Scenario) ApplyInput() { + if s.noTestData { + return + } + + s.T.Log("Applying input") + s.applyObjects(s.inputObjects, false) +} + +func (s *Scenario) VerifyInput() { + if s.noTestData { + return + } + s.T.Log("Verifying input") + s.C.MustExist(s.inputObjects, ExistTimeout) +} + +func (s *Scenario) Verify(name string, matchSpec bool, objects []*unstructured.Unstructured) { + s.manifestObjects[name] = objects + + if matchSpec { + s.T.Logf("Verifying objects spec matches: %s", name) + s.C.MustMatchSpec(s.manifestObjects[name], ExistTimeout) + } else { + s.T.Logf("Verifying objects exist: %s", name) + s.C.MustExist(s.manifestObjects[name], ExistTimeout) + } +} + +func (s *Scenario) VerifyManifests(name string, matchSpec bool, path ...string) { + if s.noTestData { + s.T.Errorf("Scenario configured with 'nodata'. But VerifyManifest(%s) called.", name) + s.T.FailNow() + return + } + + s.T.Logf("Loading manifests for: %s", name) + s.manifestObjects[name] = s.loadObjects(s.testData(path...), name) + + if matchSpec { + s.T.Logf("Verifying manifests spec matches: %s", name) + s.C.MustMatchSpec(s.manifestObjects[name], ExistTimeout) + } else { + s.T.Logf("Verifying manifests exist: %s", name) + s.C.MustExist(s.manifestObjects[name], ExistTimeout) + } +} + +func (s *Scenario) VerifyOutputExists() { + if s.noTestData { + return + } + s.T.Log("Verifying output") + s.C.MustExist(s.outputObjects, OutputExistsTimeout) +} + +func (s *Scenario) VerifyOutputSpecMatches() { + if s.noTestData { + return + } + s.T.Log("Verifying output spec matches") + s.C.MustMatchSpec(s.outputObjects, OutputExistsTimeout) +} + +func (s *Scenario) CleanupInput() { + if s.noTestData { + return + } + s.T.Log("Cleaning up input") + for _, item := range s.inputObjects { + s.C.MustDelete(item) + } + s.C.MustNotExist(s.inputObjects, DeleteTimeout) +} + +func (s *Scenario) CleanupOutput() { + if s.noTestData { + return + } + s.T.Log("Cleaning up output") + for _, item := range s.outputObjects { + s.C.MustDelete(item) + } + s.C.MustNotExist(s.outputObjects, DeleteTimeout) +} + +func (s *Scenario) CleanupIntermediateManifests() { + if s.noTestData { + return + } + for key := range s.manifestObjects { + s.T.Logf("Cleaning up objects: %s", key) + for _, item := range s.manifestObjects[key] { + s.C.MustDelete(item) + } + s.C.MustNotExist(s.manifestObjects[key], DeleteTimeout) + } +} + +// GetName - return name +func (s *Scenario) GetName() string { + return s.name +} + +// GatherLogs - grab the logs for the test case +func (s *Scenario) GatherLogs() { + s.C.ClearOldLogs() + s.C.WriteNamespacePodLogs("default") + s.C.WriteNamespacePodLogs("composition-system") +} diff --git a/composition/tests/testcases/kcc_samples_test.go b/composition/tests/testcases/kcc_samples_test.go new file mode 100644 index 0000000..9a9ac39 --- /dev/null +++ b/composition/tests/testcases/kcc_samples_test.go @@ -0,0 +1,214 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "fmt" + "strings" + "testing" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + + "github.com/cloud-native-compositions/compositions/composition/tests/scenario" + "github.com/cloud-native-compositions/compositions/composition/tests/testclient" + "github.com/cloud-native-compositions/compositions/composition/tests/utils" + "k8s.io/apimachinery/pkg/util/rand" +) + +func gvk(g, v, k string) schema.GroupVersionKind { + return schema.GroupVersionKind{Group: g, Version: v, Kind: k} +} + +// ------------------------ AppTeam -------------------------------------------- + +func getAppTeamObj(project string) []*unstructured.Unstructured { + return []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "facade.facade/v1alpha1", + "kind": "AppTeam", + "metadata": map[string]interface{}{ + "name": project, + "namespace": "config-control", + }, + "spec": map[string]interface{}{ + "project": project, + }, + }, + }, + } +} + +func getAppTeamOutputObjects(project string) []*unstructured.Unstructured { + gkvnns := []testclient.GVKNN{ + {GroupVersionKind: gvk("composition.google.com", "v1alpha1", "Context"), + NamespacedName: types.NamespacedName{Name: "context", Namespace: project}}, + {GroupVersionKind: gvk("iam.cnrm.cloud.google.com", "v1beta1", "IAMServiceAccount"), + NamespacedName: types.NamespacedName{Name: "kcc-" + project, Namespace: "config-control"}}, + {GroupVersionKind: gvk("iam.cnrm.cloud.google.com", "v1beta1", "IAMPartialPolicy"), + NamespacedName: types.NamespacedName{Name: project + "-sa-workload-identity-binding", Namespace: "config-control"}}, + {GroupVersionKind: gvk("iam.cnrm.cloud.google.com", "v1beta1", "IAMPartialPolicy"), + NamespacedName: types.NamespacedName{Name: "kcc-owners-permissions-" + project, Namespace: "config-control"}}, + {GroupVersionKind: gvk("storage.cnrm.cloud.google.com", "v1beta1", "StorageBucket"), + NamespacedName: types.NamespacedName{Name: "test-bucket-" + project, Namespace: project}}, + {GroupVersionKind: gvk("core.cnrm.cloud.google.com", "v1beta1", "ConfigConnectorContext"), + NamespacedName: types.NamespacedName{Name: "configconnectorcontext.core.cnrm.cloud.google.com", Namespace: project}}, + {GroupVersionKind: gvk("resourcemanager.cnrm.cloud.google.com", "v1beta1", "Project"), + NamespacedName: types.NamespacedName{Name: project, Namespace: "config-control"}}, + {GroupVersionKind: gvk("", "v1", "Namespace"), + NamespacedName: types.NamespacedName{Name: project}}, + } + + objs := []*unstructured.Unstructured{} + for _, gvknn := range gkvnns { + objs = append(objs, gvknn.MakeObject()) + } + + return objs +} + +func TestKCCSampleAppTeam(t *testing.T) { + //t.Parallel() + s := scenario.NewFromSample(t, scenario.Sample{Name: "AppTeam", Composition: "appteam.yaml"}, nil, true) + defer s.Cleanup() + s.Setup() + + // Apply clearing team facade + project := fmt.Sprintf("clearing-%s", strings.ToLower(rand.String(8))) + s.Apply("appteam-object", getAppTeamObj(project)) + + // Check plan object has no error + plan := utils.GetPlanObj("config-control", "appteams-clearing") + condition := utils.GetReadyCondition("ProcessedAllStages", "") + s.C.MustHaveCondition(plan, condition, 5*scenario.CompositionReconcileTimeout) + + // Since the Plan says it has processed all stages we should validate KCC resources. + s.Verify("kcc-objects", false, getAppTeamOutputObjects(project)) + + // Verify KCC object status ? +} + +// ------------------------ CloudSQL -------------------------------------------- + +func getCloudSqlObj(namespace, name string) []*unstructured.Unstructured { + return []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "facade.compositions.google.com/v1", + "kind": "CloudSQL", + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "regions": []string{"us-east1", "us-central1"}, + "name": "collateral-db", + }, + }, + }, + } +} + +func getCloudSQLOutputObjects(namespace, name string) []*unstructured.Unstructured { + gkvnns := []testclient.GVKNN{ + {GroupVersionKind: gvk("sql.cnrm.cloud.google.com", "v1beta1", "SQLInstance"), + NamespacedName: types.NamespacedName{Name: name + "-db-main", Namespace: namespace}}, + {GroupVersionKind: gvk("sql.cnrm.cloud.google.com", "v1beta1", "SQLInstance"), + NamespacedName: types.NamespacedName{Name: name + "-db-replica-us-central1", Namespace: namespace}}, + + {GroupVersionKind: gvk("kms.cnrm.cloud.google.com", "v1beta1", "KMSKeyRing"), + NamespacedName: types.NamespacedName{Name: "kmscryptokeyring-us-central1", Namespace: namespace}}, + {GroupVersionKind: gvk("kms.cnrm.cloud.google.com", "v1beta1", "KMSKeyRing"), + NamespacedName: types.NamespacedName{Name: "kmscryptokeyring-us-east1", Namespace: namespace}}, + {GroupVersionKind: gvk("kms.cnrm.cloud.google.com", "v1beta1", "KMSCryptoKey"), + NamespacedName: types.NamespacedName{Name: "kmscryptokey-enc-us-central1", Namespace: namespace}}, + {GroupVersionKind: gvk("kms.cnrm.cloud.google.com", "v1beta1", "KMSCryptoKey"), + NamespacedName: types.NamespacedName{Name: "kmscryptokey-enc-us-east1", Namespace: namespace}}, + + {GroupVersionKind: gvk("iam.cnrm.cloud.google.com", "v1beta1", "IAMPolicyMember"), + NamespacedName: types.NamespacedName{Name: "sql-kms-us-east1-policybinding", Namespace: namespace}}, + {GroupVersionKind: gvk("iam.cnrm.cloud.google.com", "v1beta1", "IAMPolicyMember"), + NamespacedName: types.NamespacedName{Name: "sql-kms-us-central1-policybinding", Namespace: namespace}}, + + {GroupVersionKind: gvk("serviceusage.cnrm.cloud.google.com", "v1beta1", "ServiceIdentity"), + NamespacedName: types.NamespacedName{Name: "sqladmin.googleapis.com", Namespace: namespace}}, + + {GroupVersionKind: gvk("serviceusage.cnrm.cloud.google.com", "v1beta1", "Service"), + NamespacedName: types.NamespacedName{Name: "cloudkms.googleapis.com", Namespace: namespace}}, + {GroupVersionKind: gvk("serviceusage.cnrm.cloud.google.com", "v1beta1", "Service"), + NamespacedName: types.NamespacedName{Name: "iam.googleapis.com", Namespace: namespace}}, + {GroupVersionKind: gvk("serviceusage.cnrm.cloud.google.com", "v1beta1", "Service"), + NamespacedName: types.NamespacedName{Name: "serviceusage.googleapis.com", Namespace: namespace}}, + {GroupVersionKind: gvk("serviceusage.cnrm.cloud.google.com", "v1beta1", "Service"), + NamespacedName: types.NamespacedName{Name: "sqladmin.googleapis.com", Namespace: namespace}}, + } + + objs := []*unstructured.Unstructured{} + for _, gvknn := range gkvnns { + objs = append(objs, gvknn.MakeObject()) + } + + return objs +} + +func TestKCCSampleCloudSQL(t *testing.T) { + //t.Parallel() + + // Create a project + s := scenario.NewFromSample(t, + scenario.Sample{Name: "CloudSQL", Composition: "hasql.yaml"}, + []scenario.Sample{ + {Name: "AppTeam", Composition: "appteam.yaml"}, + }, + true, + ) + defer s.Cleanup() + s.Setup() + + // TODO: better wait + time.Sleep(time.Second * 10) + + // ---- Setup App Team for cloudsql user -------- + // Apply clearing team facade + project := fmt.Sprintf("clearing-%s", strings.ToLower(rand.String(8))) + s.Apply("appteam-object", getAppTeamObj(project)) + + // Check plan object has no error + planName := fmt.Sprintf("appteams-%s", project) + plan := utils.GetPlanObj("config-control", planName) + condition := utils.GetReadyCondition("ProcessedAllStages", "") + s.C.MustHaveCondition(plan, condition, 5*scenario.CompositionReconcileTimeout) + + // Since the Plan says it has processed all stages we should validate KCC resources. + s.Verify("appteam-kcc-objects", false, getAppTeamOutputObjects(project)) + + // ---- Test CloudSQL ----------------------------- + // Apply cloudsql facade + namespace := project + name := "collateral" + s.Apply("appteam-object", getCloudSqlObj(namespace, name)) + + // Check plan object has no error + cloudsqlPlan := utils.GetPlanObj(namespace, "cloudsqls-collateral") + condition = utils.GetReadyCondition("ProcessedAllStages", "") + s.C.MustHaveCondition(cloudsqlPlan, condition, 5*scenario.CompositionReconcileTimeout) + + // Since the Plan says it has processed all stages we should validate KCC resources. + s.Verify("kcc-objects", false, getCloudSQLOutputObjects(namespace, name)) + // Verify KCC object status ? +} diff --git a/composition/tests/testcases/main_test.go b/composition/tests/testcases/main_test.go new file mode 100644 index 0000000..442e32a --- /dev/null +++ b/composition/tests/testcases/main_test.go @@ -0,0 +1,59 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "flag" + "log" + "os" + "testing" + + "github.com/cloud-native-compositions/compositions/composition/tests/cluster" + "github.com/cloud-native-compositions/compositions/composition/tests/cluster/kind" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + images *string = flag.String("images", "", "images") + useCC *bool = flag.Bool("use-cc", false, "use config controller cluster") +) + +// TestMain - umbrella test that runs all test cases +func TestMain(m *testing.M) { + flag.Parse() + + //rand.Seed(time.Now().UnixNano()) + logf.SetLogger(zap.New(zap.UseDevMode(true))) + + // sanity check + err := kind.VerifyKindIsInstalled() + if err != nil { + log.Fatalf("Kind not installed. Please install it from: https://kind.sigs.k8s.io/docs/user/quick-start/") + } + + clusterCount := 1 + if *useCC { + cluster.CreateCCClusters(clusterCount, *images) + } else { + cluster.CreateKindClusters(clusterCount, *images) + } + exitCode := m.Run() + + // TODO: + //cluster.RemoveKindClusters() + //cluster.RemoveCCClusters() + os.Exit(exitCode) +} diff --git a/composition/tests/testcases/simple_samples_test.go b/composition/tests/testcases/simple_samples_test.go new file mode 100644 index 0000000..b27ee4f --- /dev/null +++ b/composition/tests/testcases/simple_samples_test.go @@ -0,0 +1,118 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + + "github.com/cloud-native-compositions/compositions/composition/tests/scenario" + "github.com/cloud-native-compositions/compositions/composition/tests/testclient" + "github.com/cloud-native-compositions/compositions/composition/tests/utils" + "k8s.io/apimachinery/pkg/util/rand" +) + +// ------------------------ FirstComposition -------------------------------------------- + +func getNamespaceObj(ns string) []*unstructured.Unstructured { + return []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": ns, + }, + }, + }, + } +} + +func getTeamPageObj(team string) []*unstructured.Unstructured { + return []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "idp.mycompany.com/v1alpha1", + "kind": "TeamPage", + "metadata": map[string]interface{}{ + "name": team, + "namespace": team, + }, + // members: + "spec": map[string]interface{}{ + "members": []map[string]interface{}{ + { + "name": "Jo", + "role": "Eng Manager", + }, + { + "name": "Jane", + "role": "Lead", + }, + { + "name": "Bob", + "role": "Developer", + }, + }, + }, + }, + }, + } +} + +func getTeamPageOutputObjects(team string) []*unstructured.Unstructured { + gkvnns := []testclient.GVKNN{ + {GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + NamespacedName: types.NamespacedName{Name: "team-" + team, Namespace: team}}, + {GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, + NamespacedName: types.NamespacedName{Name: "team-" + team + "-landing", Namespace: team}}, + {GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, + NamespacedName: types.NamespacedName{Name: "team-" + team + "-page", Namespace: team}}, + } + + objs := []*unstructured.Unstructured{} + for _, gvknn := range gkvnns { + objs = append(objs, gvknn.MakeObject()) + } + + return objs +} + +func TestFirstCompositionTeamPage(t *testing.T) { + //t.Parallel() + s := scenario.NewFromSample(t, scenario.Sample{Name: "FirstComposition", Composition: "teampage.yaml"}, nil, false) + defer s.Cleanup() + s.Setup() + + team := fmt.Sprintf("team-%s", strings.ToLower(rand.String(8))) + // Create Namespace for team first + s.Apply("namespace", getNamespaceObj(team)) + + // Create team page facade + s.Apply("teampage-object", getTeamPageObj(team)) + + // Check plan object has no error + plan := utils.GetPlanObj(team, "teampages-"+team) + condition := utils.GetReadyCondition("ProcessedAllStages", "") + s.C.MustHaveCondition(plan, condition, 5*scenario.CompositionReconcileTimeout) + + // Since the Plan says it has processed all stages we should validate applied resources. + s.Verify("objects", false, getTeamPageOutputObjects(team)) +} diff --git a/composition/tests/testcases/simple_test.go b/composition/tests/testcases/simple_test.go new file mode 100644 index 0000000..dde3459 --- /dev/null +++ b/composition/tests/testcases/simple_test.go @@ -0,0 +1,533 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "context" + "testing" + "time" + + "github.com/cloud-native-compositions/compositions/composition/tests/scenario" + "github.com/cloud-native-compositions/compositions/composition/tests/utils" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func TestSimpleExpansionJob(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() +} + +func TestSimpleExpansionGrpc(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() +} + +func TestSimpleCompositionUpdate(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Make sure the pre-update CRs have been created to ensure the coming update triggers a new reconcile. + cmNames := []string{"p-proj-a", "p-proj-b", "s-proj-a", "s-proj-b"} + cms := make([]*unstructured.Unstructured, 0) + for _, cmName := range cmNames { + cms = append(cms, utils.GetConfigMapObj("team-a", cmName)) + } + s.C.MustExist(cms, scenario.ExistTimeout) + + // Apply the modified Composition + s.ApplyManifests("modified composition", "modified_composition.yaml") + + // Changing the composition should trigger the expander to re-reconcile all objects. + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() +} + +// Test adding config that results in additional expanded resources +func TestSimpleAddFacadeField(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + + // Update Facade CR to add a field/list entry + t.Log("Adding proj-b entry to Facade") + facade := utils.GetUnstructuredObj("facade.foocorp.com", "v1alpha1", "PConfig", "team-a", "team-a-config") + addProject := map[string]any{ + "op": "add", + "path": "/spec/projects/-", + "value": "proj-b", + } + s.C.MustJSONPatch(facade, addProject) + + // Check if additional ConfigMap is created + cm := utils.GetConfigMapObj("team-a", "proj-b") + s.C.MustExist([]*unstructured.Unstructured{cm}, scenario.CompositionReconcileTimeout) +} + +// Test removing config that results in removal of some expanded resource +func TestSimpleDeleteFacadeField(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + + // Update Facade CR to add a field/list entry + t.Log("Removing proj-b entry from Facade") + facade := utils.GetUnstructuredObj("facade.foocorp.com", "v1alpha1", "PConfig", "team-a", "team-a-config") + addProject := map[string]any{ + "op": "remove", + "path": "/spec/projects/1", + } + s.C.MustJSONPatch(facade, addProject) + + // Check if the second ConfigMap is removed + cm := utils.GetConfigMapObj("team-a", "proj-b") + s.C.MustNotExist([]*unstructured.Unstructured{cm}, scenario.DeleteTimeout) +} + +func TestSimpleCompositionStatusValidation(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a Validation failure status condition in compositions + composition := utils.GetCompositionObj("default", "projectconfigmap") + condition := utils.GetValidationFailedCondition("ExpanderValidationFailed", "") + s.C.MustHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) + + // Apply the fixed Composition + s.ApplyManifests("composition without validation error", "fixed_composition.yaml") + + // Check if Validation failure condition is cleared + composition = utils.GetCompositionObj("default", "projectconfigmap") + condition = utils.GetValidationFailedCondition("ExpanderValidationFailed", "") + s.C.MustNotHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) +} + +func TestSimpleCompositionJinjaValidationFailure(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is an Error capturing the expander config Validation + composition := utils.GetCompositionObj("default", "projectconfigmap") + condition := utils.GetErrorCondition("ValidationFailed", "") + s.C.MustHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) + + // Apply the fixed Composition + s.ApplyManifests("composition without validation error", "fixed_composition.yaml") + + // Check if Validation failure condition is cleared + composition = utils.GetCompositionObj("default", "projectconfigmap") + condition = utils.GetErrorCondition("ValidationFailed", "") + s.C.MustNotHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) +} + +func TestSimpleCompositionStatusFacadeCRDMissing(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a Error status condition in compositions + composition := utils.GetCompositionObj("default", "projectconfigmap") + condition := utils.GetErrorCondition("MissingFacadeCRD", "") + s.C.MustHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) + + // Apply the fixed Composition + s.ApplyManifests("Facade CRD", "facade_crd.yaml") + + // Check if Validation failure condition is cleared + composition = utils.GetCompositionObj("default", "projectconfigmap") + condition = utils.GetErrorCondition("MissingFacadeCRD", "") + s.C.MustNotHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) +} + +func TestSimplePlanStatusWaitingForValues(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a Waiting failure status condition in plan + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetWaitingCondition("EvaluateStatusWait", "") + s.C.MustHaveCondition(plan, condition, scenario.CompositionReconcileTimeout) + + // Apply Configmap with data present + s.ApplyManifests("Fixed Configmap", "configmap_with_data.yaml") + + // Check if Waiting failure condition is cleared + plan = utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition = utils.GetWaitingCondition("EvaluateStatusWait", "") + s.C.MustNotHaveCondition(plan, condition, 3*scenario.CompositionReconcileTimeout) + + // Verify the composition progresses after being unblocked + s.VerifyOutputExists() +} + +func TestSimplePlanStatusErrorExpansionFailed(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a Error failure status condition in plan + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetErrorCondition("EvaluateStatusFailed", "") + s.C.MustHaveCondition(plan, condition, scenario.CompositionReconcileTimeout) + + // Apply Configmap with data present + s.ApplyManifests("Composition without jinja error", "composition_fixed.yaml") + + // Check if Error failure condition is cleared + plan = utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition = utils.GetErrorCondition("EvaluateStatusFailed", "") + s.C.MustNotHaveCondition(plan, condition, 2*scenario.CompositionReconcileTimeout) + + // Verify the composition progresses after being unblocked + s.VerifyOutputExists() +} + +func TestSimpleExpanderJinjaWithQuotes(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a Error failure status condition in plan + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetErrorCondition("EvaluateStatusFailed", "") + s.C.MustNotHaveCondition(plan, condition, 2*scenario.CompositionReconcileTimeout) + + // Verify the composition progresses after being unblocked + s.VerifyOutputExists() +} + +func TestSimplePlanStatusErrorFailedLoadingManifestsFromPlan(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a Waiting failure status condition in plan + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetErrorCondition("FailedLoadingManifestsFromPlan", "") + s.C.MustHaveCondition(plan, condition, scenario.CompositionReconcileTimeout) + + // Apply Configmap with data present + s.ApplyManifests("Composition without yaml error", "composition_fixed.yaml") + + // Check if Waiting failure condition is cleared + plan = utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition = utils.GetErrorCondition("FailedLoadingManifestsFromPlan", "") + s.C.MustNotHaveCondition(plan, condition, 2*scenario.CompositionReconcileTimeout) + + // Verify the composition progresses after being unblocked + s.VerifyOutputExists() +} + +func TestSimplePlanStatusErrorFailedApplyingManifests(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify there is a failed apply failure status condition in plan + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetErrorCondition("FailedApplyingManifests", "") + s.C.MustHaveCondition(plan, condition, scenario.CompositionReconcileTimeout) + + // Apply Configmap with data present + s.ApplyManifests("Composition with correct object", "composition_fixed.yaml") + + // Check if failed apply failure condition is cleared + plan = utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition = utils.GetErrorCondition("FailedApplyingManifests", "") + s.C.MustNotHaveCondition(plan, condition, 2*scenario.CompositionReconcileTimeout) + + // Verify the composition progresses after being unblocked + s.VerifyOutputExists() +} + +func TestSimpleNamespaceInherit(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + // Verify presence of an error because one of the manifests is cluster scoped + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetErrorCondition("FailedApplyingManifests", "") + s.C.MustHaveCondition(plan, condition, scenario.CompositionReconcileTimeout) +} + +func TestSimpleNamespaceExplicit(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + // Verify absence of an error because explicit allows cluster scoped + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetErrorCondition("FailedApplyingManifests", "") + s.C.MustNotHaveCondition(plan, condition, 2*scenario.CompositionReconcileTimeout) +} + +// Test Bring Your OWN Schema +func TestSimpleFacadeByoSchema(t *testing.T) { + //t.Parallel() + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // TODO Add Ready condition which we should wait for + time.Sleep(time.Second * 10) + + facade := utils.GetFacadeObj("default", "projectconfigmap") + // Ensure no Error condition in Composition + condition := utils.GetErrorCondition("", "") + s.C.MustNotHaveCondition(facade, condition, 2*scenario.CompositionReconcileTimeout) + + // Verify CRD has been created and defn matches + s.VerifyManifests("facade pconfig crd", true, "out_crd_pconfigs.yaml") + + // Create a facade from the new CRD + s.ApplyManifests("facade cr", "in_pconfig.yaml") + + // Ensure no Error condition in Plan + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition = utils.GetErrorCondition("", "") + s.C.MustNotHaveCondition(plan, condition, 2*scenario.CompositionReconcileTimeout) + + // Verify the composition progresses after being unblocked + s.VerifyOutputExists() +} + +func TestSimpleExpanderInvalid(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + composition := utils.GetCompositionObj("default", "projectconfigmap") + condition := utils.GetErrorCondition("ValidationFailed", "") + s.C.MustHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) +} + +func TestSimpleExpanderVersionInvalid(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + composition := utils.GetCompositionObj("default", "projectconfigmap") + condition := utils.GetErrorCondition("ValidationFailed", "") + s.C.MustHaveCondition(composition, condition, scenario.CompositionReconcileTimeout) +} + +func TestSimpleCompositionExpanderLoggingEnabled(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + plan := utils.GetPlanObj("team-a", "pconfigs-team-a-config") + condition := utils.GetWaitingCondition("EvaluateStatusWait", "") + s.C.MustNotHaveCondition(plan, condition, 3*scenario.CompositionReconcileTimeout) + s.VerifyOutputExists() +} + +func TestUpdateCompositionCRAddStage(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Make sure the pre-update CRs have been created to ensure the coming update triggers a new reconcile. + cm := utils.GetConfigMapObj("team-a", "common-config") + s.C.MustExist([]*unstructured.Unstructured{cm}, scenario.ExistTimeout) + + // Apply the modified Composition + s.ApplyManifests("updated composition", "updated_composition.yaml") + + // Changing the composition should trigger the expander to re-reconcile all objects. + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() +} + +func TestUpdateCompositionCRRemoveStage(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify that all resources objects are created + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + // Apply the modified Composition + s.ApplyManifests("updated composition", "updated_composition.yaml") + + // Changing the composition should trigger the expander to re-reconcile all objects. + cm := utils.GetConfigMapObj("team-a", "proj-a") + s.C.MustNotExist([]*unstructured.Unstructured{cm}, scenario.ExistTimeout) +} + +func TestUpdateCompositionModifyStage(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Verify that all resources objects are created + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + // Apply the modified Composition + s.ApplyManifests("updated composition", "updated_composition.yaml") + + // Changing the composition should trigger the expander to re-reconcile all objects. + cm := utils.GetConfigMapObj("team-a", "common-config-2") + s.C.MustExist([]*unstructured.Unstructured{cm}, scenario.ExistTimeout) +} + +func TestCustomStatusCELRule(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Check plan object has no error + plan := utils.GetPlanObj("my-team", "teampages-landing") + condition := utils.GetReadyCondition("ProcessedAllStages", "") + s.C.MustHaveCondition(plan, condition, 5*scenario.CompositionReconcileTimeout) + + // Verify that all resources objects are created + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + // Verify plan has no errors +} + +func TestImplicitGetter(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + // Check plan object has no error + plan := utils.GetPlanObj("my-team2", "team2pages-landing") + condition := utils.GetReadyCondition("ProcessedAllStages", "") + s.C.MustHaveCondition(plan, condition, 5*scenario.CompositionReconcileTimeout) + + // Verify that all resources objects are created + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + // Verify plan has no errors +} + +func TestMultipleCompositionsDisallowedForSameGVK(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + s.VerifyOutputExists() + s.VerifyOutputSpecMatches() + + s.ApplyManifests("second composition", "composition2.yaml") + + // Verify the second composition has the expected error + cm := utils.GetCompositionObj("default", "secondprojectconfigmap") + condition := utils.GetErrorCondition("DuplicateForGVK", "") + s.C.MustHaveCondition(cm, condition, scenario.CompositionReconcileTimeout) +} + +// Ensures generated resources are deleted when the CR is deleted. +// Verifies the deletion occurs in reverse order by stage. +func TestDeleteFacade(t *testing.T) { + s := scenario.NewBasic(t) + defer s.Cleanup() + s.Setup() + + s.VerifyOutputExists() + + cms := []*unstructured.Unstructured{} + cms = append(cms, utils.GetConfigMapObj("team-a-ns", "d-proj-a")) + cms = append(cms, utils.GetConfigMapObj("team-a-ns", "d-proj-b")) + // Add a finalizer onto a generated resource to prevent it from being deleted to simulate a resource + // that takes a long time to delete. + controllerutil.AddFinalizer(cms[1], "test/delay") + if err := s.C.Update(context.Background(), cms[1]); err != nil { + t.Fatalf("failed adding finalizer to configmap: %v", err) + } + + cr := utils.GetUnstructuredObj("facade.foocorp.com", "v1alpha1", "DConfig", "configs", "config-team-a") + err := s.C.Delete(context.Background(), cr) + if err != nil { + t.Fatalf("failed to delete facade: %v", err) + } + // Ensure only one stage has been processed (the second is held up by the non-deleted configmap). + s.C.MustHaveEvent("configs", "Delete", "Deleting objects for stage project", scenario.DeleteTimeout) + s.C.MustNotHaveEvent("configs", "Delete", "Deleting objects for stage createnamespace", scenario.DeleteTimeout) + // Ensure the first configmap (without the finalizer) has been deleted. + s.C.MustNotExist([]*unstructured.Unstructured{cms[0]}, scenario.DeleteTimeout) + + // Remove the finalizer to allow the deletion to complete + tmpCm, err := s.C.Read(cms[1]) + if err != nil { + t.Fatalf("failed reading configmap: %v", err) + } + cms[1] = tmpCm + controllerutil.RemoveFinalizer(cms[1], "test/delay") + if err := s.C.Update(context.Background(), cms[1]); err != nil { + t.Fatalf("failed removing finalizer from configmap: %v", err) + } + + // Verify the remaining objects have been deleted + s.C.MustHaveEvent("configs", "Delete", "Deleting objects for stage createnamespace", scenario.DeleteTimeout) + s.C.MustNotExist([]*unstructured.Unstructured{cr}, scenario.DeleteTimeout) + s.C.MustNotExist(cms, scenario.DeleteTimeout) + + cr = utils.GetUnstructuredObj("facade.foocorp.com", "v1alpha1", "DConfig", "configs", "config-team-b") + err = s.C.Delete(context.Background(), cr) + if err != nil { + t.Fatalf("failed to delete facade: %v", err) + } + s.C.MustNotExist([]*unstructured.Unstructured{cr}, scenario.DeleteTimeout) + cms[0] = utils.GetConfigMapObj("team-b-ns", "d-proj-a") + cms[1] = utils.GetConfigMapObj("team-b-ns", "d-proj-b") + s.C.MustNotExist(cms, scenario.DeleteTimeout) + + planA := utils.GetPlanObj("team-a-ns", "dconfigs-team-a-config") + planB := utils.GetPlanObj("team-b-ns", "dconfigs-team-b-config") + s.C.MustNotExist([]*unstructured.Unstructured{planA, planB}, scenario.DeleteTimeout) +} diff --git a/composition/tests/testclient/client.go b/composition/tests/testclient/client.go new file mode 100644 index 0000000..b8642ca --- /dev/null +++ b/composition/tests/testclient/client.go @@ -0,0 +1,583 @@ +// Copyright 2024 Google LLC +// +// 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 testclient defines an MCS test client with basic checks. +package testclient + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + apierrors "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" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kustomize/kstatus/status" +) + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + //TODO (barney-s) remove compositionv1alpha1 from test package + utilruntime.Must(compositionv1alpha1.AddToScheme(scheme)) +} + +// Client - test client of MCS target cluster +type Client struct { + T *testing.T + Ctx context.Context + cs *kubernetes.Clientset + client.Client + name string + testName string + logRoot string +} + +func New(ctx context.Context, t *testing.T, config *rest.Config, name, testName string, logRoot string) *Client { + c, err := client.New(config, client.Options{Scheme: scheme}) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + return &Client{ + T: t, + Ctx: ctx, + cs: clientset, + Client: c, + name: name, + testName: testName, + logRoot: logRoot, + } +} + +func (c *Client) String() string { + return c.name +} + +func (c *Client) WriteNamespacePodLogs(namespace string) { + // Get pods in namespace + pods, err := c.cs.CoreV1().Pods(namespace).List(c.Ctx, metav1.ListOptions{}) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } + + for _, pod := range pods.Items { + // Get containers in pod + for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) { + // Write container logs + c.WriteContainerLogs(namespace, pod.Name, container.Name) + } + } +} + +func (c *Client) getFilePath(namespace, name, container string) string { + path := c.logRoot + filepath.Join("test-logs", c.testName, namespace) + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + panic(fmt.Sprintf("unable to create dir: %s, %v", path, err)) + } + filename := name + "." + container + ".txt" + return c.logRoot + filepath.Join("test-logs", c.testName, namespace, filename) +} + +func (c *Client) ClearOldLogs() { + path := filepath.Join("test-logs", c.testName) + err := os.RemoveAll(path) + if err != nil { + panic(fmt.Sprintf("unable to remove dir: %s, %v", path, err)) + } +} + +func (c *Client) WriteContainerLogs(namespace, name, container string) { + podLogOpts := corev1.PodLogOptions{Container: container} + + req := c.cs.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts) + + podLogs, err := req.Stream(c.Ctx) + if err != nil { + c.T.Logf("Error getting log stream for pod: %s.%s %v", name, container, err) + // non fatal error seen once. + // Lets not fail the test because of this. + // client.go:126: Error getting log stream for pod: \ + // projectconfigmap-team-a-config-project-4x7nd.copyout container "copyout" \ + // in pod "projectconfigmap-team-a-config-project-4x7nd" is waiting\ + // to start: PodInitializing + return + } + defer func() { + if err := podLogs.Close(); err != nil { + panic(fmt.Sprintf("Error closing pod logs reader: %v", err)) + } + }() + + filename := c.getFilePath(namespace, name, container) + file, err := os.Create(filename) + if err != nil { + c.T.Errorf("Error creating log file: %s %v", filename, err) + c.T.FailNow() + } + defer func() { + if err := file.Close(); err != nil { + panic(fmt.Sprintf("Error closing file reader: %s, %v", filename, err)) + } + }() + + _, err = io.Copy(file, podLogs) + if err != nil { + c.T.Errorf("Failed to copy pod(%s.%s) logs to file: %s, %v", name, container, filename, err) + c.T.FailNow() + } +} + +// MustCreate - create object +func (c *Client) MustCreate(u *unstructured.Unstructured, updateAllowed bool) { + c.T.Helper() + + id := ExtractGVKNN(u) + c.T.Logf("Creating object %q on cluster %q", id, c) + err := c.Create(c.Ctx, u) + if err != nil { + if !apierrors.IsAlreadyExists(err) { + c.T.Errorf("failed to create (absent) %q: %s", id, err) + c.T.FailNow() + } + if updateAllowed { + c.T.Logf("Updating already present %q on cluster %q", id, c) + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + readobj, err := c.Read(u) + if err != nil { + c.T.Errorf("failed reading %q when updating: %s", id, err) + return err + } + u.SetResourceVersion(readobj.GetResourceVersion()) + return c.Update(c.Ctx, u) + }) + if err != nil { + c.T.Errorf("failed to update %q: %s", id, err) + c.T.FailNow() + } + } else { + c.T.Logf("WARN object exists. Reusing. %s", err) + } + } +} + +// Read - read object with obj's gvknn +func (c *Client) Read(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + c.T.Helper() + + // new encompassing struct that is printable - has String method + id := ExtractGVKNN(obj) + readObj := &unstructured.Unstructured{} + readObj.SetGroupVersionKind(id.GroupVersionKind) + err := c.Get(c.Ctx, types.NamespacedName{ + Namespace: id.Namespace, + Name: id.Name, + }, readObj) + if err != nil { + err = fmt.Errorf("cannot read %q on %q: %w", id, c, err) + } + return readObj, err +} + +// MustDelete - Delete object if exists +func (c *Client) MustDelete(u *unstructured.Unstructured) { + c.T.Helper() + + id := ExtractGVKNN(u) + c.T.Logf("Deleting object %q on cluster %q", id, c) + err := c.Delete(c.Ctx, u) + if err != nil { + if !apierrors.IsNotFound(err) { + c.T.Errorf("failed to delete (present) %q: %s", id, err) + c.T.FailNow() + } + c.T.Logf("WARN %q not found on cluster %q: %s", id, c, err) + } +} + +// MustExist - waits for all objs to exist +func (c *Client) MustExist(objs []*unstructured.Unstructured, timeout time.Duration) { + c.T.Helper() + c.T.Logf("Checking existence of objects %q on %q", ExtractGVKNNs(objs), c) + + toFind := objs + retryFrequency := getFrequency(c.T, timeout) + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) + }, func() (err error) { + exists := func(obj *unstructured.Unstructured) error { + _, err := c.Read(obj) + return err + } + toFind, err = c.recordFailed(exists, toFind) + return err + }) + if err != nil { + c.T.Errorf("objects absent on %q: %q", c, err) + c.T.FailNow() + } +} + +// MustHaveCondition - validate if the .status.conditions[] has the condition +func (c *Client) MustHaveCondition(obj *unstructured.Unstructured, condition *metav1.Condition, timeout time.Duration) { + c.T.Helper() + + toMatch := ExtractGVKNN(obj) + c.T.Logf("Checking condition %q present in object %q on cluster %q", condition.Type, toMatch, c) + + unmatched := []*unstructured.Unstructured{obj} + errMissing := fmt.Errorf("condition missing") + errMismatch := fmt.Errorf("condition mismatch") + + retryFrequency := getFrequency(c.T, timeout) + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, errMissing) || errors.Is(err, errMismatch) + }, func() (err error) { + match := func(obj *unstructured.Unstructured) error { + read, err := c.Read(obj) + if err != nil { + return err + } + conditions, ok, err := unstructured.NestedSlice(read.Object, "status", "conditions") + if err != nil { + return fmt.Errorf("getting status.conditions: %w", err) + } + if !ok { + return fmt.Errorf(".status.conditions not found: %w", errMissing) + } + found := false + for i := range conditions { + cond, ok := conditions[i].(map[string]interface{}) + if !ok { + return fmt.Errorf(".condition[%d] not of type map[string]interface{}: %w", i, errMissing) + } + if condition.Type != cond["type"] { + continue + } + found = true + if condition.Reason != "" && condition.Reason != cond["reason"] { + return fmt.Errorf(".condition.reason=%s not expected value %s: %w", + condition.Reason, cond["reason"], errMismatch) + } + if condition.Message != "" && condition.Message != cond["message"] { + return fmt.Errorf(".condition.message=%s not expected value %s: %w", + condition.Message, cond["message"], errMismatch) + } + c.T.Logf("Has condition [%s, %s, %s]", cond["type"], cond["reason"], cond["message"]) + } + if !found { + return fmt.Errorf(".condition.type=%s not found: %w", condition.Type, errMissing) + } + return nil + } + unmatched, err = c.recordFailed(match, unmatched) + return err + }) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } +} + +// MustNotHaveCondition - validate if the .status.conditions[] does not have the condition +func (c *Client) MustNotHaveCondition(obj *unstructured.Unstructured, + condition *metav1.Condition, timeout time.Duration) { + c.T.Helper() + + toMatch := ExtractGVKNN(obj) + c.T.Logf("Checking condition %q not present in object %q on cluster %q", condition.Type, toMatch, c) + + matched := []*unstructured.Unstructured{obj} + errMatches := fmt.Errorf("condition found") + + retryFrequency := getFrequency(c.T, timeout) + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, errMatches) + }, func() (err error) { + match := func(obj *unstructured.Unstructured) error { + read, err := c.Read(obj) + if err != nil { + return err + } + conditions, ok, err := unstructured.NestedSlice(read.Object, "status", "conditions") + if err != nil { + return err + } + if !ok { + // No condition array means condition doesnt exist + return nil + } + found := false + for i := range conditions { + c, ok := conditions[i].(map[string]interface{}) + if !ok { + // shouldnt happen. we will skip over + continue + } + if condition.Type == c["type"] { + found = true + break + } + } + if found { + return fmt.Errorf(".condition.type=%s found. %w", condition.Type, errMatches) + } + return nil + } + matched, err = c.recordFailed(match, matched) + return err + }) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } +} + +// MustMatchSpec - verify specs of objects match objs +func (c *Client) MustMatchSpec(objs []*unstructured.Unstructured, timeout time.Duration) { + c.T.Helper() + + toMatch := ExtractGVKNNs(objs) + c.T.Logf("Matching specs of objects %q on %q", toMatch, c) + + unmatched := objs + errMismatch := fmt.Errorf("unexpected spec") + retryFrequency := getFrequency(c.T, timeout) + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, errMismatch) + }, func() (err error) { + match := func(obj *unstructured.Unstructured) error { + spec := obj.UnstructuredContent()["spec"] + read, err := c.Read(obj) + if err != nil { + return err + } + readSpec := read.UnstructuredContent()["spec"] + if !reflect.DeepEqual(spec, readSpec) { + diff := cmp.Diff(spec, readSpec) + // Cannot use %q on spec strings. + // The double quotes change the rendering of many characters. + return fmt.Errorf(`%w: spec for object %q on %q is: %s +and not equal to: %s, +with diff %s`, errMismatch, ExtractGVKNN(obj), c, readSpec, spec, diff) + } + return nil + } + unmatched, err = c.recordFailed(match, unmatched) + return err + }) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } +} + +// MustBeReady - waits for all objs to be available +func (c *Client) MustBeReady(objs []*unstructured.Unstructured, timeout time.Duration) { + c.T.Helper() + c.T.Logf("Checking readiness of objects %q on %q", ExtractGVKNNs(objs), c) + + notReady := objs + errNotReady := fmt.Errorf("object is not available") + retryFrequency := getFrequency(c.T, timeout) + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, errNotReady) + }, func() (err error) { + checkReady := func(obj *unstructured.Unstructured) error { + read, err := c.Read(obj) + if err != nil { + return err + } + result, err := status.Compute(read) + if err != nil { + return err + } + if result.Status != status.CurrentStatus { + return fmt.Errorf("missing object %q: %w", ExtractGVKNN(read), errNotReady) + } + return nil + } + notReady, err = c.recordFailed(checkReady, notReady) + return err + }) + if err != nil { + c.T.Errorf("objects unavailable on %q", c) + c.T.FailNow() + } +} + +// MustJSONPatch - applies JSON patch to obj +func (c *Client) MustJSONPatch(obj *unstructured.Unstructured, patch map[string]any) { + c.T.Helper() + c.T.Logf("Applying patch %q to %q on %q", patch, ExtractGVKNN(obj), c) + + serialPatch, err := json.Marshal([]map[string]any{ + patch, + }) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } + formalPatch := client.RawPatch(types.JSONPatchType, serialPatch) + err = c.Patch(c.Ctx, obj, formalPatch) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } +} + +// MustNotExist - checks none of objs exists +func (c *Client) MustNotExist(objs []*unstructured.Unstructured, timeout time.Duration) { + c.T.Helper() + c.T.Logf("Checking absence of objects %q on %q", ExtractGVKNNs(objs), c) + + existing := objs + retryFrequency := getFrequency(c.T, timeout) + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsAlreadyExists(err) + }, func() (err error) { + doesNotExist := func(obj *unstructured.Unstructured) error { + return c.checkNotExist(obj) + } + existing, err = c.recordFailed(doesNotExist, existing) + return err + }) + if err != nil { + c.T.Errorf("objects should not exist on %q, err: %q", c, err) + c.T.FailNow() + } +} + +func (c *Client) MustHaveEvent(ns, reason, msg string, timeout time.Duration) { + c.T.Helper() + c.T.Logf("Checking event with reason %q and message %q is present in namespace %q", reason, msg, ns) + retryFrequency := getFrequency(c.T, timeout) + errMissing := fmt.Errorf("could not find any events") + errMismatch := fmt.Errorf("could not find matching event") + + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, errMissing) || errors.Is(err, errMismatch) + }, func() (err error) { + var eventList corev1.EventList + opts := []client.ListOption{client.InNamespace(ns)} + err = c.List(c.Ctx, &eventList, opts...) + if err != nil { + return err + } + if len(eventList.Items) == 0 { + return errMissing + } + for _, event := range eventList.Items { + if event.Reason == reason && event.Message == msg { + return nil + } + } + return errMismatch + }) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } +} + +func (c *Client) MustNotHaveEvent(ns, reason, msg string, timeout time.Duration) { + c.T.Helper() + c.T.Logf("Checking event with reason %q and message %q is not present in namespace %q", reason, msg, ns) + retryFrequency := getFrequency(c.T, timeout) + errMatches := fmt.Errorf("matching event found") + + err := retry.OnError(retryFrequency, func(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, errMatches) + }, func() (err error) { + var eventList corev1.EventList + opts := []client.ListOption{client.InNamespace(ns)} + err = c.List(c.Ctx, &eventList, opts...) + if err != nil { + return err + } + if len(eventList.Items) == 0 { + return nil + } + for _, event := range eventList.Items { + if event.Reason == reason && event.Message == msg { + return errMatches + } + } + return nil + }) + if err != nil { + c.T.Errorf("unexpected error: %v", err) + c.T.FailNow() + } +} + +// recordFailed - returns all objects on which op failed and an error +// encompassing all corresponding errors +func (c *Client) recordFailed(op func(*unstructured.Unstructured) error, + objs []*unstructured.Unstructured) ([]*unstructured.Unstructured, error) { + c.T.Helper() + + var failed []*unstructured.Unstructured + var errs []error + for _, obj := range objs { + if err := op(obj); err != nil { + failed = append(failed, obj) + errs = append(errs, err) + } + } + return failed, errors.Join(errs...) +} + +// checkNotExist - expects that obj does not exist +func (c *Client) checkNotExist(obj *unstructured.Unstructured) error { + c.T.Helper() + + _, err := c.Read(obj) + if err == nil { + err = apierrors.NewAlreadyExists(schema.GroupResource{ + Group: obj.GroupVersionKind().Group, + Resource: obj.GroupVersionKind().Kind, + }, obj.GetName()) + } else if apierrors.IsNotFound(err) { + err = nil + } + return err +} diff --git a/composition/tests/testclient/framework.go b/composition/tests/testclient/framework.go new file mode 100644 index 0000000..ea78191 --- /dev/null +++ b/composition/tests/testclient/framework.go @@ -0,0 +1,51 @@ +// Copyright 2024 Google LLC +// +// 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 testclient + +import ( + "testing" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" +) + +const opDuration = 2 * time.Second + +// Poll - polls for op to return without an error. If op has not executed +// without error by timeout, Poll fails the test. +func Poll(t *testing.T, op func() error, timeout time.Duration) { + t.Helper() + + retryFrequency := wait.Backoff{ + Duration: opDuration, + Steps: int(timeout / opDuration), + } + err := retry.OnError(retryFrequency, func(_ error) bool { return true }, op) + if err != nil { + t.Errorf("timeout") + t.FailNow() + } +} + +// getFrequency - calculates the cadence at which test checks, time boxed +// by duration, are retried until timeout +func getFrequency(t *testing.T, timeout time.Duration) wait.Backoff { + t.Helper() + return wait.Backoff{ + Duration: opDuration, + Steps: int(timeout / opDuration), + } +} diff --git a/composition/tests/testclient/gkvnn.go b/composition/tests/testclient/gkvnn.go new file mode 100644 index 0000000..dbb246d --- /dev/null +++ b/composition/tests/testclient/gkvnn.go @@ -0,0 +1,68 @@ +// Copyright 2024 Google LLC +// +// 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 util provides utilities for e2e testing. +package testclient + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// GVKNN - id for k8s object +type GVKNN struct { + schema.GroupVersionKind + types.NamespacedName +} + +// ExtractGVKNN - returns GVKNN of obj +func ExtractGVKNN(obj client.Object) *GVKNN { + return &GVKNN{ + obj.GetObjectKind().GroupVersionKind(), + types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + }, + } +} + +// ExtractGVKNNs - returns GVKNNs of objs +func ExtractGVKNNs(objs []*unstructured.Unstructured) []*GVKNN { + ids := make([]*GVKNN, len(objs)) + for i, obj := range objs { + ids[i] = ExtractGVKNN(obj) + } + return ids +} + +// MakeObject - create object with GVKNN of id +func (id *GVKNN) MakeObject() *unstructured.Unstructured { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(id.GroupVersionKind) + obj.SetNamespace(id.Namespace) + obj.SetName(id.Name) + return obj +} + +// String - returns GVKNN delimited by "/" +func (id *GVKNN) String() string { + if id.Namespace == "" { + return fmt.Sprintf("%s/%s", id.GroupVersionKind.Kind, id.Name) + } + return fmt.Sprintf("%s/%s/%s", id.GroupVersionKind.Kind, id.Namespace, id.Name) +} diff --git a/composition/tests/testclient/unstructured.go b/composition/tests/testclient/unstructured.go new file mode 100644 index 0000000..6e19b89 --- /dev/null +++ b/composition/tests/testclient/unstructured.go @@ -0,0 +1,58 @@ +// Copyright 2024 Google LLC +// +// 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 testclient + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Note: The functions below can be used in Polling because they are called on +// paths holding fields that should absolutely be of the specified type. If not, +// there is a fundamental error in which case we can exit the test immediately +// instead of telling Poll to err. +// MustGetBool - returns bool on u at path if set +func MustGetBool(t *testing.T, u *unstructured.Unstructured, path ...string) (value, set bool) { + t.Helper() + value, set, err := unstructured.NestedBool(u.Object, path...) + if err != nil { + t.Errorf("value at path %v not bool", path) + t.FailNow() + } + return +} + +// MustGetString - return string on u at path if set +func MustGetString(t *testing.T, u *unstructured.Unstructured, path ...string) (string, bool) { + t.Helper() + value, set, err := unstructured.NestedString(u.Object, path...) + if err != nil { + t.Errorf("value at path %v not string", path) + t.FailNow() + } + return value, set +} + +// GetInt - return int64 on u at path if set +func MustGetInt64(t *testing.T, u *unstructured.Unstructured, path ...string) (int64, bool) { + t.Helper() + value, set, err := unstructured.NestedInt64(u.Object, path...) + if err != nil { + t.Errorf("value at path %v not int64", path) + t.FailNow() + } + return value, set +} diff --git a/composition/tests/utils/utils.go b/composition/tests/utils/utils.go new file mode 100644 index 0000000..07c24c8 --- /dev/null +++ b/composition/tests/utils/utils.go @@ -0,0 +1,156 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "fmt" + "time" + + compositionv1alpha1 "github.com/cloud-native-compositions/compositions/composition/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/composition/internal/controller" + 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" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" +) + +var ( + scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(compositionv1alpha1.AddToScheme(scheme)) +} + +func StartLocalController(config *rest.Config) error { + mgr, err := ctrl.NewManager(config, ctrl.Options{ + Scheme: scheme, + LeaderElection: false, + }) + + if err != nil { + return fmt.Errorf("Unable to start manager: %w", err) + } + + if err = (&controller.CompositionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create Composition controller: %w", err) + } + if err = (&controller.ContextReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create Context controller: %w", err) + } + if err = (&controller.PlanReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create Plan controller: %w", err) + } + + var managerStartError error + go func() { + managerStartError = mgr.Start(ctrl.SetupSignalHandler()) + }() + time.Sleep(time.Second * 5) + if managerStartError != nil { + return fmt.Errorf("Problem running manager: %w", managerStartError) + } + return nil +} + +func GetUnstructuredObj(g, v, k, ns, n string) *unstructured.Unstructured { + cr := unstructured.Unstructured{} + cr.SetGroupVersionKind(schema.GroupVersionKind{Group: g, Version: v, Kind: k}) + cr.SetName(n) + cr.SetNamespace(ns) + return &cr +} + +func GetFacadeBindingObj(ns, n string) *unstructured.Unstructured { + return GetUnstructuredObj("composition.google.com", "v1alpha1", "FacadeBinding", ns, n) +} + +func GetCompositionObj(ns, n string) *unstructured.Unstructured { + return GetUnstructuredObj("composition.google.com", "v1alpha1", "Composition", ns, n) +} + +func GetFacadeObj(ns, n string) *unstructured.Unstructured { + return GetUnstructuredObj("composition.google.com", "v1alpha1", "Facade", ns, n) +} + +func GetPlanObj(ns, n string) *unstructured.Unstructured { + return GetUnstructuredObj("composition.google.com", "v1alpha1", "Plan", ns, n) +} + +func GetConfigMapObj(ns, n string) *unstructured.Unstructured { + return GetUnstructuredObj("", "v1", "ConfigMap", ns, n) +} + +func GetValidationFailedCondition(reason, message string) *metav1.Condition { + return &metav1.Condition{ + Message: message, + Reason: reason, + Type: string(compositionv1alpha1.ValidationFailed), + } +} + +func GetReadyCondition(reason, message string) *metav1.Condition { + return &metav1.Condition{ + Message: message, + Reason: reason, + Type: string(compositionv1alpha1.Ready), + } +} + +func GetErrorCondition(reason, message string) *metav1.Condition { + return &metav1.Condition{ + Message: message, + Reason: reason, + Type: string(compositionv1alpha1.Error), + } +} + +func GetWaitingCondition(reason, message string) *metav1.Condition { + return &metav1.Condition{ + Message: message, + Reason: reason, + Type: string(compositionv1alpha1.Waiting), + } +} + +func GetContextObj(context map[string]string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "composition.google.com/v1alpha1", + "kind": "Context", + "metadata": map[string]interface{}{ + "name": "context", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "project": context["gcpProject"], + }, + }, + } +} diff --git a/expanders/cel-expander/.gitignore b/expanders/cel-expander/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/expanders/cel-expander/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/expanders/cel-expander/Dockerfile b/expanders/cel-expander/Dockerfile new file mode 100644 index 0000000..673903c --- /dev/null +++ b/expanders/cel-expander/Dockerfile @@ -0,0 +1,52 @@ +# Copyright 2024 Google LLC +# +# 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. + + +# syntax=docker/dockerfile:1 + +# ----------------- Build Container --------------------------- +# Build the go app. +# Explicitly set to latest vs golang:1.22 +FROM golang:1.23.2 AS build-stage + +# Set destination for COPY +WORKDIR /go/src/app + +# Download Go modules +# https://docs.docker.com/reference/dockerfile/#copy +COPY ./ ./ +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -v -o expander main.go + + +FROM golang:1.23.2 as bins + +WORKDIR /workspace + + +#ARG VERSION +# -ldflags "-X kpt.dev/configsync/pkg/version.VERSION=${VERSION}" \ + +FROM gcr.io/distroless/static:latest as cel-sync +# Setting HOME ensures that whatever UID this ultimately runs as can write files. +ENV HOME=/tmp +WORKDIR / +COPY --from=build-stage /go/src/app/expander . + +ENTRYPOINT ["expander"] + +# Switch to non-root user +USER 1000 \ No newline at end of file diff --git a/expanders/cel-expander/Makefile b/expanders/cel-expander/Makefile new file mode 100644 index 0000000..5107111 --- /dev/null +++ b/expanders/cel-expander/Makefile @@ -0,0 +1,172 @@ +GCP_PROJECT_ID ?= $(shell gcloud config get-value project) + +# Image URLs to use for building/pushing image targets +GIT_IMG_VERSION ?= $(shell git rev-parse --short HEAD) +IMG_VERSION ?= v0.0.1 +IMG_REGISTRY ?= gcr.io/$(GCP_PROJECT_ID) +EXPANDER_IMG ?= $(IMG_REGISTRY)/expander-cel:$(IMG_VERSION) +EXPANDER_BINARY ?= cel +EXPANDER_SERVICE ?= composition-cel-v0-0-1 +KIND_CLUSTER ?= kind +GOPREFIX ?= GOWORK=off + +.PHONY: all +all: build + +.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) + +##@ CEL Expander CRD and manifests + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(GOPREFIX) $(CONTROLLER_GEN) crd paths="./api/..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen + $(GOPREFIX) $(CONTROLLER_GEN) object paths="./api/..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + $(GOPREFIX) go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + $(GOPREFIX) go vet ./... + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter & yamllint + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/expanders && $(KUSTOMIZE) edit set image expander-cel=${EXPANDER_IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + cd config/expanders && $(KUSTOMIZE) edit set image expander-cel=expander-cel:latest + +.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 - + + +.PHONY: release-manifests +release-manifests: manifests kustomize + $(KUSTOMIZE) build config/release -o release/manifest.yaml + +##@ Tooling +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 +GOLANGCI_LINT_VERSION ?= v1.54.2 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) + + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary (ideally with version) +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f $(1) ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +GOBIN=$(LOCALBIN) $(GOPREFIX) go install $${package} ;\ +mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ +} +endef + + +###### ----------- CEL Expander ---------------------------- + +##@ CEL expander pod + +.PHONY: build +build: generate + $(GOPREFIX) go build -v -o bin/${EXPANDER_BINARY} ./ + +.PHONY: clean +clean: ## clean binary. + rm -fr bin/${EXPANDER_BINARY} + docker rmi ${EXPANDER_IMG} . + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: build #build ## Build docker image with the manager. + docker build -t ${EXPANDER_IMG} -f Dockerfile . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${EXPANDER_IMG} + +.PHONY: docker-run +docker-run: docker-build + docker run -p 8443:8443 --entrypoint /expander ${EXPANDER_IMG} + +.PHONY: create-kind +create-kind: + kind delete clusters ${KIND_CLUSTER} || true + kind create cluster --name ${KIND_CLUSTER} + +.PHONY: deploy-kind +deploy-kind: kustomize manifests docker-build + kind load docker-image ${EXPANDER_IMG} --name ${KIND_CLUSTER} + $(KUBECTL) --context kind-${KIND_CLUSTER} create namespace composition-system || true + $(KUSTOMIZE) build config/crd | $(KUBECTL) --context kind-${KIND_CLUSTER} apply -f - + sleep 5 # for CRDs to be registered + cd config/expanders && $(KUSTOMIZE) edit set image expander-cel=${EXPANDER_IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) --context kind-${KIND_CLUSTER} apply -f - + cd config/expanders && $(KUSTOMIZE) edit set image expander-cel=expander-cel:latest + # this is ok to fail for unit-test cases + $(KUSTOMIZE) build config/expander-versions | $(KUBECTL) --context kind-${KIND_CLUSTER} apply -f - || true + sleep 5 + kubectl --context kind-${KIND_CLUSTER} get pods -A + +.PHONY: unit-test +unit-test: create-kind deploy-kind + kubectl patch service -n composition-system ${EXPANDER_SERVICE} -p '{"spec":{"type":"LoadBalancer"}}' + sleep 30 + nodeip=$$(kubectl get nodes -o json | jq '.items[0].status.addresses[0].address' | xargs echo );\ + nodeport=$$(kubectl get service -n composition-system ${EXPANDER_SERVICE} -o json | jq ".spec.ports[0].nodePort");\ + echo $$nodeip:$$nodeport; \ + $(GOPREFIX) go test -v --addr=$$nodeip:$$nodeport \ No newline at end of file diff --git a/expanders/cel-expander/api/v1alpha1/celconfiguration_types.go b/expanders/cel-expander/api/v1alpha1/celconfiguration_types.go new file mode 100644 index 0000000..b5b3482 --- /dev/null +++ b/expanders/cel-expander/api/v1alpha1/celconfiguration_types.go @@ -0,0 +1,62 @@ +/* +Copyright 2024. + +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 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +type Resource struct { + Name string `json:"name"` + Definition runtime.RawExtension `json:"definition"` + Template string `json:"template,omitempty"` +} + +// CELConfigurationSpec defines the desired state of CELConfiguration +type CELConfigurationSpec struct { + Resources []Resource `json:"resources"` +} + +// CELConfigurationStatus defines the observed state of CELConfiguration +type CELConfigurationStatus struct { +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// CELConfiguration is the Schema for the celconfigurations API +type CELConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CELConfigurationSpec `json:"spec,omitempty"` + Status CELConfigurationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// CELConfigurationList contains a list of CELConfiguration +type CELConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CELConfiguration `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CELConfiguration{}, &CELConfigurationList{}) +} diff --git a/expanders/cel-expander/api/v1alpha1/groupversion_info.go b/expanders/cel-expander/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..61f6455 --- /dev/null +++ b/expanders/cel-expander/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +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 composition.google.com v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=composition.google.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "composition.google.com", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/expanders/cel-expander/api/v1alpha1/zz_generated.deepcopy.go b/expanders/cel-expander/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..4934371 --- /dev/null +++ b/expanders/cel-expander/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,121 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CELConfiguration) DeepCopyInto(out *CELConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CELConfiguration. +func (in *CELConfiguration) DeepCopy() *CELConfiguration { + if in == nil { + return nil + } + out := new(CELConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CELConfiguration) 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 *CELConfigurationList) DeepCopyInto(out *CELConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CELConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CELConfigurationList. +func (in *CELConfigurationList) DeepCopy() *CELConfigurationList { + if in == nil { + return nil + } + out := new(CELConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CELConfigurationList) 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 *CELConfigurationSpec) DeepCopyInto(out *CELConfigurationSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]Resource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CELConfigurationSpec. +func (in *CELConfigurationSpec) DeepCopy() *CELConfigurationSpec { + if in == nil { + return nil + } + out := new(CELConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CELConfigurationStatus) DeepCopyInto(out *CELConfigurationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CELConfigurationStatus. +func (in *CELConfigurationStatus) DeepCopy() *CELConfigurationStatus { + if in == nil { + return nil + } + out := new(CELConfigurationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resource) DeepCopyInto(out *Resource) { + *out = *in + in.Definition.DeepCopyInto(&out.Definition) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. +func (in *Resource) DeepCopy() *Resource { + if in == nil { + return nil + } + out := new(Resource) + in.DeepCopyInto(out) + return out +} diff --git a/expanders/cel-expander/config/crd/bases/composition.google.com_celconfigurations.yaml b/expanders/cel-expander/config/crd/bases/composition.google.com_celconfigurations.yaml new file mode 100644 index 0000000..fe0e1c4 --- /dev/null +++ b/expanders/cel-expander/config/crd/bases/composition.google.com_celconfigurations.yaml @@ -0,0 +1,81 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: celconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: CELConfiguration + listKind: CELConfigurationList + plural: celconfigurations + singular: celconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CELConfiguration is the Schema for the celconfigurations API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: CELConfigurationSpec defines the desired state of CELConfiguration + properties: + resources: + items: + properties: + definition: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + template: + type: string + required: + - definition + - name + type: object + type: array + required: + - resources + type: object + status: + description: CELConfigurationStatus defines the observed state of CELConfiguration + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/expanders/cel-expander/config/crd/kustomization.yaml b/expanders/cel-expander/config/crd/kustomization.yaml new file mode 100644 index 0000000..e2cbab4 --- /dev/null +++ b/expanders/cel-expander/config/crd/kustomization.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# 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. + +# 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/composition.google.com_celconfigurations.yaml \ No newline at end of file diff --git a/expanders/cel-expander/config/default/kustomization.yaml b/expanders/cel-expander/config/default/kustomization.yaml new file mode 100644 index 0000000..3e48019 --- /dev/null +++ b/expanders/cel-expander/config/default/kustomization.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Adds namespace to all resources. +namespace: composition-system +namePrefix: composition- +resources: +- ../crd +- ../expanders + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/expanders/cel-expander/config/expander-versions/expander_version.yaml b/expanders/cel-expander/config/expander-versions/expander_version.yaml new file mode 100644 index 0000000..883b804 --- /dev/null +++ b/expanders/cel-expander/config/expander-versions/expander_version.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: cel + namespace: system +spec: + config: + group: composition.google.com + kind: CELConfiguration + version: v1alpha1 + type: grpc + validVersions: + - v0.0.1 \ No newline at end of file diff --git a/expanders/cel-expander/config/expander-versions/kustomization.yaml b/expanders/cel-expander/config/expander-versions/kustomization.yaml new file mode 100644 index 0000000..ef25141 --- /dev/null +++ b/expanders/cel-expander/config/expander-versions/kustomization.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# 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. +namespace: composition-system +namePrefix: composition- +resources: +- expander_version.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/expanders/cel-expander/config/expanders/cel-v0.0.1.yaml b/expanders/cel-expander/config/expanders/cel-v0.0.1.yaml new file mode 100644 index 0000000..2f3056c --- /dev/null +++ b/expanders/cel-expander/config/expanders/cel-v0.0.1.yaml @@ -0,0 +1,115 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cel-v0.0.1 + namespace: system + labels: + control-plane: expander-cel-v0.0.1 + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: cel-v0.0.1 + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: expander-cel-v0.0.1 + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-cel-v0.0.1 + spec: + # (user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + #securityContext: + # runAsNonRoot: true + # (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: + - /expander + args: + - --port=8443 + image: expander-cel:v0.0.1 + #imagePullPolicy: Always + name: cel + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + # (user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + #serviceAccountName: cel-expander + terminationGracePeriodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: expander-cel + app.kubernetes.io/created-by: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + app.kubernetes.io/instance: cel-v0.0.1 + app.kubernetes.io/component: expanders + name: cel-v0-0-1 + namespace: system +spec: + #type: LoadBalancer + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-cel-v0.0.1 \ No newline at end of file diff --git a/expanders/cel-expander/config/expanders/kustomization.yaml b/expanders/cel-expander/config/expanders/kustomization.yaml new file mode 100644 index 0000000..78d3def --- /dev/null +++ b/expanders/cel-expander/config/expanders/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# 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. + +resources: +- cel-v0.0.1.yaml +images: +- name: expander-cel + newName: expander-cel + newTag: latest +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/expanders/cel-expander/config/release/kustomization.yaml b/expanders/cel-expander/config/release/kustomization.yaml new file mode 100644 index 0000000..6f8ef66 --- /dev/null +++ b/expanders/cel-expander/config/release/kustomization.yaml @@ -0,0 +1,31 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Adds namespace to all resources. +namespace: composition-system +namePrefix: composition- +resources: +- ../crd +- ../expanders +- ../expander-versions + +patches: +- patch: '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", + "value": "gcr.io/krmapihosting-release/expander-cel:v0.0.1"}]' + target: + kind: Deployment + name: cel-v0.0.1 + namespace: system +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization \ No newline at end of file diff --git a/expanders/cel-expander/go.mod b/expanders/cel-expander/go.mod new file mode 100644 index 0000000..7043fb3 --- /dev/null +++ b/expanders/cel-expander/go.mod @@ -0,0 +1,39 @@ +module github.com/cloud-native-compositions/compositions/expander/cel-expander + +go 1.22.4 + +toolchain go1.23.2 + +require ( + github.com/cloud-native-compositions/compositions/composition v0.0.0-20240701212443-eb03df209286 + github.com/google/cel-go v0.18.2 + github.com/wzshiming/easycel v0.6.0 + google.golang.org/grpc v1.64.1 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/apimachinery v0.29.1 + sigs.k8s.io/controller-runtime v0.17.2 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/go-logr/logr v1.4.1 // 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 + github.com/stoewer/go-strcase v1.2.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/expanders/cel-expander/go.sum b/expanders/cel-expander/go.sum new file mode 100644 index 0000000..d3ecb1e --- /dev/null +++ b/expanders/cel-expander/go.sum @@ -0,0 +1,120 @@ +github.com/cloud-native-compositions/compositions/composition v0.0.0-20240701212443-eb03df209286 h1:U8TWXJeAegVv/kzPj2rAXWlxQbsWkhWSfKXWlJvuaUQ= +github.com/cloud-native-compositions/compositions/composition v0.0.0-20240701212443-eb03df209286/go.mod h1:b+qvSKhDpHuIDqWavpFEBsFW/1VjjFgjVqlD+KgGOHk= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +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.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/cel-go v0.18.2 h1:L0B6sNBSVmt0OyECi8v6VOS74KOc9W/tLiWKfZABvf4= +github.com/google/cel-go v0.18.2/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +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-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +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/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +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/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wzshiming/easycel v0.6.0 h1:TwvbeAhi3a1Hf2wjcxlHQvE3W5pQAnhHynekCg5caJY= +github.com/wzshiming/easycel v0.6.0/go.mod h1:e9t2Fk3f6jxAN4JVwOq8BLPPeOC/4eknIy+QFIYaa2w= +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/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +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/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +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= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/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/expanders/cel-expander/main.go b/expanders/cel-expander/main.go new file mode 100644 index 0000000..2ff1631 --- /dev/null +++ b/expanders/cel-expander/main.go @@ -0,0 +1,269 @@ +// Copyright 2024 Google LLC +// +// 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 ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + celconfigurationv1alpha1 "github.com/cloud-native-compositions/compositions/expander/cel-expander/api/v1alpha1" + "github.com/cloud-native-compositions/compositions/expander/cel-expander/pkg/cel" + "github.com/cloud-native-compositions/compositions/expander/cel-expander/pkg/resource" + "google.golang.org/grpc" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" +) + +var ( + port = flag.Int("port", 8443, "The server port") +) + +type Expander struct { + resource string + config *celconfigurationv1alpha1.CELConfiguration + facade *unstructured.Unstructured + context *unstructured.Unstructured + fetchedValues *map[string]interface{} + cel *cel.Engine + resources []*resource.Resource + ctx context.Context +} + +func NewExpander(ctx context.Context, req *pb.EvaluateRequest, vreq *pb.ValidateRequest) (*Expander, string, error) { + var configBytes []byte + var facadeBytes []byte + var contextBytes []byte + var fetchedValueBytes []byte + var resource string + + failedMessage := "" + + e := &Expander{ + ctx: ctx, + } + + if req != nil { + configBytes = req.Config + facadeBytes = req.Facade + contextBytes = req.Context + fetchedValueBytes = req.Value + resource = req.Resource + if string(facadeBytes) == "" { + return nil, failedMessage, fmt.Errorf("Empty Facade for an Evaluate call") + } + } + if vreq != nil { + configBytes = vreq.Config + facadeBytes = vreq.Facade + contextBytes = vreq.Context + fetchedValueBytes = vreq.Value + resource = vreq.Resource + } + + e.config = &celconfigurationv1alpha1.CELConfiguration{} + e.resource = resource + + if string(configBytes) == "" { + failedMessage = fmt.Sprintf("empty Config passed") + return nil, failedMessage, nil + } + err := json.Unmarshal(configBytes, e.config) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Config into CELConfiguration CR: %w", err) + } + + if string(contextBytes) != "" { + e.context = &unstructured.Unstructured{} + err := e.context.UnmarshalJSON(contextBytes) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Context: %w", err) + } + } + + if string(fetchedValueBytes) != "" { + e.fetchedValues = &map[string]interface{}{} + err := json.Unmarshal(fetchedValueBytes, e.fetchedValues) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Context: %w", err) + } + } + + if string(facadeBytes) != "" { + e.facade = &unstructured.Unstructured{} + err := e.facade.UnmarshalJSON(facadeBytes) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Facade into Unstructured: %w", err) + } + if e.facade.GetNamespace() == "" { + //e.facade.SetNamespace("default") + failedMessage = fmt.Sprintf("missing namespace in req.Facade object") + return nil, failedMessage, nil + } + } + + return e, failedMessage, nil +} + +func (e *Expander) ProcessInputs() error { + var err error + + values := map[string]interface{}{} + // Setup values + if e.facade != nil { + values[e.resource] = e.facade.Object + } + if e.context != nil { + values["context"] = e.context.Object + } + if e.fetchedValues != nil { + values["fetched"] = *e.fetchedValues + } + + // Create a CEL engine + e.cel, err = cel.NewEngine(e.resource, values) + if err != nil { + return fmt.Errorf("error creating CEL engine: %w", err) + } + + // load resources + for _, rsrc := range e.config.Spec.Resources { + yamlContent, err := yaml.JSONToYAML(rsrc.Definition.Raw) + if err != nil { + return fmt.Errorf("failed to marshall resource:%s file to yaml: %w", rsrc.Name, err) + } + r, err := resource.NewResourceFromRaw(rsrc.Name, yamlContent) + if err != nil { + return fmt.Errorf("error creating resource for %s, %w", rsrc.Name, err) + } + e.resources = append(e.resources, r) + } + return nil + +} + +func (e *Expander) Validate(result *pb.ValidateResult) (*pb.ValidateResult, error) { + return result, nil +} + +func (e *Expander) Evaluate(result *pb.EvaluateResult) (*pb.EvaluateResult, error) { + // Loop through resources + manifests := []byte{} + for _, rsrc := range e.resources { + // loop through variables + for vindex := range rsrc.Variables { + // cel.eval() + result, err := e.cel.Eval(rsrc.Variables[vindex].CELExpression) + if err != nil { + // TODO: consume the error and mark result failed ? + return nil, fmt.Errorf("error Evaluating expression: %s, %w", rsrc.Variables[vindex].Expression, err) + } + rsrc.Variables[vindex].ResolvedValue = result + } + // Replace variables + err := rsrc.ApplyResolvedVariables() + if err != nil { + return nil, fmt.Errorf("error applying resolved variables for %s, %w", rsrc.Name, err) + } + manifests = append(manifests, []byte("\n---\n")...) + manifests = append(manifests, rsrc.Raw...) + } + + // return objects + result.Manifests = manifests + return result, nil +} + +// ------------- GRPC Server implementation ---------------- + +// server is used to implement exander.Evaluator +type server struct { + pb.UnimplementedExpanderServer +} + +// Verify the expander config/template +func (s *server) Validate(ctx context.Context, req *pb.ValidateRequest) (*pb.ValidateResult, error) { + result := &pb.ValidateResult{ + Status: pb.Status_SUCCESS, + Error: &pb.Error{}, + } + + log.Printf("Validate called") + e, failedMessage, err := NewExpander(ctx, nil, req) + if err != nil { + return nil, err + } + if failedMessage != "" { + result.Error.Message = failedMessage + result.Status = pb.Status_VALIDATE_FAILED + return result, nil + } + + if err = e.ProcessInputs(); err != nil { + newerr := fmt.Errorf("error processing inputs: %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + + return e.Validate(result) +} + +// Evaluate the expander config in context of inputs and return manifests +func (s *server) Evaluate(ctx context.Context, req *pb.EvaluateRequest) (*pb.EvaluateResult, error) { + result := &pb.EvaluateResult{ + Status: pb.Status_SUCCESS, + Type: pb.ResultType_MANIFESTS, + Error: &pb.Error{}, + } + + log.Printf("Evaluate called") + e, failedMessage, err := NewExpander(ctx, req, nil) + if err != nil { + return nil, err + } + if failedMessage != "" { + result.Error.Message = failedMessage + result.Status = pb.Status_EVALUATE_FAILED + return result, nil + } + + if err = e.ProcessInputs(); err != nil { + newerr := fmt.Errorf("error processing inputs: %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + + return e.Evaluate(result) +} + +// -------------------- MAIN ------------------------------- + +func main() { + flag.Parse() + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterExpanderServer(s, &server{}) + log.Printf("server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/expanders/cel-expander/main_test.go b/expanders/cel-expander/main_test.go new file mode 100644 index 0000000..bd5ab00 --- /dev/null +++ b/expanders/cel-expander/main_test.go @@ -0,0 +1,531 @@ +// Copyright 2024 Google LLC +// +// 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_test + +import ( + "context" + "flag" + "log" + "math/rand" + "os" + "strings" + "testing" + "time" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "sigs.k8s.io/yaml" +) + +var ( + addr = flag.String("addr", "[::]:8443", "the address to connect to") + + // Empty Chart with just chart.yaml + emptyCEL = `apiVersion: composition.google.com/v1alpha1 +kind: CELConfiguration +metadata: + name: foobar + namespace: config-control +spec: +` + emptyCELRenderedManifets = "" + + // Simple chart with a static template file (no templating) + staticSimpleChart = `apiVersion: composition.google.com/v1alpha1 +kind: CELConfiguration +metadata: + name: foobar + namespace: config-control +spec: + resources: + - name: configmap + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: "3" + bar: "interface" +` + staticSimpleChartRenderedManifets = ` +--- +apiVersion: v1 +data: + bar: interface + foo: "3" +kind: ConfigMap +metadata: + name: demo +` + + simpleCELConfig = `apiVersion: composition.google.com/v1alpha1 +kind: CELConfiguration +metadata: + name: foobar + namespace: config-control +spec: + resources: + - name: configmap + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: ${sqls.spec.foo} + car: ${sqls.spec.car} +` + simpleCELConfigRenderedManifets = ` +--- +apiVersion: v1 +data: + car: sedan + foo: bar +kind: ConfigMap +metadata: + name: demo +` + + simpleConfigUsingFetched = `apiVersion: composition.google.com/v1alpha1 +kind: CELConfiguration +metadata: + name: foobar + namespace: config-control +spec: + resources: + - name: configmap + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: ${sqls.spec.foo} + car: ${fetched.car} +` +) + +var expanderClient pb.ExpanderClient + +func dummyValues(t *testing.T) []byte { + y := `foo: bar +car: sedan +` + j, err := yaml.YAMLToJSON([]byte(y)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, y) + } + return j +} + +func configFrom(t *testing.T, config string) []byte { + j, err := yaml.YAMLToJSON([]byte(config)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, config) + } + return j +} + +func testFacade(t *testing.T, facade string) []byte { + if facade == "" { + facade = `apiVersion: facade.foobar.com/v1alpha1 +kind: Foo +metadata: + name: appteam-sample + namespace: default +spec: + foo: bar + car: sedan +` + } + j, err := yaml.YAMLToJSON([]byte(facade)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, facade) + } + return j +} + +func testContext(t *testing.T) []byte { + y := `apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: config-control +spec: + project: test-project +` + j, err := yaml.YAMLToJSON([]byte(y)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, y) + } + return j +} + +// TestMain - umbrella test that runs all test cases +func TestMain(m *testing.M) { + flag.Parse() + + rand.Seed(time.Now().UnixNano()) + flag.Parse() + // Set up a connection to the server. + conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + expanderClient = pb.NewExpanderClient(conn) + + exitCode := m.Run() + os.Exit(exitCode) +} + +func TestEvaluateEmptyConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err != nil { + t.Fatalf("could not evaluate: %v", err) + } + if r.GetStatus() != pb.Status_EVALUATE_FAILED { + t.Fatalf("want FAILURE, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) +} + +func TestEvaluateBadConfig(t *testing.T) { + config := `dummy config` + _, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, config), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err == nil { + t.Fatalf("expected error. got none") + } + + errMessage := "cannot unmarshal string into Go value of type v1alpha1.CELConfiguration" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestEvaluateNoResourcesConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyCEL), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != emptyCELRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", emptyCELRenderedManifets, r.Manifests) + } +} + +func TestEvaluateEmptyContext(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyCEL), + Context: []byte{}, + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if string(r.Manifests) != emptyCELRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", emptyCELRenderedManifets, r.Manifests) + } +} + +func TestEvaluateEmptyFacade(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyCEL), + Context: testContext(t), + Facade: []byte{}, + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err == nil { + t.Fatalf("expected error. got none") + } + + errMessage := "Empty Facade for an Evaluate call" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestEvaluateEmptyValue(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyCEL), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: []byte{}, + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if string(r.Manifests) != emptyCELRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", emptyCELRenderedManifets, r.Manifests) + } +} + +func TestEvaluateStaticConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, staticSimpleChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != staticSimpleChartRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", staticSimpleChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateUsingFacade(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleCELConfig), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != simpleCELConfigRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", simpleCELConfigRenderedManifets, r.Manifests) + } +} + +func TestEvaluateMissingFacadeField(t *testing.T) { + facade := `apiVersion: facade.foobar.com/v1alpha1 +kind: Foo +metadata: + name: appteam-sample + namespace: default +spec: + # Missing .Values.spec.car + foo: bar + #car: sedan +` + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleCELConfig), + Context: testContext(t), + Facade: testFacade(t, facade), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err == nil { + t.Fatalf("expected error. Got none") + } + + errMessage := "error Evaluating expression: ${sqls.spec.car}, no such key: car" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestEvaluateTemplateUsesValues(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleConfigUsingFetched), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS , got: %s", r.GetStatus()) + } + + if string(r.Manifests) != simpleCELConfigRenderedManifets { + t.Fatalf("\nexpected: %s\ngot: %s", simpleCELConfigRenderedManifets, r.Manifests) + } +} + +// -------------------------------------------------------- +// ----------------- Validate Tests ----------------------- +// -------------------------------------------------------- + +func TestValidateEmptyConfig(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_VALIDATE_FAILED { + t.Fatalf("want FAILURE, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) +} + +func TestValidateEmptyContext(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: configFrom(t, emptyCEL), + Resource: "sqls", + Context: []byte{}, + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateEmptyFacade(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: configFrom(t, emptyCEL), + Resource: "sqls", + Context: testContext(t), + Facade: []byte{}, + Value: dummyValues(t), + }) + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateEmptyValue(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: configFrom(t, emptyCEL), + Resource: "sqls", + Context: testContext(t), + Facade: testFacade(t, ""), + Value: []byte{}, + }) + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateBadConfig(t *testing.T) { + config := `dummy config` + _, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Resource: "sqls", + Config: configFrom(t, config), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err == nil { + t.Fatalf("expected error. got none") + } + + errMessage := "cannot unmarshal string into Go value of type v1alpha1.CELConfiguration" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestValidateNoTemplateConfig(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyCEL), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} diff --git a/expanders/cel-expander/pkg/cel/engine.go b/expanders/cel-expander/pkg/cel/engine.go new file mode 100644 index 0000000..a5dc733 --- /dev/null +++ b/expanders/cel-expander/pkg/cel/engine.go @@ -0,0 +1,73 @@ +// Copyright 2024 Google LLC +// +// 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 cel + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" + "github.com/wzshiming/easycel" +) + +type Engine struct { + resource string + values map[string]interface{} + registry *easycel.Registry + env *easycel.Environment +} + +func NewEngine(resource string, values map[string]interface{}) (*Engine, error) { + // TODO: what withtagname ? can we pass yaml ? + registry := easycel.NewRegistry("cel-engine", easycel.WithTagName("json")) + + // Register variables + err := registry.RegisterVariable(resource, map[string]interface{}{}) + if err != nil { + return nil, err + } + err = registry.RegisterVariable("fetched", map[string]interface{}{}) + if err != nil { + return nil, err + } + err = registry.RegisterVariable("context", map[string]interface{}{}) + if err != nil { + return nil, err + } + + // Environment + env, err := easycel.NewEnvironment(cel.Lib(registry)) + if err != nil { + return nil, err + } + + return &Engine{ + resource: resource, + values: values, + registry: registry, + env: env, + }, nil +} + +func (e *Engine) Eval(expression string) (ref.Val, error) { + prog, err := e.env.Program(expression) + if err != nil { + return nil, err + } + val, _, err := prog.Eval(e.values) + if err != nil { + return nil, err + } + + return val, nil +} diff --git a/expanders/cel-expander/pkg/resource/resource.go b/expanders/cel-expander/pkg/resource/resource.go new file mode 100644 index 0000000..6a7dbc4 --- /dev/null +++ b/expanders/cel-expander/pkg/resource/resource.go @@ -0,0 +1,112 @@ +// Copyright 2024 Google LLC +// +// 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 resource + +import ( + "bytes" + "fmt" + "regexp" + "strings" + + "github.com/google/cel-go/common/types" + "gopkg.in/yaml.v2" +) + +type Resource struct { + Name string + Data map[string]interface{} + Raw []byte + Variables []*Variable +} + +func NewResourceFromRaw(name string, raw []byte) (*Resource, error) { + var data map[string]interface{} + err := yaml.Unmarshal(raw, &data) + if err != nil { + return nil, err + } + + variables, err := extractVariables(raw) + if err != nil { + return nil, err + } + + resource := &Resource{ + Name: name, + Data: data, + Raw: raw, + Variables: variables, + } + return resource, nil +} + +func (r *Resource) replaceVariables(vars map[string]string) []byte { + copy := bytes.Clone(r.Raw) + for expr, elem := range vars { + trapRegex := regexp.MustCompile(regexExpression(expr)) + copy = trapRegex.ReplaceAll(copy, []byte(elem)) + } + return copy +} + +func (r *Resource) ApplyResolvedVariables() error { + vars := make(map[string]string) + for _, variable := range r.Variables { + if variable.ResolvedValue != nil { + switch v := variable.ResolvedValue.(type) { + case string: + vars[variable.Expression] = v + case types.String: + vars[variable.Expression] = v.Value().(string) + case int, int64, int32, int16, int8: + vars[variable.Expression] = fmt.Sprintf("%d", v) + case types.Int: + vars[variable.Expression] = fmt.Sprintf("%d", v.Value().(int)) + case float64, float32: + vars[variable.Expression] = fmt.Sprintf("%f", v) + case types.Double: + vars[variable.Expression] = fmt.Sprintf("%f", v.Value().(float64)) + case bool: + vars[variable.Expression] = fmt.Sprintf("%t", v) + case types.Bool: + vars[variable.Expression] = fmt.Sprintf("%t", v.Value().(bool)) + case types.Null: + vars[variable.Expression] = "" + default: + return fmt.Errorf("unknown variable type: %v: type=%v", variable.ResolvedValue, v) + } + vv := vars[variable.Expression] + if strings.Contains(vv, "\"") && strings.Contains(vv, "{") { + // This is a json string, so we need to wrap it with single quotes and + // escape the double quotes. + vv = strings.ReplaceAll(vv, "\"", "\\\"") + if !strings.HasPrefix(vv, "'") && !strings.HasSuffix(vv, "'") { + // vv = "'" + vv + "'" + } + vars[variable.Expression] = vv + } + } + } + + r.Raw = r.replaceVariables(vars) + var newData map[string]interface{} + err := yaml.Unmarshal(r.Raw, &newData) + if err != nil { + return err + } + r.Data = newData + + return nil +} diff --git a/expanders/cel-expander/pkg/resource/variables.go b/expanders/cel-expander/pkg/resource/variables.go new file mode 100644 index 0000000..8ac4387 --- /dev/null +++ b/expanders/cel-expander/pkg/resource/variables.go @@ -0,0 +1,55 @@ +// Copyright 2024 Google LLC +// +// 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 resource + +import ( + "fmt" + "regexp" + "strings" +) + +type Variable struct { + Expression string + CELExpression string + ResolvedValue interface{} +} + +var ( + referencesRegex = regexp.MustCompile(`\$\{[a-zA-Z0-9\.\-]*\}`) +) + +func extractVariables(raw []byte) ([]*Variable, error) { + matches := referencesRegex.FindAll(raw, -1) + variables := make([]*Variable, len(matches)) + for i, match := range matches { + variables[i] = &Variable{Expression: string(match), CELExpression: trimReferenceSyntax(string(match))} + } + return variables, nil +} + +func trimReferenceSyntax(reference string) string { + if !strings.HasPrefix(reference, "${") && !strings.HasSuffix(reference, "}") { + return reference + } + reference = strings.TrimLeft(reference, "${") + reference = strings.TrimRight(reference, "}") + return reference +} + +func regexExpression(expression string) string { + expression = trimReferenceSyntax(expression) + expression = fmt.Sprintf(`\$\{%s\}`, expression) + return strings.ReplaceAll(expression, ".", `\.`) +} diff --git a/expanders/cel-expander/release/manifest.yaml b/expanders/cel-expander/release/manifest.yaml new file mode 100644 index 0000000..bd73141 --- /dev/null +++ b/expanders/cel-expander/release/manifest.yaml @@ -0,0 +1,163 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: celconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: CELConfiguration + listKind: CELConfigurationList + plural: celconfigurations + singular: celconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CELConfiguration is the Schema for the celconfigurations API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: CELConfigurationSpec defines the desired state of CELConfiguration + properties: + resources: + items: + properties: + definition: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + template: + type: string + required: + - definition + - name + - template + type: object + type: array + required: + - resources + type: object + status: + description: CELConfigurationStatus defines the observed state of CELConfiguration + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: cel-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + control-plane: expander-cel + name: composition-cel-v0-0-1 + namespace: composition-system +spec: + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-cel-v0.0.1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: cel-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: composition + control-plane: expander-cel-v0.0.1 + name: composition-cel-v0.0.1 + namespace: composition-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: expander-cel-v0.0.1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-cel-v0.0.1 + spec: + containers: + - args: + - --port=8443 + command: + - /expander + image: gcr.io/krmapihosting-release/expander-cel:v0.0.1 + name: cel + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + terminationGracePeriodSeconds: 10 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: composition-cel + namespace: composition-system +spec: + config: + group: composition.google.com + kind: CELConfiguration + version: v1alpha1 + type: grpc + validVersions: + - v0.0.1 diff --git a/expanders/helm-expander/.gitignore b/expanders/helm-expander/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/expanders/helm-expander/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/expanders/helm-expander/Dockerfile b/expanders/helm-expander/Dockerfile new file mode 100644 index 0000000..ef69a06 --- /dev/null +++ b/expanders/helm-expander/Dockerfile @@ -0,0 +1,62 @@ +# Copyright 2024 Google LLC +# +# 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. + + +# syntax=docker/dockerfile:1 + +# ----------------- Build Container --------------------------- +# Build the go app. +# Explicitly set to latest vs golang:1.22 +FROM golang:1.23.2 AS build-stage + +# Set destination for COPY +WORKDIR /go/src/app + +# Download Go modules +# https://docs.docker.com/reference/dockerfile/#copy +COPY ./ ./ +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -v -o expander main.go + + +FROM golang:1.23.2 as bins + +WORKDIR /workspace + +# Version string to embed in built binary. +ARG HELM_VERSION=v3.9.0 +ARG KUSTOMIZE_VERSION=v4.5.2 + +# Install Helm +RUN wget https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz -O /tmp/helm-${HELM_VERSION}-linux-amd64.tar.gz && \ + tar -zxvf /tmp/helm-${HELM_VERSION}-linux-amd64.tar.gz -C /tmp && \ + mv /tmp/linux-amd64/helm /usr/local/bin/helm && \ + rm -rf /tmp/linux-amd64 /tmp/helm-${HELM_VERSION}-linux-amd64.tar.gz + +#ARG VERSION +# -ldflags "-X kpt.dev/configsync/pkg/version.VERSION=${VERSION}" \ + +FROM gcr.io/distroless/static:latest as helm-sync +# Setting HOME ensures that whatever UID this ultimately runs as can write files. +ENV HOME=/tmp +WORKDIR / +COPY --from=bins /usr/local/bin/helm /usr/local/bin/helm +COPY --from=build-stage /go/src/app/expander . + +ENTRYPOINT ["helm"] + +# Switch to non-root user +USER 1000 \ No newline at end of file diff --git a/expanders/helm-expander/Makefile b/expanders/helm-expander/Makefile new file mode 100644 index 0000000..b341b94 --- /dev/null +++ b/expanders/helm-expander/Makefile @@ -0,0 +1,173 @@ +GCP_PROJECT_ID ?= $(shell gcloud config get-value project) + +# Image URLs to use for building/pushing image targets +GIT_IMG_VERSION ?= $(shell git rev-parse --short HEAD) +IMG_VERSION ?= v0.0.1 +IMG_REGISTRY ?= gcr.io/$(GCP_PROJECT_ID) +EXPANDER_IMG ?= $(IMG_REGISTRY)/expander-helm:$(IMG_VERSION) +EXPANDER_BINARY ?= helm +EXPANDER_SERVICE ?= composition-helm-v0-0-1 +KIND_CLUSTER ?= kind +GOPREFIX ?= GOWORK=off + +.PHONY: all +all: build + +.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) + +##@ Helm Expander CRD and manifests + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(GOPREFIX) $(CONTROLLER_GEN) crd paths="./api/..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen + $(GOPREFIX) $(CONTROLLER_GEN) object paths="./api/..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + $(GOPREFIX) go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + $(GOPREFIX) go vet ./... + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter & yamllint + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/expanders && $(KUSTOMIZE) edit set image expander-helm=${EXPANDER_IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + cd config/expanders && $(KUSTOMIZE) edit set image expander-helm=expander-helm:latest + +.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 - + + +.PHONY: release-manifests +release-manifests: manifests kustomize + $(KUSTOMIZE) build config/release -o release/manifest.yaml + +##@ Tooling +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 +GOLANGCI_LINT_VERSION ?= v1.54.2 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) + + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary (ideally with version) +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f $(1) ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +GOBIN=$(LOCALBIN) $(GOPREFIX) go install $${package} ;\ +mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ +} +endef + + +###### ----------- Helm Expander ---------------------------- + +##@ Helm expander pod + +.PHONY: build +build: generate + $(GOPREFIX) go build -v -o bin/${EXPANDER_BINARY} ./ + +.PHONY: clean +clean: ## clean binary. + rm -fr bin/${EXPANDER_BINARY} + docker rmi ${EXPANDER_IMG} . + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: build #build ## Build docker image with the manager. + docker build -t ${EXPANDER_IMG} -f Dockerfile . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${EXPANDER_IMG} + +.PHONY: docker-run +docker-run: docker-build + docker run -p 8443:8443 --entrypoint /expander ${EXPANDER_IMG} + +.PHONY: create-kind +create-kind: + kind delete clusters ${KIND_CLUSTER} || true + kind create cluster --name ${KIND_CLUSTER} + +.PHONY: deploy-kind +deploy-kind: kustomize manifests docker-build + kind load docker-image ${EXPANDER_IMG} --name ${KIND_CLUSTER} + # this is ok to fail for existing kind cluster + $(KUBECTL) --context kind-${KIND_CLUSTER} create namespace composition-system || true + $(KUSTOMIZE) build config/crd | $(KUBECTL) --context kind-${KIND_CLUSTER} apply -f - + sleep 5 # for CRDs to be registered + cd config/expanders && $(KUSTOMIZE) edit set image expander-helm=${EXPANDER_IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) --context kind-${KIND_CLUSTER} apply -f - + cd config/expanders && $(KUSTOMIZE) edit set image expander-helm=expander-helm:latest + # this is ok to fail for unit-test cases + $(KUSTOMIZE) build config/expander-versions | $(KUBECTL) --context kind-${KIND_CLUSTER} apply -f - || true + sleep 5 + kubectl --context kind-${KIND_CLUSTER} get pods -A + +.PHONY: unit-test +unit-test: create-kind deploy-kind + kubectl patch service -n composition-system ${EXPANDER_SERVICE} -p '{"spec":{"type":"LoadBalancer"}}' + sleep 30 + nodeip=$$(kubectl get nodes -o json | jq '.items[0].status.addresses[0].address' | xargs echo );\ + nodeport=$$(kubectl get service -n composition-system ${EXPANDER_SERVICE} -o json | jq ".spec.ports[0].nodePort");\ + echo $$nodeip:$$nodeport; \ + $(GOPREFIX) go test -v --addr=$$nodeip:$$nodeport \ No newline at end of file diff --git a/expanders/helm-expander/api/v1alpha1/groupversion_info.go b/expanders/helm-expander/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..61f6455 --- /dev/null +++ b/expanders/helm-expander/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +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 composition.google.com v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=composition.google.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "composition.google.com", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/expanders/helm-expander/api/v1alpha1/helmconfiguration_types.go b/expanders/helm-expander/api/v1alpha1/helmconfiguration_types.go new file mode 100644 index 0000000..b08fcca --- /dev/null +++ b/expanders/helm-expander/api/v1alpha1/helmconfiguration_types.go @@ -0,0 +1,66 @@ +/* +Copyright 2024. + +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 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +type FileContent struct { + FileName string `json:"name"` + Content runtime.RawExtension `json:"content,omitempty"` + Template string `json:"template,omitempty"` +} + +// HelmConfigurationSpec defines the desired state of HelmConfiguration +type HelmConfigurationSpec struct { + // Chart https://helm.sh/docs/topics/charts/#the-chartyaml-file + Chart runtime.RawExtension `json:"chart"` + DefaultValues runtime.RawExtension `json:"defaultValues,omitempty"` + Templates []FileContent `json:"templates"` + CRDs []FileContent `json:"crds,omitempty"` +} + +// HelmConfigurationStatus defines the observed state of HelmConfiguration +type HelmConfigurationStatus struct { +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// HelmConfiguration is the Schema for the helmconfigurations API +type HelmConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HelmConfigurationSpec `json:"spec,omitempty"` + Status HelmConfigurationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// HelmConfigurationList contains a list of HelmConfiguration +type HelmConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HelmConfiguration `json:"items"` +} + +func init() { + SchemeBuilder.Register(&HelmConfiguration{}, &HelmConfigurationList{}) +} diff --git a/expanders/helm-expander/api/v1alpha1/zz_generated.deepcopy.go b/expanders/helm-expander/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..f411857 --- /dev/null +++ b/expanders/helm-expander/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,130 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileContent) DeepCopyInto(out *FileContent) { + *out = *in + in.Content.DeepCopyInto(&out.Content) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileContent. +func (in *FileContent) DeepCopy() *FileContent { + if in == nil { + return nil + } + out := new(FileContent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmConfiguration) DeepCopyInto(out *HelmConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmConfiguration. +func (in *HelmConfiguration) DeepCopy() *HelmConfiguration { + if in == nil { + return nil + } + out := new(HelmConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HelmConfiguration) 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 *HelmConfigurationList) DeepCopyInto(out *HelmConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HelmConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmConfigurationList. +func (in *HelmConfigurationList) DeepCopy() *HelmConfigurationList { + if in == nil { + return nil + } + out := new(HelmConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HelmConfigurationList) 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 *HelmConfigurationSpec) DeepCopyInto(out *HelmConfigurationSpec) { + *out = *in + in.Chart.DeepCopyInto(&out.Chart) + in.DefaultValues.DeepCopyInto(&out.DefaultValues) + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]FileContent, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CRDs != nil { + in, out := &in.CRDs, &out.CRDs + *out = make([]FileContent, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmConfigurationSpec. +func (in *HelmConfigurationSpec) DeepCopy() *HelmConfigurationSpec { + if in == nil { + return nil + } + out := new(HelmConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmConfigurationStatus) DeepCopyInto(out *HelmConfigurationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmConfigurationStatus. +func (in *HelmConfigurationStatus) DeepCopy() *HelmConfigurationStatus { + if in == nil { + return nil + } + out := new(HelmConfigurationStatus) + in.DeepCopyInto(out) + return out +} diff --git a/expanders/helm-expander/config/crd/bases/composition.google.com_helmconfigurations.yaml b/expanders/helm-expander/config/crd/bases/composition.google.com_helmconfigurations.yaml new file mode 100644 index 0000000..fb6bbab --- /dev/null +++ b/expanders/helm-expander/config/crd/bases/composition.google.com_helmconfigurations.yaml @@ -0,0 +1,102 @@ +# Copyright 2024 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: helmconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: HelmConfiguration + listKind: HelmConfigurationList + plural: helmconfigurations + singular: helmconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: HelmConfiguration is the Schema for the helmconfigurations API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HelmConfigurationSpec defines the desired state of HelmConfiguration + properties: + chart: + description: Chart https://helm.sh/docs/topics/charts/#the-chartyaml-file + type: object + x-kubernetes-preserve-unknown-fields: true + crds: + items: + properties: + content: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + template: + type: string + required: + - name + type: object + type: array + defaultValues: + type: object + x-kubernetes-preserve-unknown-fields: true + templates: + items: + properties: + content: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + template: + type: string + required: + - name + type: object + type: array + required: + - chart + - templates + type: object + status: + description: HelmConfigurationStatus defines the observed state of HelmConfiguration + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/expanders/helm-expander/config/crd/kustomization.yaml b/expanders/helm-expander/config/crd/kustomization.yaml new file mode 100644 index 0000000..75a339d --- /dev/null +++ b/expanders/helm-expander/config/crd/kustomization.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# 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. + +# 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/composition.google.com_helmconfigurations.yaml \ No newline at end of file diff --git a/expanders/helm-expander/config/default/kustomization.yaml b/expanders/helm-expander/config/default/kustomization.yaml new file mode 100644 index 0000000..3e48019 --- /dev/null +++ b/expanders/helm-expander/config/default/kustomization.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Adds namespace to all resources. +namespace: composition-system +namePrefix: composition- +resources: +- ../crd +- ../expanders + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/expanders/helm-expander/config/expander-versions/expander_version.yaml b/expanders/helm-expander/config/expander-versions/expander_version.yaml new file mode 100644 index 0000000..6c4b6f6 --- /dev/null +++ b/expanders/helm-expander/config/expander-versions/expander_version.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: helm + namespace: system +spec: + config: + group: composition.google.com + kind: HelmConfiguration + version: v1alpha1 + type: grpc + validVersions: + - v0.0.1 \ No newline at end of file diff --git a/expanders/helm-expander/config/expander-versions/kustomization.yaml b/expanders/helm-expander/config/expander-versions/kustomization.yaml new file mode 100644 index 0000000..d5b4275 --- /dev/null +++ b/expanders/helm-expander/config/expander-versions/kustomization.yaml @@ -0,0 +1,18 @@ +# Copyright 2024 Google LLC +# +# 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. + +resources: +- expander_version.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/expanders/helm-expander/config/expanders/helm-v0.0.1.yaml b/expanders/helm-expander/config/expanders/helm-v0.0.1.yaml new file mode 100644 index 0000000..32d2713 --- /dev/null +++ b/expanders/helm-expander/config/expanders/helm-v0.0.1.yaml @@ -0,0 +1,132 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helm-v0.0.1 + namespace: system + labels: + control-plane: expander-helm-v0.0.1 + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: helm-v0.0.1 + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/part-of: composition + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: expander-helm-v0.0.1 + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-helm-v0.0.1 + spec: + # (user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + #securityContext: + # runAsNonRoot: true + # (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: + - /expander + args: + - --port=8443 + image: expander-helm:v0.0.1 + #imagePullPolicy: Always + name: helm + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + # (user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + #serviceAccountName: helm-expander + terminationGracePeriodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: expander-helm + app.kubernetes.io/created-by: composition + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + app.kubernetes.io/instance: helm-v0.0.1 + app.kubernetes.io/component: expanders + name: helm-v0-0-1 + namespace: system +spec: + # type: LoadBalancer + # Useful when testing the expander pod inside a kind cluster + # + # kubectl get nodes -o wide + # NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME + # kind-control-plane Ready control-plane,master 22m v1.21.1 172.17.0.5 Ubuntu 21.04 6.6.15-2rodete2-amd64 containerd://1.5.2 + # ^^^^^^^^ + # kubectl get service -n composition-system composition-helm-v0-0-1 + # NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE + # composition-helm-v0-0-1 LoadBalancer 10.96.154.242 8443:30066/TCP 19m + # ^^^^ + # + # run tests: + # cd expanders/helm + # go test -v --addr=172.17.0.5:30066 + # + + #type: LoadBalancer + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-helm-v0.0.1 \ No newline at end of file diff --git a/expanders/helm-expander/config/expanders/kustomization.yaml b/expanders/helm-expander/config/expanders/kustomization.yaml new file mode 100644 index 0000000..eda685a --- /dev/null +++ b/expanders/helm-expander/config/expanders/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# 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. + +resources: +- helm-v0.0.1.yaml +images: +- name: expander-helm + newName: expander-helm + newTag: latest +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/expanders/helm-expander/config/release/kustomization.yaml b/expanders/helm-expander/config/release/kustomization.yaml new file mode 100644 index 0000000..819b2b2 --- /dev/null +++ b/expanders/helm-expander/config/release/kustomization.yaml @@ -0,0 +1,31 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Adds namespace to all resources. +namespace: composition-system +namePrefix: composition- +resources: +- ../crd +- ../expanders +- ../expander-versions + +patches: +- patch: '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", + "value": "gcr.io/krmapihosting-release/expander-helm:v0.0.1"}]' + target: + kind: Deployment + name: helm-v0.0.1 + namespace: system +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization \ No newline at end of file diff --git a/expanders/helm-expander/go.mod b/expanders/helm-expander/go.mod new file mode 100644 index 0000000..7515fa8 --- /dev/null +++ b/expanders/helm-expander/go.mod @@ -0,0 +1,34 @@ +module github.com/cloud-native-compositions/compositions/expander/helm-expander + +go 1.22.4 + +toolchain go1.23.2 + +require ( + github.com/cloud-native-compositions/compositions/composition v0.0.0-20240701212443-eb03df209286 + google.golang.org/grpc v1.64.1 + k8s.io/apimachinery v0.29.1 + sigs.k8s.io/controller-runtime v0.17.2 + sigs.k8s.io/yaml v1.4.0 + tailscale.com v1.62.0 +) + +require ( + github.com/go-logr/logr v1.4.1 // 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.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/expanders/helm-expander/go.sum b/expanders/helm-expander/go.sum new file mode 100644 index 0000000..adea741 --- /dev/null +++ b/expanders/helm-expander/go.sum @@ -0,0 +1,108 @@ +github.com/cloud-native-compositions/compositions/composition v0.0.0-20240701212443-eb03df209286 h1:U8TWXJeAegVv/kzPj2rAXWlxQbsWkhWSfKXWlJvuaUQ= +github.com/cloud-native-compositions/compositions/composition v0.0.0-20240701212443-eb03df209286/go.mod h1:b+qvSKhDpHuIDqWavpFEBsFW/1VjjFgjVqlD+KgGOHk= +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.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/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-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +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/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +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/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +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= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/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= +tailscale.com v1.62.0 h1:iI1fPDNXXETMbVEatos7xSR6Bv6aCuonD7B1X3glnPE= +tailscale.com v1.62.0/go.mod h1:cC0b0vYCoSDOLufJX5J5zDUrvV3lYwOLqlt9NW8y4cY= diff --git a/expanders/helm-expander/main.go b/expanders/helm-expander/main.go new file mode 100644 index 0000000..8d6b715 --- /dev/null +++ b/expanders/helm-expander/main.go @@ -0,0 +1,406 @@ +// Copyright 2024 Google LLC +// +// 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 ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + helmconfigurationv1alpha1 "github.com/cloud-native-compositions/compositions/expander/helm-expander/api/v1alpha1" + "google.golang.org/grpc" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + "tailscale.com/atomicfile" +) + +var ( + port = flag.Int("port", 8443, "The server port") +) + +type Expander struct { + config *helmconfigurationv1alpha1.HelmConfiguration + facade *unstructured.Unstructured + context *unstructured.Unstructured + fetchedValues *map[string]interface{} + inputResource string + ctx context.Context + path string +} + +func NewExpander(ctx context.Context, req *pb.EvaluateRequest, vreq *pb.ValidateRequest) (*Expander, string, error) { + var configBytes []byte + var facadeBytes []byte + var contextBytes []byte + var fetchedValueBytes []byte + var inputResource string + + failedMessage := "" + + e := &Expander{ + ctx: ctx, + } + + if req != nil { + configBytes = req.Config + facadeBytes = req.Facade + contextBytes = req.Context + fetchedValueBytes = req.Value + inputResource = req.Resource + if string(facadeBytes) == "" { + return nil, failedMessage, fmt.Errorf("Empty Facade for an Evaluate call") + } + } + if vreq != nil { + configBytes = vreq.Config + facadeBytes = vreq.Facade + contextBytes = vreq.Context + fetchedValueBytes = vreq.Value + inputResource = vreq.Resource + } + + e.inputResource = inputResource + e.config = &helmconfigurationv1alpha1.HelmConfiguration{} + + if string(configBytes) == "" { + failedMessage = fmt.Sprintf("empty Config passed") + return nil, failedMessage, nil + } + err := json.Unmarshal(configBytes, e.config) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Config into HelmConfiguration CR: %w", err) + } + + if string(contextBytes) != "" { + e.context = &unstructured.Unstructured{} + err := e.context.UnmarshalJSON(contextBytes) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Context: %w", err) + } + } + + if string(fetchedValueBytes) != "" { + e.fetchedValues = &map[string]interface{}{} + err := json.Unmarshal(fetchedValueBytes, e.fetchedValues) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Context: %w", err) + } + } + + if string(facadeBytes) != "" { + e.facade = &unstructured.Unstructured{} + err := e.facade.UnmarshalJSON(facadeBytes) + if err != nil { + return nil, failedMessage, fmt.Errorf("error unmarshalling req.Facade into Unstructured: %w", err) + } + if e.facade.GetNamespace() == "" { + //e.facade.SetNamespace("default") + failedMessage = fmt.Sprintf("missing namespace in req.Facade object") + return nil, failedMessage, nil + } + } + + return e, failedMessage, nil +} + +func (e *Expander) WriteInputsToFileSystem() error { + // Write Chart.yaml to file + yamlContent, err := yaml.JSONToYAML(e.config.Spec.Chart.Raw) + if err != nil { + return fmt.Errorf("failed to marshall chart.yaml: %w", err) + } + err = atomicfile.WriteFile(filepath.Join(e.path, "Chart.yaml"), yamlContent, 0644) + if err != nil { + return fmt.Errorf("failed to write Chart file: %w", err) + } + + // Write values.yaml to file + if len(e.config.Spec.DefaultValues.Raw) != 0 { + yamlContent, err := yaml.JSONToYAML(e.config.Spec.DefaultValues.Raw) + if err != nil { + return fmt.Errorf("failed to marshall values.yaml: %w", err) + } + err = atomicfile.WriteFile(filepath.Join(e.path, "values.yaml"), yamlContent, 0644) + if err != nil { + return fmt.Errorf("failed to write values.yaml file: %w", err) + } + } + + // Write CRDs + for _, crd := range e.config.Spec.CRDs { + yamlContent, err := yaml.JSONToYAML(crd.Content.Raw) + if err != nil { + return fmt.Errorf("failed to marshall crds/%s file to yaml: %w", crd.FileName, err) + } + err = atomicfile.WriteFile(filepath.Join(e.path, "crds", crd.FileName), yamlContent, 0644) + if err != nil { + return fmt.Errorf("failed to write crds/%s file: %w", crd.FileName, err) + } + } + + // Write template files + for _, template := range e.config.Spec.Templates { + content := []byte{} + if len(template.Content.Raw) != 0 { + yamlContent, err := yaml.JSONToYAML(template.Content.Raw) + if err != nil { + return fmt.Errorf("failed to marshall crds/%s file to yaml: %w", template.FileName, err) + } + content = yamlContent + } else if template.Template != "" { + content = []byte(template.Template) + } + err = atomicfile.WriteFile(filepath.Join(e.path, "templates", template.FileName), content, 0644) + if err != nil { + return fmt.Errorf("failed to write templates/%s file: %w", template.FileName, err) + } + } + + valuesObj := map[string]interface{}{} + + // Write Facade as values ? + if e.context != nil { + spec, found, err := unstructured.NestedMap(e.context.Object, "spec") + if err != nil { + return fmt.Errorf("failed to get .spec from Context: %w", err) + } + if !found { + return fmt.Errorf("Missing .spec from Context") + } + valuesObj["context"] = spec + } + if e.fetchedValues != nil { + valuesObj["fetched"] = *e.fetchedValues + } + if e.facade != nil { + valuesObj[e.inputResource] = e.facade.Object + } + valuesBytes, err := json.Marshal(valuesObj) + if err != nil { + return fmt.Errorf("Unable to marshall values object") + } + err = atomicfile.WriteFile(filepath.Join(e.path, "facade-values.yaml"), valuesBytes, 0644) + if err != nil { + return fmt.Errorf("failed to write facade-values.yaml file: %w", err) + } + return nil +} + +func (e *Expander) Validate(result *pb.ValidateResult) (*pb.ValidateResult, error) { + // https://helm.sh/docs/helm/helm_lint/ + // Usage: + // helm lint PATH [flags] + // + args := []string{"lint", e.path} + if e.facade != nil { + facadeFile := filepath.Join(e.path, "facade-values.yaml") + args = append(args, "-f", facadeFile) + } + + op, err := exec.Command("helm", args...).CombinedOutput() + if err != nil { + result.Error.Message = fmt.Sprintf("failed validating template:\n %s", string(op)) + result.Status = pb.Status_VALIDATE_FAILED + log.Print(result.Error.Message) + return result, nil + } + + //log.Printf("Result:\n type: %s\n error: %s\n status: %s\n", result.Type, result.Errors, result.Status) + return result, nil +} + +func (e *Expander) Evaluate(result *pb.EvaluateResult) (*pb.EvaluateResult, error) { + // https://helm.sh/docs/helm/helm_template/ + // Usage: + // helm template [NAME] [CHART] [flags] + // + name := "unknown" + args := []string{"template", name, e.path} + if e.facade != nil { + name = e.facade.GetName() + facadeFile := filepath.Join(e.path, "facade-values.yaml") + args = append(args, "-f", facadeFile) + } + + //args = append(args, "--output-dir", filepath.Join(e.path, "rendered")) + + log.Printf("running: helm %s", strings.Join(args, " ")) + op, err := exec.Command("helm", args...).CombinedOutput() + if err != nil { + result.Error.Message = fmt.Sprintf("failed evaluating helm chart:\n %s", string(op)) + result.Status = pb.Status_EVALUATE_FAILED + log.Print(result.Error.Message) + return result, nil + } + + manifests := op + /* + // If using output-dir we need to collect the generated files + //args = append(args, "--output-dir", filepath.Join(e.path, "rendered")) + manifests := []byte{} + // parse output and read files + lines := strings.Split(string(op), "\n") + for _, line := range lines { + log.Print(line) + if strings.HasPrefix(line, "wrote ") { + file := strings.TrimPrefix(line, "wrote ") + // read file + fileContent, err := os.ReadFile(file) + if err != nil { + newerr := fmt.Errorf("failed reading expanded file: %s, %w", file, err) + log.Print(newerr) + return nil, newerr + } + manifests = append(manifests, fileContent...) + } + } + */ + result.Manifests = manifests + + //log.Printf("Result:\n type: %s\n error: %s\n status: %s\n manifests: %s\n values: %s\n", result.Type, result.Errors, result.Status, result.Manifests, result.Values) + return result, nil +} + +// ------------- GRPC Server implementation ---------------- + +// server is used to implement exander.Evaluator +type server struct { + pb.UnimplementedExpanderServer +} + +// Verify the expander config/template +func (s *server) Validate(ctx context.Context, req *pb.ValidateRequest) (*pb.ValidateResult, error) { + result := &pb.ValidateResult{ + Status: pb.Status_SUCCESS, + Error: &pb.Error{}, + } + + log.Printf("Validate called") + e, failedMessage, err := NewExpander(ctx, nil, req) + if err != nil { + return nil, err + } + if failedMessage != "" { + result.Error.Message = failedMessage + result.Status = pb.Status_VALIDATE_FAILED + return result, nil + } + + dir, err := os.MkdirTemp("", "validate") + if err != nil { + newerr := fmt.Errorf("unexpected tmp file creation failure. %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + err = os.Mkdir(filepath.Join(dir, "templates"), 0700) + if err != nil { + newerr := fmt.Errorf("%s/templates dir creation failed: %w", dir, err) + log.Print(newerr.Error()) + return nil, newerr + } + err = os.Mkdir(filepath.Join(dir, "crds"), 0700) + if err != nil { + newerr := fmt.Errorf("%s/crds dir creation failed: %w", dir, err) + log.Print(newerr.Error()) + return nil, newerr + } + // cleanup + defer os.RemoveAll(dir) + + e.path = dir + + if err = e.WriteInputsToFileSystem(); err != nil { + newerr := fmt.Errorf("error processing inputs: %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + + return e.Validate(result) +} + +// Evaluate the expander config in context of inputs and return manifests +func (s *server) Evaluate(ctx context.Context, req *pb.EvaluateRequest) (*pb.EvaluateResult, error) { + result := &pb.EvaluateResult{ + Status: pb.Status_SUCCESS, + Type: pb.ResultType_MANIFESTS, + Error: &pb.Error{}, + } + + log.Printf("Evaluate called") + e, failedMessage, err := NewExpander(ctx, req, nil) + if err != nil { + return nil, err + } + if failedMessage != "" { + result.Error.Message = failedMessage + result.Status = pb.Status_EVALUATE_FAILED + return result, nil + } + + dir, err := os.MkdirTemp("", "eval") + if err != nil { + newerr := fmt.Errorf("tmp eval dir creation failure. %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + err = os.Mkdir(filepath.Join(dir, "templates"), 0700) + if err != nil { + newerr := fmt.Errorf("%s/templates dir creation failed: %w", dir, err) + log.Print(newerr.Error()) + return nil, newerr + } + err = os.Mkdir(filepath.Join(dir, "crds"), 0700) + if err != nil { + newerr := fmt.Errorf("%s/crds dir creation failed: %w", dir, err) + log.Print(newerr.Error()) + return nil, newerr + } + // cleanup + defer os.RemoveAll(dir) + + e.path = dir + + if err = e.WriteInputsToFileSystem(); err != nil { + newerr := fmt.Errorf("error processing inputs: %w", err) + log.Print(newerr.Error()) + return nil, newerr + } + + return e.Evaluate(result) +} + +// -------------------- MAIN ------------------------------- + +func main() { + flag.Parse() + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterExpanderServer(s, &server{}) + log.Printf("server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/expanders/helm-expander/main_test.go b/expanders/helm-expander/main_test.go new file mode 100644 index 0000000..a63872e --- /dev/null +++ b/expanders/helm-expander/main_test.go @@ -0,0 +1,703 @@ +// Copyright 2024 Google LLC +// +// 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_test + +import ( + "context" + "flag" + "log" + "math/rand" + "os" + "strings" + "testing" + "time" + + pb "github.com/cloud-native-compositions/compositions/composition/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "sigs.k8s.io/yaml" +) + +var ( + addr = flag.String("addr", "[::]:8443", "the address to connect to") + + // Empty Chart with just chart.yaml + emptyChart = `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + chart: + apiVersion: v2 + name: hello-world + description: sample chart + version: 0.1.0 +` + emptyChartRenderedManifets = "\n" + + // Simple chart with a static template file (no templating) + staticSimpleChart = `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + chart: + apiVersion: v2 + name: hello-world + description: sample chart + version: 0.1.0 + templates: + - name: configmap.yaml + content: + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: "3" + bar: "interface" +` + staticSimpleChartRenderedManifets = `--- +# Source: hello-world/templates/configmap.yaml +apiVersion: v1 +data: + bar: interface + foo: "3" +kind: ConfigMap +metadata: + name: demo +` + + simpleChart = `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + chart: + apiVersion: v2 + name: hello-world + description: sample chart + version: 0.1.0 + templates: + - name: configmap.yaml + content: + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: "{{ .Values.sqls.spec.foo }}" + car: "{{ .Values.sqls.spec.car }}" +` + simpleChartRenderedManifets = `--- +# Source: hello-world/templates/configmap.yaml +apiVersion: v1 +data: + car: 'sedan' + foo: 'bar' +kind: ConfigMap +metadata: + name: demo +` + + simpleTemplateChart = `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + chart: + apiVersion: v2 + name: hello-world + description: sample chart + version: 0.1.0 + templates: + - name: configmap.yaml + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: "{{ .Values.sqls.spec.foo }}" + car: "{{ .Values.sqls.spec.car }}" + {{ if eq .Values.sqls.spec.car "sedan" }}trunk: "true"{{ end }} +` + simpleTemplateChartRenderedManifets = `--- +# Source: hello-world/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: demo +data: + foo: "bar" + car: "sedan" + trunk: "true" +` + + simpleTemplateChartUsingFetched = `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + chart: + apiVersion: v2 + name: hello-world + description: sample chart + version: 0.1.0 + templates: + - name: configmap.yaml + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo + data: + foo: "{{ .Values.sqls.spec.foo }}" + car: "{{ .Values.fetched.car }}" + {{ if eq .Values.fetched.car "sedan" }}trunk: "true"{{ end }} +` +) + +var expanderClient pb.ExpanderClient + +func dummyValues(t *testing.T) []byte { + y := `foo: bar +car: sedan +` + j, err := yaml.YAMLToJSON([]byte(y)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, y) + } + return j +} + +func configFrom(t *testing.T, config string) []byte { + j, err := yaml.YAMLToJSON([]byte(config)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, config) + } + return j +} + +func testFacade(t *testing.T, facade string) []byte { + if facade == "" { + facade = `apiVersion: facade.foobar.com/v1alpha1 +kind: Foo +metadata: + name: appteam-sample + namespace: default +spec: + foo: bar + car: sedan +` + } + j, err := yaml.YAMLToJSON([]byte(facade)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, facade) + } + return j +} + +func testContext(t *testing.T) []byte { + y := `apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: config-control +spec: + project: test-project +` + j, err := yaml.YAMLToJSON([]byte(y)) + if err != nil { + t.Fatalf("error marshalling to json: %v\n %s", err, y) + } + return j +} + +// TestMain - umbrella test that runs all test cases +func TestMain(m *testing.M) { + flag.Parse() + + rand.Seed(time.Now().UnixNano()) + flag.Parse() + // Set up a connection to the server. + conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + expanderClient = pb.NewExpanderClient(conn) + + exitCode := m.Run() + os.Exit(exitCode) +} + +func TestEvaluateEmptyConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err != nil { + t.Fatalf("could not evaluate: %v", err) + } + if r.GetStatus() != pb.Status_EVALUATE_FAILED { + t.Fatalf("want FAILURE, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) +} + +func TestEvaluateBadConfig(t *testing.T) { + config := `dummy config` + _, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, config), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err == nil { + t.Fatalf("expected error. got none") + } + + errMessage := "cannot unmarshal string into Go value of type v1alpha1.HelmConfiguration" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestEvaluateNoChartConfig(t *testing.T) { + config := `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + ignore: this +` + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, config), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_EVALUATE_FAILED { + t.Fatalf("want FAILURE, got: %s", r.GetStatus()) + } + expectedErrorString := "validation: chart.metadata.name is required" + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error: %s \n got: %s", expectedErrorString, r) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) +} + +func TestEvaluateNoTemplateConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != emptyChartRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", emptyChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateEmptyContext(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyChart), + Context: []byte{}, + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if string(r.Manifests) != emptyChartRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", emptyChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateEmptyFacade(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyChart), + Context: testContext(t), + Facade: []byte{}, + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err == nil { + t.Fatalf("expected error. got none") + } + + errMessage := "Empty Facade for an Evaluate call" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestEvaluateEmptyValue(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: []byte{}, + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("expected no error. got: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if string(r.Manifests) != emptyChartRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", emptyChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateStaticConfig(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, staticSimpleChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != staticSimpleChartRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", staticSimpleChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateUsingFacade(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != simpleChartRenderedManifets { + t.Fatalf("\nexpected: %s\n got: %s", simpleChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateTemplateUsingFacade(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleTemplateChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } + if string(r.Manifests) != simpleTemplateChartRenderedManifets { + t.Fatalf("\nexpected: %s\ngot: %s", simpleTemplateChartRenderedManifets, r.Manifests) + } +} + +func TestEvaluateTemplateMissingFacadeField(t *testing.T) { + facade := `apiVersion: facade.foobar.com/v1alpha1 +kind: Foo +metadata: + name: appteam-sample + namespace: default +spec: + # Missing .Values.spec.car + foo: bar + #car: sedan +` + renderedManifests := `--- +# Source: hello-world/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: demo +data: + foo: "bar" + car: "" +` + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleTemplateChart), + Context: testContext(t), + Facade: testFacade(t, facade), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS , got: %s", r.GetStatus()) + } + + if string(r.Manifests) != renderedManifests { + t.Fatalf("\nexpected: %s\ngot: %s", renderedManifests, r.Manifests) + } +} + +func TestEvaluateTemplateUsesValues(t *testing.T) { + r, err := expanderClient.Evaluate(context.Background(), + &pb.EvaluateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleTemplateChartUsingFetched), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS , got: %s", r.GetStatus()) + } + + if string(r.Manifests) != simpleTemplateChartRenderedManifets { + t.Fatalf("\nexpected: %s\ngot: %s", simpleTemplateChartRenderedManifets, r.Manifests) + } +} + +// -------------------------------------------------------- +// ----------------- Validate Tests ----------------------- +// -------------------------------------------------------- + +func TestValidateEmptyConfig(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: []byte{}, + Resource: "sqls", + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_VALIDATE_FAILED { + t.Fatalf("want FAILURE, got: %s", r.GetStatus()) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) +} + +func TestValidateEmptyContext(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: configFrom(t, emptyChart), + Resource: "sqls", + Context: []byte{}, + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateEmptyFacade(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: configFrom(t, emptyChart), + Resource: "sqls", + Context: testContext(t), + Facade: []byte{}, + Value: dummyValues(t), + }) + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateEmptyValue(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Config: configFrom(t, emptyChart), + Resource: "sqls", + Context: testContext(t), + Facade: testFacade(t, ""), + Value: []byte{}, + }) + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) + if err != nil { + t.Fatalf("could not validate: %v", err) + } + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateTemplateUsingFacade(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Resource: "sqls", + Config: configFrom(t, simpleTemplateChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} + +func TestValidateBadConfig(t *testing.T) { + config := `dummy config` + _, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Resource: "sqls", + Config: configFrom(t, config), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err == nil { + t.Fatalf("expected error. got none") + } + + errMessage := "cannot unmarshal string into Go value of type v1alpha1.HelmConfiguration" + if !strings.Contains(err.Error(), errMessage) { + t.Fatalf("Did not find expected string in err: %s, got: %s", errMessage, err.Error()) + } +} + +func TestValidateNoChartConfig(t *testing.T) { + config := `apiVersion: composition.google.com/v1alpha1 +kind: HelmConfiguration +metadata: + name: foobar + namespace: config-control +spec: + ignore: this +` + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Resource: "sqls", + Config: configFrom(t, config), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_VALIDATE_FAILED { + t.Fatalf("want FAILURE, got: %s", r.GetStatus()) + } + expectedErrorString := "validation: chart.metadata.name is required" + if !strings.Contains(r.Error.Message, expectedErrorString) { + t.Fatalf("expected error: %s \n got: %s", expectedErrorString, r) + } + t.Logf("status returned: %s, %s", r.GetStatus(), r.GetError()) +} + +func TestValidateNoTemplateConfig(t *testing.T) { + r, err := expanderClient.Validate(context.Background(), + &pb.ValidateRequest{ + Resource: "sqls", + Config: configFrom(t, emptyChart), + Context: testContext(t), + Facade: testFacade(t, ""), + Value: dummyValues(t), + }) + t.Logf("status returned: %s", r) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if r.GetStatus() != pb.Status_SUCCESS { + t.Fatalf("want SUCCESS, got: %s", r.GetStatus()) + } +} diff --git a/expanders/helm-expander/release/manifest.yaml b/expanders/helm-expander/release/manifest.yaml new file mode 100644 index 0000000..016c1b6 --- /dev/null +++ b/expanders/helm-expander/release/manifest.yaml @@ -0,0 +1,183 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: helmconfigurations.composition.google.com +spec: + group: composition.google.com + names: + kind: HelmConfiguration + listKind: HelmConfigurationList + plural: helmconfigurations + singular: helmconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: HelmConfiguration is the Schema for the helmconfigurations API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HelmConfigurationSpec defines the desired state of HelmConfiguration + properties: + chart: + description: Chart https://helm.sh/docs/topics/charts/#the-chartyaml-file + type: object + x-kubernetes-preserve-unknown-fields: true + crds: + items: + properties: + content: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + template: + type: string + required: + - name + type: object + type: array + defaultValues: + type: object + x-kubernetes-preserve-unknown-fields: true + templates: + items: + properties: + content: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + template: + type: string + required: + - name + type: object + type: array + required: + - chart + - templates + type: object + status: + description: HelmConfigurationStatus defines the observed state of HelmConfiguration + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: helm-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: composition + control-plane: expander-helm + name: composition-helm-v0-0-1 + namespace: composition-system +spec: + ports: + - name: grpc + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: expander-helm-v0.0.1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: expanders + app.kubernetes.io/created-by: composition + app.kubernetes.io/instance: helm-v0.0.1 + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: composition + control-plane: expander-helm-v0.0.1 + name: composition-helm-v0.0.1 + namespace: composition-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: expander-helm-v0.0.1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: expander + labels: + control-plane: expander-helm-v0.0.1 + spec: + containers: + - args: + - --port=8443 + command: + - /expander + image: gcr.io/krmapihosting-release/expander-helm:v0.0.1 + name: helm + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + terminationGracePeriodSeconds: 10 +--- +apiVersion: composition.google.com/v1alpha1 +kind: ExpanderVersion +metadata: + name: composition-helm + namespace: composition-system +spec: + config: + group: composition.google.com + kind: HelmConfiguration + version: v1alpha1 + type: grpc + validVersions: + - v0.0.1 diff --git a/samples/AppTeam/README.md b/samples/AppTeam/README.md new file mode 100644 index 0000000..cbad05e --- /dev/null +++ b/samples/AppTeam/README.md @@ -0,0 +1,69 @@ +# AppTeam + +For now this will only work in CC from a specific project. + +## [Platform Admin] Create a Context object + +The first step is to create a context object in the namespace where AppTeam will +be created. + +``` +kubectl apply -f - < +EOF +``` + +## [Platform Admin] Create the composition + +``` +kubectl create -f composition/appteam.yaml +``` + +## [Platform Admin] Create a new team + +Create a new `AppTeam` CR in the `config-control` namespace. + +``` +# Important to avoid collision +TEAM_NAME=team-$(tr -dc a-z " + exit 1 +fi + +project=$1 + +kubectl delete appteams.facade.facade -n config-control ${project} +kubectl delete context.composition.google.com context -n ${project} + +# These are redundant +kubectl delete iamserviceaccount.iam.cnrm.cloud.google.com kcc-${project} -n config-control +kubectl delete iampartialpolicy.iam.cnrm.cloud.google.com ${project}-sa-workload-identity-binding -n config-control +kubectl delete iampartialpolicy.iam.cnrm.cloud.google.com kcc-owners-permissions-${project} -n config-control +kubectl delete storagebuckets.storage.cnrm.cloud.google.com test-bucket-${project} -n ${project} +kubectl delete project.resourcemanager.cnrm.cloud.google.com ${project} -n config-control + +echo "waiting for project to be deleted ......" +sleep 30 + +kubectl delete configconnectorcontext.core.cnrm.cloud.google.com configconnectorcontext.core.cnrm.cloud.google.com -n ${project} +kubectl delete namespace ${project} diff --git a/samples/AppTeam/composition/appteam.yaml b/samples/AppTeam/composition/appteam.yaml new file mode 100644 index 0000000..0041b47 --- /dev/null +++ b/samples/AppTeam/composition/appteam.yaml @@ -0,0 +1,216 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: appteams + namespace: default +spec: + inputAPIGroup: appteams.idp.mycompany.com + namespaceMode: explicit + expanders: + - type: jinja2 + version: v0.0.1 + name: project + template: | + {% set managedProject = appteams.spec.project %} + # https://github.com/GoogleCloudPlatform/blueprints/tree/main/catalog/project + apiVersion: resourcemanager.cnrm.cloud.google.com/v1beta1 + kind: Project + metadata: + name: {{ managedProject }} + namespace: {{ appteams.metadata.namespace }} + #annotations: + # cnrm.cloud.google.com/auto-create-network: "false" + labels: + createdby: "composition-appteam" + spec: + name: {{ managedProject }} + billingAccountRef: + external: "{{ appteams.spec.billingAccount }}" + folderRef: + external: "{{ appteams.spec.folder }}" + - type: jinja2 + name: namespace + version: v0.0.1 + template: | + {% set managedProject = appteams.spec.project %} + {% set namespace = appteams.spec.project %} + # Create namespace in the Kubernetes cluster to manage KCC resources in this project + apiVersion: v1 + kind: Namespace + metadata: + name: {{ namespace }} + annotations: + cnrm.cloud.google.com/project-id: {{ managedProject }} + - type: jinja2 + name: setup-kcc + version: v0.0.1 + template: | + {% set hostProject = context.spec.project %} + {% set managedProject = appteams.spec.project %} + {% set namespace = appteams.spec.project %} + # Enable KCC for this namespace + apiVersion: core.cnrm.cloud.google.com/v1beta1 + kind: ConfigConnectorContext + metadata: + name: configconnectorcontext.core.cnrm.cloud.google.com + namespace: {{ namespace }} + spec: + googleServiceAccount: kcc-{{ managedProject }}@{{ hostProject }}.iam.gserviceaccount.com + --- + # Create GCP ServiceAccount for use by KCC to manage resources in this project + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMServiceAccount + metadata: + name: kcc-{{ managedProject }} + namespace: {{ appteams.metadata.namespace }} + #annotations: + #cnrm.cloud.google.com/project-id: {{ hostProject }} + #cnrm.cloud.google.com/ignore-clusterless: "true" + spec: + displayName: kcc-{{ managedProject }} + --- + # Allow KCC's Kubernetes Service Account to use the GCP ServiceAccount + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPartialPolicy + metadata: + name: {{ managedProject }}-sa-workload-identity-binding + namespace: {{ appteams.metadata.namespace }} + spec: + resourceRef: + name: kcc-{{ managedProject }} + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMServiceAccount + bindings: + - role: roles/iam.workloadIdentityUser + members: + - member: serviceAccount:{{ hostProject }}.svc.id.goog[cnrm-system/cnrm-controller-manager-{{ managedProject }}] + - type: jinja2 + name: project-owner + version: v0.0.1 + template: | + {% set hostProject = context.spec.project %} + {% set managedProject = appteams.spec.project %} + {% set namespace = appteams.spec.project %} + # Grant KCC owner of this project + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPartialPolicy + metadata: + name: kcc-owners-permissions-{{ managedProject }} + namespace: {{ appteams.metadata.namespace }} + #annotations: + # cnrm.cloud.google.com/project-id: {{ hostProject }} + # cnrm.cloud.google.com/ignore-clusterless: "true" + spec: + resourceRef: + apiVersion: resourcemanager.cnrm.cloud.google.com/v1beta1 + kind: Project + external: {{ managedProject }} + bindings: + - role: roles/owner + members: + - member: serviceAccount:kcc-{{ managedProject }}@{{ hostProject }}.iam.gserviceaccount.com + - member: user:{{ appteams.spec.adminUser }} + - type: jinja2 + name: bucket + version: v0.0.1 + template: | + {% set managedProject = appteams.spec.project %} + {% set namespace = appteams.spec.project %} + apiVersion: storage.cnrm.cloud.google.com/v1beta1 + kind: StorageBucket + metadata: + name: test-bucket-{{ managedProject }} + namespace: {{ namespace }} + annotations: + cnrm.cloud.google.com/project-id: {{ managedProject }} + spec: + location: us-central1 + storageClass: standard + uniformBucketLevelAccess: true + versioning: + enabled: false + - type: jinja2 + name: compositions-context + version: v0.0.1 + template: | + {% set managedProject = appteams.spec.project %} + {% set namespace = appteams.spec.project %} + apiVersion: composition.google.com/v1alpha1 + kind: Context + metadata: + name: context + namespace: {{ namespace }} + spec: + project: {{ managedProject }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: appteams.idp.mycompany.com +spec: + group: idp.mycompany.com + names: + kind: AppTeam + listKind: AppTeamList + plural: appteams + singular: appteam + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AppTeam is the Schema for the appteams API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AppTeamSpec defines the desired state of AppTeam + properties: + project: + type: string + adminUser: + type: string + billingAccount: + type: string + folder: + type: string + required: + - project + - adminUser + - billingAccount + - folder + type: object + status: + description: AppTeamStatus defines the observed state of AppTeam + type: object + type: object + served: true + storage: true + subresources: + status: {} \ No newline at end of file diff --git a/samples/AppTeam/get_appteam.sh b/samples/AppTeam/get_appteam.sh new file mode 100755 index 0000000..51f8db6 --- /dev/null +++ b/samples/AppTeam/get_appteam.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Copyright 2024 Google LLC +# +# 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. + + +scriptpath=$(realpath $0) +base=$(dirname $scriptpath) + +if [[ $# -lt 1 ]]; +then + echo "usage: ./get_appteam.sh " + exit 1 +fi + +project=$1 +opmodifier=$2 + +echo "AppTeam ----------------------------------------" +kubectl get appteam ${project} -n config-control $opmodifier +echo +echo "Composition Context --------------------------------" +kubectl get context.composition.google.com context -n ${project} $opmodifier +echo +echo "IAMServiceAccount ----------------------------------------" +kubectl get iamserviceaccount kcc-${project} -n config-control $opmodifier +echo +echo "IAMPartialPolicy --------------------------------------------" +kubectl get iampartialpolicy -n config-control ${project}-sa-workload-identity-binding $opmodifier +kubectl get iampartialpolicy -n config-control kcc-owners-permissions-${project} $opmodifier +echo +echo "StorageBuckets --------------------------------------------" +kubectl get storagebucket -n ${project} test-bucket-${project} $opmodifier +echo +echo "CCC --------------------------------------------" +kubectl get configconnectorcontext -n ${project} $opmodifier +echo +echo "Project ------------------------------------------" +kubectl get project ${project} -n config-control $opmodifier +echo diff --git a/samples/AttachedAKS/01-composition.yaml b/samples/AttachedAKS/01-composition.yaml new file mode 100644 index 0000000..199cfe2 --- /dev/null +++ b/samples/AttachedAKS/01-composition.yaml @@ -0,0 +1,217 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: attachedakses.idp.mycompany.com +spec: + conversion: + strategy: None + group: idp.mycompany.com + names: + categories: + - facade + - facades + kind: AttachedAKS + listKind: AttachedAKSList + plural: attachedakses + singular: attachedaks + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: TODO + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: AttachedAKS defines the parameters for attaching ASK cluster + to GCP. + properties: + adminUsers: + items: + type: string + type: array + attachedPlatformVersion: + type: string + azureRegion: + type: string + gcpProjectNumber: + type: string + gcpRegion: + type: string + kubernetesVersion: + type: string + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + description: human readable message + maxLength: 1024 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 256 + 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. + 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 + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: aks-issuer + namespace: default +spec: + valuesFrom: + - name: issuer + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + nameSuffix: "-aks-issuer" + fieldRef: + - path: ".data.oidc" + as: url +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: compo-aks-1 +spec: + inputAPIGroup: attachedakses.idp.mycompany.com + expanders: + - type: jinja2 + version: v0.0.1 + name: create-resource-group + template: | + apiVersion: resources.azure.com/v1api20200601 + kind: ResourceGroup + metadata: + name: {{ attachedakses.metadata.name }}-rg + namespace: {{ attachedakses.metadata.namespace }} + spec: + location: {{ attachedakses.spec.azureRegion }} + - type: jinja2 + version: v0.0.1 + name: create-aks-cluster + template: | + apiVersion: containerservice.azure.com/v1api20230201 + kind: ManagedCluster + metadata: + name: {{ attachedakses.metadata.name }}-aks + namespace: {{ attachedakses.metadata.namespace }} + spec: + location: westcentralus + owner: + name: {{ attachedakses.metadata.name }}-rg + dnsPrefix: aso + oidcIssuerProfile: + enabled: true + agentPoolProfiles: + - name: pool1 + count: 1 + vmSize: Standard_DS2_v2 + osType: Linux + mode: System + identity: + type: SystemAssigned + kubernetesVersion: "{{ attachedakses.spec.kubernetesVersion }}" + operatorSpec: + configMaps: + oidcIssuerProfile: + name: {{ attachedakses.metadata.name }}-aks-issuer + key: oidc + secrets: + adminCredentials: + name: {{ attachedakses.metadata.name }}-admin + key: adminCredentials + userCredentials: + name: {{ attachedakses.metadata.name }}-user + key: userCredentials + - type: getter + version: v0.0.1 + name: aks-issuer + template: "" + configref: + name: aks-issuer + namespace: default + - type: jinja2 + version: v0.0.1 + name: attach + template: | + apiVersion: containerattached.cnrm.cloud.google.com/v1beta1 + kind: ContainerAttachedCluster + metadata: + name: {{ attachedakses.metadata.name }}-attached + namespace: {{ attachedakses.metadata.namespace }} + spec: + resourceID: {{ attachedakses.metadata.name }} + location: {{ attachedakses.spec.gcpRegion }} + projectRef: + external: {{ context.spec.project }} + description: "Test composition with aks" + distribution: aks + oidcConfig: + issuerUrl: {{ values.issuer.url }} + platformVersion: {{ attachedakses.spec.attachedPlatformVersion }} + fleet: + projectRef: + external: "projects/{{ attachedakses.spec.gcpProjectNumber }}" + authorization: + adminUsers: {{ attachedakses.spec.adminUsers }} + diff --git a/samples/AttachedAKS/02-context.yaml b/samples/AttachedAKS/02-context.yaml new file mode 100644 index 0000000..e2575d9 --- /dev/null +++ b/samples/AttachedAKS/02-context.yaml @@ -0,0 +1,51 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Create a namespace for user team +apiVersion: v1 +kind: Namespace +metadata: + name: "${NAMESPACE}" +--- +# Config this namespace for KCC +apiVersion: core.cnrm.cloud.google.com/v1beta1 +kind: ConfigConnectorContext +metadata: + name: configconnectorcontext.core.cnrm.cloud.google.com + namespace: "${NAMESPACE}" +spec: + billingProject: "${PROJECT_ID}" + googleServiceAccount: "${TEAM_GSA_EMAIL}" + requestProjectPolicy: BILLING_PROJECT +--- +# Config this namespace for composition +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: "${NAMESPACE}" +spec: + project: "${PROJECT_ID}" +--- +# Config this namespace for ASO +apiVersion: v1 +kind: Secret +metadata: + name: aso-credential + namespace: "${NAMESPACE}" +stringData: + AZURE_SUBSCRIPTION_ID: "${AZURE_SUBSCRIPTION_ID}" + AZURE_TENANT_ID: "${AZURE_TENANT_ID}" + AZURE_CLIENT_ID: "${AZURE_CLIENT_ID}" + USE_WORKLOAD_IDENTITY_AUTH: "true" diff --git a/samples/AttachedAKS/03-attached-1.yaml b/samples/AttachedAKS/03-attached-1.yaml new file mode 100644 index 0000000..73cf3cf --- /dev/null +++ b/samples/AttachedAKS/03-attached-1.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: idp.mycompany.com/v1 +kind: AttachedAKS +metadata: + name: test-composition-aks-1 + namespace: team-aks +spec: + gcpRegion: us-west1 + kubernetesVersion: "1.28" + attachedPlatformVersion: "1.28.0-gke.2" + azureRegion: westcentralus + gcpProjectNumber: "000000000000" # change this + adminUsers: + - user@example.com # change this + diff --git a/samples/AttachedAKS/README.md b/samples/AttachedAKS/README.md new file mode 100644 index 0000000..15a2c3c --- /dev/null +++ b/samples/AttachedAKS/README.md @@ -0,0 +1,7 @@ +# Create an AKS cluster and attach it to GCP + +1. Follow the [setup-Azure-ASO.md](setup-Azure-ASO.md) guide to set up ASO. +2. Act as a platform admin and run the commands in [aks-charlie.md](aks-charlie.md) + to set up the composition and a user team. +3. Act as a user and run the commands in [aks-alice.md](aks-alice.md) to create the + attached AKS cluster. diff --git a/samples/AttachedAKS/aks-alice.md b/samples/AttachedAKS/aks-alice.md new file mode 100644 index 0000000..f8b28ae --- /dev/null +++ b/samples/AttachedAKS/aks-alice.md @@ -0,0 +1,40 @@ +## Step 1. Apply the CR to create the composition of AKS and its attachment +``` +kubectl apply -f 03-attached-1.yaml +``` + + +## Step 2. Log in to the cluster and apply the attached cluster install manifest +``` +AKS_NAME=$(cat 03-attached-1.yaml |yq .metadata.name) +ATTACHED_REGION=$(cat 03-attached-1.yaml |yq .spec.gcpRegion) +ATTACHED_PLATFORM_VERSION=$(cat 03-attached-1.yaml |yq .spec.attachedPlatformVersion) + +gcloud container attached clusters generate-install-manifest \ + $AKS_NAME \ + --location=${ATTACHED_REGION} \ + --platform-version ${ATTACHED_PLATFORM_VERSION} \ + --output-file=/tmp/install-agent-${AKS_NAME}.yaml + +az aks get-credentials --name ${AKS_NAME}-aks \ + --resource-group ${AKS_NAME}-rg + +kubectl apply -f /tmp/install-agent-${AKS_NAME}.yaml +``` + + +## Commands to check progress: +``` +kubectl get AttachedAKS -n team-aks + +kubectl get ResourceGroup.resources.azure.com \ + -n team-aks + +kubectl get managedcluster.containerservice.azure.com \ + -n team-aks + +kubectl get cm -n team-aks + +kubectl get containerattachedcluster \ + -n team-aks +``` diff --git a/samples/AttachedAKS/aks-charlie.md b/samples/AttachedAKS/aks-charlie.md new file mode 100644 index 0000000..07e783a --- /dev/null +++ b/samples/AttachedAKS/aks-charlie.md @@ -0,0 +1,105 @@ +## Apply the compostion +``` +kubectl apply -f 01-composition.yaml +``` + +## Create a GCP service account for this team and grant KCC permissions + +This step follows the following documentation: +https://cloud.google.com/config-connector/docs/how-to/install-namespaced + +``` +export NAMESPACE=team-aks +export TEAM_GCP_SA_NAME="${NAMESPACE}" +export PROJECT_ID=$(gcloud config get-value project) +export TEAM_GSA_EMAIL="${TEAM_GCP_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" +gcloud iam service-accounts create ${TEAM_GCP_SA_NAME} --project ${PROJECT_ID} +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${TEAM_GSA_EMAIL}" \ + --role="roles/owner" +gcloud iam service-accounts add-iam-policy-binding \ + ${TEAM_GSA_EMAIL} \ + --member="serviceAccount:${PROJECT_ID}.svc.id.goog[cnrm-system/cnrm-controller-manager-${NAMESPACE}]" \ + --role="roles/iam.workloadIdentityUser" \ + --project ${PROJECT_ID} +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${TEAM_GSA_EMAIL}" \ + --role="roles/monitoring.metricWriter" +WORKLOAD_IDENTITY_POOL="${PROJECT_ID}.svc.id.goog" +export ASO_NAMESPACE=azureserviceoperator-system # Don’t change +export ASO_KSA=azureserviceoperator-default # Don’t change +gcloud iam service-accounts add-iam-policy-binding ${TEAM_GSA_EMAIL} \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:${WORKLOAD_IDENTITY_POOL}[${ASO_NAMESPACE}/${ASO_KSA}]" \ + --condition None +``` + +## Grant ASO permission for the GCP service accout used by this team + +This step follows the [following documentation](setup-Azure-ASO.md#create-a-gcp-service-account) +``` +export MI_RESOURCE_GROUP="$USER"-kcc-demo-${NAMESPACE} +export MI_NAME="$USER-aso-mi" +export AZURE_SUBSCRIPTION_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +export AZURE_TENANT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +AZURE_REGION=eastus +``` + +## Create a resource group +``` +az group create -l ${AZURE_REGION} -n ${MI_RESOURCE_GROUP} +``` + +## Create an MI +``` +az identity create --name ${MI_NAME} \ + --resource-group ${MI_RESOURCE_GROUP} \ + --location ${AZURE_REGION} +``` + +## Get the MI ID +``` +export AZURE_CLIENT_ID=$(az identity show \ + --name ${MI_NAME} --resource-group ${MI_RESOURCE_GROUP} \ + --query "clientId" -otsv) +``` + +## Assign permissions to this MI + +Note: The user can use other permissions to manage their resources. +``` +az role assignment create \ + --assignee $AZURE_CLIENT_ID \ + --role contributor \ + --scope /subscriptions/$AZURE_SUBSCRIPTION_ID +``` + +## Set up impersonation + +This step allows the GCP service created +[here](setup-Azure-ASO.md#create-a-gcp-service-account) to impersonate the Azure +managed identity. + +In this example, we use the default GCP service account. + +``` +DEFAULT_GSA_EMAIL=$(kubectl get asokontroller \ + asokontroller.kontrollers.cnrm.cloud.google.com \ + -ojson | jq -r .spec.googleServiceAccount) +DEFAULT_GSA_SUB=$(gcloud iam service-accounts describe ${TEAM_GSA_EMAIL} \ + --format "value(oauth2ClientId)") + +az identity federated-credential create \ + --name gsa-azure-federated-credential \ + --identity-name ${MI_NAME} \ + --resource-group ${MI_RESOURCE_GROUP} \ + --issuer https://accounts.google.com \ + --subject ${DEFAULT_GSA_SUB} \ + --audience api://AzureADTokenExchange +``` + +## Create a namespace for Alice's team +``` +cat 02-context.yaml | envsubst | \ + kubectl apply -f - +``` diff --git a/samples/AttachedAKS/setup-Azure-ASO.md b/samples/AttachedAKS/setup-Azure-ASO.md new file mode 100644 index 0000000..f81a75e --- /dev/null +++ b/samples/AttachedAKS/setup-Azure-ASO.md @@ -0,0 +1,105 @@ +# Set up Azure Service Operator v2 (ASO) + +Note: ASO support is only availabe for GCP managed [Config +Connector](https://cloud.google.com/config-connector/docs/overview). + +Here is an overview of the ASO setup process: + +1. Create a GCP service account. +1. Create an Azure managed identity. + 1. Grant this managed identity the permissions to manage the Azure resouces. + 1. Allow the GCP service account to impersonate the Azure managed identity. +1. Create the ASO controller with the GCP service account. + +## Create a GCP service account + +``` +PROJECT_ID=$(gcloud config get-value project) +PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") + + +# Create the service account +gcloud iam service-accounts create $USER-allotrope \ + --description="Allotrope proof of concept" \ + --display-name="Allotrope POC" + +export GSA_EMAIL=$USER-allotrope@${PROJECT_ID}.iam.gserviceaccount.com + +# Get the workload identity pool for the gke/kcc cluster +WORKLOAD_IDENTITY_POOL="${PROJECT_ID}.svc.id.goog" + +# Grant workload identity bindings permissions +export ASO_NAMESPACE=kontrollers-azureserviceoperator-system # Don’t change +export ASO_KSA=azureserviceoperator-default # Don’t change +gcloud iam service-accounts add-iam-policy-binding ${GSA_EMAIL} \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:${WORKLOAD_IDENTITY_POOL}[${ASO_NAMESPACE}/${ASO_KSA}]" \ + --condition None +``` + +## Create an Azure managed identity and allow the GCP service account to +impersonate it + +``` +# Please change the parameters in this section +export MI_RESOURCE_GROUP="${USER}-kcc-demo" +export MI_NAME="${USER}-aso-mi" +AZURE_SUBSCRIPTION_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +AZURE_TENANT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +AZURE_REGION=eastus + +# Create a resource group +az group create -l ${AZURE_REGION} -n ${MI_RESOURCE_GROUP} + +# Create an MI +az identity create --name ${MI_NAME} \ + --resource-group ${MI_RESOURCE_GROUP} \ + --location ${AZURE_REGION} + +# Get the MI ID +AZURE_CLIENT_ID=$(az identity show \ + --name ${MI_NAME} --resource-group ${MI_RESOURCE_GROUP} \ + --query "clientId" -otsv) +MI_PRINCIPAL_ID=$(az identity show \ + --name ${MI_NAME} --resource-group ${MI_RESOURCE_GROUP} \ + --query "principalId" -otsv) + +# Assign the permissions to this MI. +# The user can use other permissions to manage their resources. +az role assignment create \ + --assignee ${MI_PRINCIPAL_ID} \ + --role contributor \ + --scope /subscriptions/${AZURE_SUBSCRIPTION_ID} + +# Allow the GCP service account to impersonate this MI +GSA_SUB=$(gcloud iam service-accounts describe ${GSA_EMAIL} \ + --format "value(oauth2ClientId)") +az identity federated-credential create \ + --name gsa-azure-federated-credential \ + --identity-name ${MI_NAME} \ + --resource-group ${MI_RESOURCE_GROUP} \ + --issuer https://accounts.google.com \ + --subject ${GSA_SUB} \ + --audience api://AzureADTokenExchange +``` + +## Create the ASO controller + +``` +cat < /tmp/aso.yaml +apiVersion: kontrollers.cnrm.cloud.google.com/v1alpha1 +kind: ASOKontroller +metadata: + name: asokontroller.kontrollers.cnrm.cloud.google.com +spec: + defaultAzureSubscriptionID: "${AZURE_SUBSCRIPTION_ID}" + defaultAzureTenantID: "${AZURE_TENANT_ID}" + defaultAzureClientID: "${AZURE_CLIENT_ID}" + crdPatterns: + - "resources.azure.com/resourcegroup" # Use any pattern that fits your need. + - "containerservice.azure.com/*" + googleServiceAccount: "${GSA_EMAIL}" +EOF + +kubectl apply -f /tmp/aso.yaml +``` diff --git a/samples/AttachedEKS/01-composition.yaml b/samples/AttachedEKS/01-composition.yaml new file mode 100644 index 0000000..12b8212 --- /dev/null +++ b/samples/AttachedEKS/01-composition.yaml @@ -0,0 +1,526 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: attachedekses.idp.mycompany.com +spec: + conversion: + strategy: None + group: idp.mycompany.com + names: + categories: + - facade + - facades + kind: AttachedEKS + listKind: AttachedEKSList + plural: attachedekses + singular: attachedeks + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: TODO + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: AttachedEKS defines the parameters for attaching EKS cluster + to GCP. + properties: + adminUsers: + items: + type: string + type: array + attachedPlatformVersion: + type: string + awsAccessIdentity: + type: string + awsAvailabilityZones: + items: + properties: + privateSubnet: + type: string + publicSubnet: + type: string + zoneNameSuffix: + type: string + type: object + type: array + awsRegion: + type: string + gcpProjectNumber: + type: string + gcpRegion: + type: string + kubernetesVersion: + type: string + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + description: human readable message + maxLength: 1024 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 256 + 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. + 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 + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: eks-issuer + namespace: default +spec: + valuesFrom: + - name: issuer + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + nameSuffix: "-eks-issuer" + fieldRef: + - path: ".data.oidc" + as: url +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: subnet-ids + namespace: default +spec: + valuesFrom: + - name: subnet + resourceRef: + group: "" + version: v1 + kind: ConfigMap + resource: configmaps + nameSuffix: "-subnet-ids" + fieldRef: + - path: ".data" + as: ids +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: compo-eks-1 + namespace: default +spec: + inputAPIGroup: attachedekses.idp.mycompany.com + expanders: + - type: jinja2 + version: v0.0.1 + name: create-vpc + template: | + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: VPC + metadata: + name: {{ attachedekses.metadata.name }}-vpc + namespace: {{ attachedekses.metadata.namespace }} + spec: + cidrBlocks: + - "10.0.0.0/16" + enableDNSSupport: true + enableDNSHostnames: true + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-vpc + - type: jinja2 + version: v0.0.1 + name: create-route-tables + template: | + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: InternetGateway + metadata: + name: {{ attachedekses.metadata.name }}-igw + namespace: {{ attachedekses.metadata.namespace }} + spec: + vpcRef: + from: + name: {{ attachedekses.metadata.name }}-vpc + # routeTableRefs: + # - from: + # name: {{ attachedekses.metadata.name }}-public-rt + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-igw + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: RouteTable + metadata: + name: {{ attachedekses.metadata.name }}-public-rt + namespace: {{ attachedekses.metadata.namespace }} + spec: + vpcRef: + from: + name: {{ attachedekses.metadata.name }}-vpc + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-public-rt + routes: + - destinationCIDRBlock: "0.0.0.0/0" + gatewayRef: + from: + name: {{ attachedekses.metadata.name }}-igw + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: RouteTable + metadata: + name: {{ attachedekses.metadata.name }}-private-rt + namespace: {{ attachedekses.metadata.namespace }} + spec: + vpcRef: + from: + name: {{ attachedekses.metadata.name }}-vpc + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-private-rt + - type: jinja2 + version: v0.0.1 + name: create-subnets + template: | + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ attachedekses.metadata.name }}-subnet-ids + namespace: {{ attachedekses.metadata.namespace }} + data: {} + {% for zone in attachedekses.spec.awsAvailabilityZones %} + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: Subnet + metadata: + name: {{ attachedekses.metadata.name }}-public-{{ zone.zoneNameSuffix }} + namespace: {{ attachedekses.metadata.namespace }} + spec: + vpcRef: + from: + name: {{ attachedekses.metadata.name }}-vpc + availabilityZone: {{ attachedekses.spec.awsRegion }}{{ zone.zoneNameSuffix }} + cidrBlock: {{ zone.publicSubnet }} + mapPublicIPOnLaunch: true + routeTableRefs: + - from: + name: {{ attachedekses.metadata.name }}-public-rt + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-public-{{ zone.zoneNameSuffix }} + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: Subnet + metadata: + name: {{ attachedekses.metadata.name }}-private-{{ zone.zoneNameSuffix }} + namespace: {{ attachedekses.metadata.namespace }} + spec: + vpcRef: + from: + name: {{ attachedekses.metadata.name }}-vpc + availabilityZone: {{ attachedekses.spec.awsRegion }}{{ zone.zoneNameSuffix }} + cidrBlock: {{ zone.privateSubnet }} + routeTableRefs: + - from: + name: {{ attachedekses.metadata.name }}-private-rt + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-private-{{ zone.zoneNameSuffix }} + --- + apiVersion: services.k8s.aws/v1alpha1 + kind: FieldExport + metadata: + name: {{ attachedekses.metadata.name }}-subnet-id-export-{{ zone.zoneNameSuffix }} + namespace: {{ attachedekses.metadata.namespace }} + spec: + from: + path: ".status.subnetID" + resource: + group: ec2.services.k8s.aws + kind: Subnet + name: {{ attachedekses.metadata.name }}-public-{{ zone.zoneNameSuffix }} + to: + key: {{ attachedekses.metadata.name }}-public-{{ zone.zoneNameSuffix }} + kind: configmap + name: {{ attachedekses.metadata.name }}-subnet-ids + namespace: {{ attachedekses.metadata.namespace }} + {% endfor %} + - type: jinja2 + version: v0.0.1 + name: create-nat + template: | + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: ElasticIPAddress + metadata: + name: {{ attachedekses.metadata.name }}-nat-ip + namespace: {{ attachedekses.metadata.namespace }} + spec: + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-nat-ip + --- + apiVersion: ec2.services.k8s.aws/v1alpha1 + kind: NATGateway + metadata: + name: {{ attachedekses.metadata.name }}-nat + namespace: {{ attachedekses.metadata.namespace }} + spec: + allocationRef: + from: + name: {{ attachedekses.metadata.name }}-nat-ip + subnetRef: + from: + name: {{ attachedekses.metadata.name }}-public-{{ attachedekses.spec.awsAvailabilityZones[0].zoneNameSuffix }} + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-nat + - type: jinja2 + version: v0.0.1 + name: create-role + template: | + --- + apiVersion: iam.services.k8s.aws/v1alpha1 + kind: Role + metadata: + name: {{ attachedekses.metadata.name }}-cluster-role + namespace: {{ attachedekses.metadata.namespace }} + spec: + name: {{ attachedekses.metadata.name }}-cluster-role + assumeRolePolicyDocument: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + policies: + - "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-cluster-role + --- + apiVersion: iam.services.k8s.aws/v1alpha1 + kind: Role + metadata: + name: {{ attachedekses.metadata.name }}-node-role + namespace: {{ attachedekses.metadata.namespace }} + spec: + name: {{ attachedekses.metadata.name }}-node-role + assumeRolePolicyDocument: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + policies: + - "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" + - "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + tags: + - key: Name + value: {{ attachedekses.metadata.name }}-node-role + - type: getter + version: v0.0.1 + name: subnet-ids + template: "" + configref: + name: subnet-ids + namespace: default + - type: jinja2 + version: v0.0.1 + name: create-eks-cluster + template: | + --- + apiVersion: eks.services.k8s.aws/v1alpha1 + kind: Cluster + metadata: + name: {{ attachedekses.metadata.name }}-cluster + namespace: {{ attachedekses.metadata.namespace }} + spec: + name: {{ attachedekses.metadata.name }}-cluster + version: "{{ attachedekses.spec.kubernetesVersion }}" + roleRef: + from: + name: {{ attachedekses.metadata.name }}-cluster-role + accessConfig: + authenticationMode: "API_AND_CONFIG_MAP" + resourcesVPCConfig: + endpointPrivateAccess: true + endpointPublicAccess: true + subnetIDs: + {% for key, value in values.subnet.ids|items %} + - {{ value }} + {% endfor %} + --- + apiVersion: eks.services.k8s.aws/v1alpha1 + kind: Nodegroup + metadata: + name: {{ attachedekses.metadata.name }}-np + namespace: {{ attachedekses.metadata.namespace }} + spec: + name: {{ attachedekses.metadata.name }}-np + clusterName: {{ attachedekses.metadata.name }}-cluster + subnetRefs: + {% for zone in attachedekses.spec.awsAvailabilityZones %} + - from: + name: {{ attachedekses.metadata.name }}-public-{{ zone.zoneNameSuffix }} + {% endfor %} + nodeRoleRef: + from: + name: {{ attachedekses.metadata.name }}-node-role + scalingConfig: + minSize: 1 + maxSize: 2 + desiredSize: 1 + version: "{{ attachedekses.spec.kubernetesVersion }}" + --- + apiVersion: eks.services.k8s.aws/v1alpha1 + kind: AccessEntry + metadata: + name: {{ attachedekses.metadata.name }}-access + namespace: {{ attachedekses.metadata.namespace }} + spec: + clusterName: {{ attachedekses.metadata.name }}-cluster + principalARN: {{ attachedekses.spec.awsAccessIdentity }} + accessPolicies: + - policyARN: arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy + accessScope: + type: cluster + kubernetesGroups: + - defaultUsers + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ attachedekses.metadata.name }}-eks-issuer + namespace: {{ attachedekses.metadata.namespace }} + data: {} + --- + - type: jinja2 + version: v0.0.1 + name: export-eks-data + template: | + --- + apiVersion: services.k8s.aws/v1alpha1 + kind: FieldExport + metadata: + name: {{ attachedekses.metadata.name }}-export + namespace: {{ attachedekses.metadata.namespace }} + spec: + from: + path: ".status.identity.oidc.issuer" + resource: + group: eks.services.k8s.aws + kind: Cluster + name: {{ attachedekses.metadata.name }}-cluster + to: + key: oidc + kind: configmap + name: {{ attachedekses.metadata.name }}-eks-issuer + namespace: {{ attachedekses.metadata.namespace }} + --- + - type: getter + version: v0.0.1 + name: eks-issuer + template: "" + configref: + name: eks-issuer + namespace: default + - type: jinja2 + version: v0.0.1 + name: attach + template: | + apiVersion: containerattached.cnrm.cloud.google.com/v1beta1 + kind: ContainerAttachedCluster + metadata: + name: {{ attachedekses.metadata.name }}-attached + namespace: {{ attachedekses.metadata.namespace }} + spec: + resourceID: {{ attachedekses.metadata.name }} + location: {{ attachedekses.spec.gcpRegion }} + projectRef: + external: {{ context.spec.project }} + description: "Test composition with EKS" + distribution: eks + oidcConfig: + issuerUrl: {{ values.issuer.url }} + platformVersion: {{ attachedekses.spec.attachedPlatformVersion }} + fleet: + projectRef: + external: "projects/{{ attachedekses.spec.gcpProjectNumber }}" + authorization: + adminUsers: {{ attachedekses.spec.adminUsers }} + + diff --git a/samples/AttachedEKS/02-context.yaml b/samples/AttachedEKS/02-context.yaml new file mode 100644 index 0000000..560afce --- /dev/null +++ b/samples/AttachedEKS/02-context.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# 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. + +# Create a namespace for user team +apiVersion: v1 +kind: Namespace +metadata: + name: "${NAMESPACE}" +--- +# Config this namespace for KCC +apiVersion: core.cnrm.cloud.google.com/v1beta1 +kind: ConfigConnectorContext +metadata: + name: configconnectorcontext.core.cnrm.cloud.google.com + namespace: "${NAMESPACE}" +spec: + billingProject: "${PROJECT_ID}" + googleServiceAccount: "${TEAM_GSA_EMAIL}" + requestProjectPolicy: BILLING_PROJECT +--- +# Config this namespace for composition +apiVersion: composition.google.com/v1alpha1 +kind: Context +metadata: + name: context + namespace: "${NAMESPACE}" +spec: + project: "${PROJECT_ID}" \ No newline at end of file diff --git a/samples/AttachedEKS/03-attached-1.yaml b/samples/AttachedEKS/03-attached-1.yaml new file mode 100644 index 0000000..ca79827 --- /dev/null +++ b/samples/AttachedEKS/03-attached-1.yaml @@ -0,0 +1,36 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: idp.mycompany.com/v1 +kind: AttachedEKS +metadata: + name: test-composition-eks-1 + namespace: team-eks +spec: + gcpRegion: us-west1 + kubernetesVersion: "1.28" + attachedPlatformVersion: "1.28.0-gke.2" + awsRegion: us-west-1 + gcpProjectNumber: "000000000000" # change this + adminUsers: + - user@example.com # change this + awsAccessIdentity: "arn:aws:iam::000000000000:user/username" # change this + awsAvailabilityZones: + - zoneNameSuffix: b + publicSubnet: "10.0.11.0/24" + privateSubnet: "10.0.1.0/24" + - zoneNameSuffix: c + publicSubnet: "10.0.12.0/24" + privateSubnet: "10.0.2.0/24" + diff --git a/samples/AttachedEKS/README.md b/samples/AttachedEKS/README.md new file mode 100644 index 0000000..1f7db23 --- /dev/null +++ b/samples/AttachedEKS/README.md @@ -0,0 +1,7 @@ +# Create an EKS cluster and attach it to GCP + +1. Follow the [setup-AWS-ACK.md](setup-AWS-ACK.md) guide to setup ACK. +2. Act as a platform admin and run the commands in [eks-charlie.md](eks-charlie.md) + to set up the composition and a user team. +3. Act as a user and run the commands in [eks-alice.md](eks-alice.md) to create + an attached EKS cluster. diff --git a/samples/AttachedEKS/eks-alice.md b/samples/AttachedEKS/eks-alice.md new file mode 100644 index 0000000..509f7fc --- /dev/null +++ b/samples/AttachedEKS/eks-alice.md @@ -0,0 +1,62 @@ +## Step 1. Apply CR to create the composition of AKS and its attachment +kubectl apply -f 03-attached-1.yaml + + +## Step 2. Log in to the cluster and apply the attached cluster install manifest +EKS_NAME=$(cat 03-attached-1.yaml |yq .metadata.name) +ATTACHED_REGION=$(cat 03-attached-1.yaml |yq .spec.gcpRegion) +ATTACHED_PLATFORM_VERSION=$(cat 03-attached-1.yaml |yq .spec.attachedPlatformVersion) + +gcloud container attached clusters generate-install-manifest \ + $EKS_NAME \ + --location=${ATTACHED_REGION} \ + --platform-version ${ATTACHED_PLATFORM_VERSION} \ + --output-file=/tmp/install-agent-${EKS_NAME}.yaml + +aws eks update-kubeconfig --name $EKS_NAME-cluster + +kubectl apply -f /tmp/install-agent-${EKS_NAME}.yaml + + +## Commands to check progress: + +kubectl get AttachedEKS -n team-eks + +kubectl get vpc.ec2.services.k8s.aws \ + -n team-eks + +kubectl get InternetGateway.ec2.services.k8s.aws \ + -n team-eks + +kubectl get RouteTable.ec2.services.k8s.aws \ + -n team-eks + +kubectl get subnet.ec2.services.k8s.aws \ + -n team-eks + +kubectl get ElasticIPAddress.ec2.services.k8s.aws \ + -n team-eks + +kubectl get NATGateway.ec2.services.k8s.aws \ + -n team-eks + +kubectl get role.iam.services.k8s.aws \ + -n team-eks + +kubectl get cluster.eks.services.k8s.aws \ + -n team-eks + +kubectl get Nodegroup.eks.services.k8s.aws \ + -n team-eks + +kubectl get AccessEntry.eks.services.k8s.aws \ + -n team-eks + +kubectl get FieldExport.services.k8s.aws \ + -n team-eks + +kubectl get cm \ + -n team-eks + +kubectl get containerattachedcluster \ + -n team-eks diff --git a/samples/AttachedEKS/eks-charlie.md b/samples/AttachedEKS/eks-charlie.md new file mode 100644 index 0000000..336120a --- /dev/null +++ b/samples/AttachedEKS/eks-charlie.md @@ -0,0 +1,31 @@ +## Apply the compostion +kubectl apply -f 01-composition.yaml + + +## Create a GCP service account for this team and grant KCC permissions + +This step follows the following documentation +https://cloud.google.com/config-connector/docs/how-to/install-namespaced + +export NAMESPACE=team-eks +export TEAM_GCP_SA_NAME="${NAMESPACE}" +export PROJECT_ID=$(gcloud config get-value project) +export TEAM_GSA_EMAIL="${TEAM_GCP_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +gcloud iam service-accounts create ${TEAM_GCP_SA_NAME} --project ${PROJECT_ID} +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${TEAM_GSA_EMAIL}" \ + --role="roles/owner" +gcloud iam service-accounts add-iam-policy-binding \ + ${TEAM_GSA_EMAIL} \ + --member="serviceAccount:${PROJECT_ID}.svc.id.goog[cnrm-system/cnrm-controller-manager-${NAMESPACE}]" \ + --role="roles/iam.workloadIdentityUser" \ + --project ${PROJECT_ID} +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${TEAM_GSA_EMAIL}" \ + --role="roles/monitoring.metricWriter" + + +## Create a namespace for Alice's team +cat 02-context.yaml | envsubst | \ + kubectl apply -f - diff --git a/samples/AttachedEKS/setup-AWS-ACK.md b/samples/AttachedEKS/setup-AWS-ACK.md new file mode 100644 index 0000000..f408d44 --- /dev/null +++ b/samples/AttachedEKS/setup-AWS-ACK.md @@ -0,0 +1,115 @@ +# Set up AWS Controllers for Kubernetes (ACK) + +Note: ACK support is only availabe for GCP managed [Config +Connector](https://cloud.google.com/config-connector/docs/overview). + +Here is an overview of the ACK setup process: + +1. Create a GCP service account. +1. Create an AWS role. + 1. Grant this role the permissions to manage the AWS resouces. + 1. Allow the GCP service account to impersonate the AWS role. +1. Create ACK controller(s) with the GCP service account. + +## Create a GCP service account + +``` +PROJECT_ID=$(gcloud config get-value project) +PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") + + +# Create the service account +gcloud iam service-accounts create $USER-allotrope \ + --description="Allotrope proof of concept" \ + --display-name="Allotrope POC" + +export GSA_EMAIL=$USER-allotrope@${PROJECT_ID}.iam.gserviceaccount.com + +# Get the workload identity pool for the gke/kcc cluster +WORKLOAD_IDENTITY_POOL="${PROJECT_ID}.svc.id.goog" + +# Grant workload identity bindings permissions +export ACK_NAMESPACE=kontrollers-ack-system # Don’t change +export ACK_KSA_NAME=ack-controller # Don’t change +gcloud iam service-accounts add-iam-policy-binding ${GSA_EMAIL} \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:${WORKLOAD_IDENTITY_POOL}[${ACK_NAMESPACE}/${ACK_KSA_NAME}]" \ + --condition None +``` + +## Create an AWS role and allow the GCP service account to impersonate it + +``` +# In this example we use the AdministratorAccess policy to manage AWS resouces. +# The user can use a different policy to manage the resouces. +AWS_POLICY=arn:aws:iam::aws:policy/AdministratorAccess + +# Get the subject of the GSA +GSA_SUB=$(gcloud iam service-accounts describe ${GSA_EMAIL} \ + --format "value(oauth2ClientId)") +export AWS_ROLE_NAME=${USER}-ack-role + +# Create an AWS role and policy +cat > /tmp/trust-policy.json << EOF +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "accounts.google.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "accounts.google.com:oaud": "sts.amazonaws.com", + "accounts.google.com:aud": "${GSA_SUB}", + "accounts.google.com:sub": "${GSA_SUB}" + } + } + } + ] +} +EOF +aws iam create-role \ + --role-name ${AWS_ROLE_NAME} \ + --assume-role-policy-document file:///tmp/trust-policy.json + +aws iam attach-role-policy \ + --role-name ${AWS_ROLE_NAME} \ + --policy-arn ${AWS_POLICY} + +AWS_ROLE_ARN=$(aws iam get-role --role-name ${AWS_ROLE_NAME} | jq -r .Role.Arn) +``` + + +## Create ACK controller(s) with the GCP service account + +We only support a subset of the ACK controllers. The user can create one or more +of the following ACK controllers: +- ec2-controller +- eks-controller +- iam-controller + +Use the following commands to create the ACK controller(s): + +``` +AWS_REGION=us-west-2 # or other regions + +cat < /tmp/ack-config.yaml +apiVersion: kontrollers.cnrm.cloud.google.com/v1alpha1 +kind: ACKKontroller +metadata: + name: ackkontroller.kontrollers.cnrm.cloud.google.com +spec: + defaultAWSRegion: "${AWS_REGION}" + defaultRoleARN: "${AWS_ROLE_ARN}" + controllers: + - "ec2-controller" # Choose one or more controllers. + - "eks-controller" # Choose one or more controllers. + - "iam-controller" # Choose one or more controllers. + googleServiceAccount: "${GSA_EMAIL}" +EOF + +kubectl apply -f /tmp/ack-config.yaml +``` diff --git a/samples/CloudSQL/README.md b/samples/CloudSQL/README.md new file mode 100644 index 0000000..0f3ad29 --- /dev/null +++ b/samples/CloudSQL/README.md @@ -0,0 +1,45 @@ +# CloudSQL + +## [Platform Admin] Create the composition + +``` +kubectl create -f composition/hasql.yaml +``` + +## [Team Admin] Create CloudSQL instance + +Please note we are creating this in `config-control` namespace for this sample. +If KCC is setup in a tenant namespace (say using the `AppTeam` sample +composition), then we can use the tenant namespace instead. + +``` +NAMESPACE=config-control +#NAMESPACE= + +kubectl apply -f - < " + exit 1 +fi + +name=$1 +namespace=$2 + +kubectl delete -f cloudsqls.facade.facade -n $namespace ${name} + +kubectl delete sqlinstances.sql.cnrm.cloud.google.com -n $namespace ${name}-db-main +kubectl delete sqlinstances.sql.cnrm.cloud.google.com -n $namespace ${name}-db-replica-us-central1 +kubectl delete kmskeyring -n $namespace kmscryptokeyring-us-central1 +kubectl delete kmskeyring -n $namespace kmscryptokeyring-us-east1 +kubectl delete kmscryptokey -n $namespace kmscryptokey-enc-us-central1 +kubectl delete kmscryptokey -n $namespace kmscryptokey-enc-us-east1 +kubectl delete iampolicymember -n $namespace sql-kms-us-east1-policybinding +kubectl delete iampolicymember -n $namespace sql-kms-us-central1-policybinding +kubectl delete serviceidentity -n $namespace sqladmin.googleapis.com +kubectl delete services.serviceusage.cnrm.cloud.google.com -n $namespace cloudkms.googleapis.com +kubectl delete services.serviceusage.cnrm.cloud.google.com -n $namespace iam.googleapis.com +kubectl delete services.serviceusage.cnrm.cloud.google.com -n $namespace serviceusage.googleapis.com +kubectl delete services.serviceusage.cnrm.cloud.google.com -n $namespace sqladmin.googleapis.com diff --git a/samples/CloudSQL/composition/hasql.yaml b/samples/CloudSQL/composition/hasql.yaml new file mode 100644 index 0000000..84ed7d8 --- /dev/null +++ b/samples/CloudSQL/composition/hasql.yaml @@ -0,0 +1,213 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: cloudsqls.idp.mycompany.com +spec: + conversion: + strategy: None + group: idp.mycompany.com + names: + categories: + - facade + - facades + kind: CloudSQL + listKind: CloudSQLList + plural: cloudsqls + singular: cloudsql + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CloudSQL composition Facade + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: CloudSQLSpec defines the desired state of CloudSQL + properties: + name: + type: string + regions: + items: + type: string + type: array + required: + - name + - regions + type: object + status: + description: status of CloudSQL object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: sql-siemail + namespace: default +spec: + valuesFrom: + - name: identity + resourceRef: + group: serviceusage.cnrm.cloud.google.com + version: v1beta1 + kind: ServiceIdentity + resource: serviceidentities + name: sqladmin.googleapis.com + fieldRef: + - path: ".status.email" + as: email +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: sqlha + namespace: default +spec: + inputAPIGroup: cloudsqls.idp.mycompany.com + expanders: + - type: jinja2 + version: v0.0.1 + name: enable-services + template: | + {% set services=[ 'cloudkms.googleapis.com', 'iam.googleapis.com', 'serviceusage.googleapis.com', 'sqladmin.googleapis.com' ] %} + {% for service in services %} + --- + apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1 + kind: Service + metadata: + annotations: + cnrm.cloud.google.com/deletion-policy: "abandon" + cnrm.cloud.google.com/disable-dependent-services: "false" + name: {{service}} + namespace: {{ cloudsqls.metadata.namespace }} + spec: + resourceID: {{service}} + {% endfor %} + - type: jinja2 + name: block2 + version: v0.0.1 + template: | + apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1 + kind: ServiceIdentity + metadata: + name: sqladmin.googleapis.com + namespace: {{ cloudsqls.metadata.namespace }} + spec: + projectRef: + external: {{ context.spec.project }} + - type: getter + version: v0.0.1 + name: get-serviceidentity + template: "" + configref: + name: sql-siemail + namespace: default + - type: jinja2 + name: block3 + version: v0.0.1 + template: | + {% for region in cloudsqls.spec.regions %} + --- + apiVersion: kms.cnrm.cloud.google.com/v1beta1 + kind: KMSKeyRing + metadata: + name: kmscryptokeyring-{{ region }} + namespace: {{ cloudsqls.metadata.namespace }} + spec: + location: {{ region }} + --- + apiVersion: kms.cnrm.cloud.google.com/v1beta1 + kind: KMSCryptoKey + metadata: + labels: + failure-zone: {{ region }} + name: kmscryptokey-enc-{{ region }} + namespace: {{ cloudsqls.metadata.namespace }} + spec: + keyRingRef: + name: kmscryptokeyring-{{ region }} + namespace: {{ cloudsqls.metadata.namespace }} + purpose: ENCRYPT_DECRYPT + versionTemplate: + algorithm: GOOGLE_SYMMETRIC_ENCRYPTION + protectionLevel: SOFTWARE + importOnly: false + --- + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPolicyMember + metadata: + name: sql-kms-{{ region }}-policybinding + namespace: {{ cloudsqls.metadata.namespace }} + spec: + member: serviceAccount:{{ values.identity.email }} + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + resourceRef: + kind: KMSCryptoKey + name: kmscryptokey-enc-{{ region }} + namespace: {{ cloudsqls.metadata.namespace }} + --- + apiVersion: sql.cnrm.cloud.google.com/v1beta1 + kind: SQLInstance + metadata: + annotations: + cnrm.cloud.google.com/deletion-policy: abandon + labels: + failure-zone: {{ region }} + {% if loop.index == 1 %} + name: {{ cloudsqls.spec.name }}-main + {% else %} + name: {{ cloudsqls.spec.name }}-replica-{{ region }} + {% endif %} + namespace: {{ cloudsqls.metadata.namespace }} + spec: + databaseVersion: POSTGRES_13 + encryptionKMSCryptoKeyRef: + external: projects/{{ context.spec.project }}/locations/{{ region }}/keyRings/kmscryptokeyring-{{ region }}/cryptoKeys/kmscryptokey-enc-{{ region }} + {% if loop.index > 1 %} + masterInstanceRef: + name: {{ cloudsqls.spec.name }}-main + namespace: {{ cloudsqls.metadata.namespace }} + {% endif %} + region: {{ region }} + settings: + availabilityType: REGIONAL + {% if loop.index == 1 %} + backupConfiguration: + backupRetentionSettings: + retainedBackups: 6 + enabled: true + location: us + {% endif %} + diskSize: 50 + diskType: PD_SSD + {% if loop.index == 1 %} + maintenanceWindow: + day: 7 + hour: 3 + {% endif %} + tier: db-custom-8-30720 + {% endfor %} \ No newline at end of file diff --git a/samples/CloudSQL/get_cloudsql.sh b/samples/CloudSQL/get_cloudsql.sh new file mode 100755 index 0000000..3706b4b --- /dev/null +++ b/samples/CloudSQL/get_cloudsql.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Copyright 2024 Google LLC +# +# 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. + + +scriptpath=$(realpath $0) +base=$(dirname $scriptpath) + +if [[ $# -ne 1 ]]; +then + echo "usage: ./get_cloudsql.sh " + exit 1 +fi + +namespace=$1 + +echo "ServiceIdentity ----------------------------------------" +kubectl get serviceidentity -n $namespace +echo +echo "SqlInstance --------------------------------------------" +kubectl get sqlinstances.sql.cnrm.cloud.google.com -n $namespace +echo +echo "KMSKeyRings --------------------------------------------" +kubectl get kmskeyring -n $namespace +echo +echo "KMSCryptoKeys ------------------------------------------" +kubectl get kmscryptokey -n $namespace +echo +echo "IAMPolicyMember ----------------------------------------" +kubectl get iampolicymember -n $namespace +echo +echo "ServiceUsage -------------------------------------------" +kubectl get services.serviceusage.cnrm.cloud.google.com -n $namespace diff --git a/samples/Eventarc/README.md b/samples/Eventarc/README.md new file mode 100644 index 0000000..9640744 --- /dev/null +++ b/samples/Eventarc/README.md @@ -0,0 +1,55 @@ +# Eventarc with GCS Trigger - KCC Composition + +This directory provides a KCC Compositions approach to setting up an event-driven workflow that triggers a Cloud Workflow when objects are finalized in a Google Cloud Storage (GCS) bucket. + +**Key Components** + +* **KCC Composition:** Manages the creation of the Eventarc trigger and associated resources. +* **Custom Resource Definition (CRD):** Defines a CRD for `EventarcConfig` to simplify Eventarc trigger configuration. +* **Custom Resource (CR):** Provides a custom resource to specify the Eventarc trigger configuration. +* **Storage Notification:** Configures a `StorageNotification` to publish events from the GCS bucket to a Pub/Sub topic. +* **Service Account:** Creates a service account with necessary permissions for Eventarc and Pub/Sub. + +**Prerequisites** + +* **Kubernetes Cluster:** With Config Connector installed. +* **Service Account:** With IAM permissions to manage GCS buckets, Pub/Sub topics, Eventarc triggers, and Cloud Workflows. +* **Cloud Workflow:** The workflow you want to trigger. + +**Why Getters and the Context API are Used** + +This configuration utilizes KCC's `GetterConfiguration` and `Context` API to improve resource management and streamline deployments. + +* **Getters (`GetterConfiguration`)** + + Getters allow the composition to extract values from resources within the cluster. In this case, they ensure that the `StorageNotification` resource is created only after the GCS bucket and Pub/Sub topic are fully available, preventing dependency errors. + +* **Context API (`Context`)** + + The Context API provides a way to define contextual information, such as the project ID, that can be accessed by the composition. This avoids redundant specification of the project ID for each resource and promotes centralized configuration management. + *Note that in the `config-control` namespace, the Context API is pre-populated.* + +**Deployment Steps** + +1. **Apply `eventarcconfigs-crd.yaml`:** Creates the CRD (Custom Resource Definition) for EventarcConfig. + + ```bash + kubectl apply -f eventarcconfigs-crd.yaml -n config-control + +2. **Apply `eventarc-composition.yaml`:** Creates the KCC composition for EventarcConfig. + + ```bash + kubectl apply -f eventarc-composition.yaml -n config-control + +3. **Apply `facade.yaml`:** Creates the EventarcConfig custom resource, triggering the composition to create the Eventarc trigger, GCS bucket, Pub/Sub topic, and StorageNotification. + + ```bash + kubectl apply -f facade.yaml -n config-control + +To delete the resources, delete the YAML files in reverse order: + +```bash +kubectl delete -f facade.yaml -n config-control +kubectl delete -f eventarc-composition.yaml -n config-control +kubectl delete -f eventarcconfigs-crd.yaml -n config-control +``` \ No newline at end of file diff --git a/samples/Eventarc/eventarc-composition.yaml b/samples/Eventarc/eventarc-composition.yaml new file mode 100644 index 0000000..e5d37ad --- /dev/null +++ b/samples/Eventarc/eventarc-composition.yaml @@ -0,0 +1,212 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: gcs-bucket-getter + namespace: config-control +spec: + valuesFrom: + - name: bucket + resourceRef: + group: storage.cnrm.cloud.google.com + version: v1beta1 + kind: StorageBucket + resource: storagebuckets + name: my-gcs-bucket-101 # Replace with your bucket name + fieldRef: + - path: ".metadata.name" + as: name +--- +apiVersion: composition.google.com/v1alpha1 +kind: GetterConfiguration +metadata: + name: pubsub-topic-getter + namespace: config-control +spec: + valuesFrom: + - name: topic + resourceRef: + group: pubsub.cnrm.cloud.google.com + version: v1beta1 + kind: PubSubTopic + resource: pubsubtopics + name: eventarc-topic-1 # Replace with your Pub/Sub topic name + fieldRef: + - path: ".metadata.name" + as: name +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: complete-eventarc-composition + namespace: config-control +spec: + inputAPIGroup: eventarcconfigs.idp.mycompany.com + expanders: + # STAGE 1: Enable necessary APIs. + - type: jinja2 + version: v0.0.1 + name: enable-services + template: | + {% set services=[ 'storage.googleapis.com', 'pubsub.googleapis.com', 'eventarc.googleapis.com', 'workflows.googleapis.com' ] %} + {% for service in services %} + --- + apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1 + kind: Service + metadata: + annotations: + cnrm.cloud.google.com/deletion-policy: "abandon" + cnrm.cloud.google.com/disable-dependent-services: "false" + name: {{service}} + namespace: {{ eventarcconfigs.spec.namespace }} + spec: + resourceID: {{service}} + {% endfor %} + + # STAGE 2: Apply SAs + - type: jinja2 # applies service accounts + name: apply-sa + template: | + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMServiceAccount + metadata: + annotations: + cnrm.cloud.google.com/project-id: {{ context.spec.project }} + name: eventarctrigger-iam-gsa + namespace: {{ context.spec.namespace }} + spec: + displayName: ExampleGSA-EventArc-POC + --- + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPolicyMember + metadata: + name: eventarctrigger-iam-gsa-wf + namespace: {{ context.spec.namespace }} + spec: + memberFrom: + serviceAccountRef: + name: eventarctrigger-iam-gsa + role: roles/workflows.admin + resourceRef: + kind: Project + external: {{ context.spec.project }} + --- + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPolicyMember + metadata: + name: eventarctrigger-iam-gsa-ev + namespace: {{ context.spec.namespace }} + spec: + memberFrom: + serviceAccountRef: + name: eventarctrigger-iam-gsa + role: roles/eventarc.admin + resourceRef: + kind: Project + external: {{ context.spec.project }} + --- + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPolicyMember + metadata: + name: gcs-bucket-pubsub-publisher + namespace: {{ context.spec.namespace }} + spec: + member: serviceAccount:service-{{ eventarcconfigs.spec.projectNumber }}@gs-project-accounts.iam.gserviceaccount.com + role: roles/pubsub.publisher + resourceRef: + kind: PubSubTopic + name: {{ eventarcconfigs.spec.topicName }} + + # STAGE 3: Create the StorageBucket and PubSubTopic resources + - type: jinja2 # create-gcs-bucket + name: create-gcs-bucket + template: | + apiVersion: storage.cnrm.cloud.google.com/v1beta1 + kind: StorageBucket + metadata: + name: {{ eventarcconfigs.spec.bucketName }} + namespace: {{ eventarcconfigs.spec.namespace }} + spec: + uniformBucketLevelAccess: true + - type: jinja2 # create-pubsub-topic + name: create-pubsub-topic + template: | + apiVersion: pubsub.cnrm.cloud.google.com/v1beta1 + kind: PubSubTopic + metadata: + name: {{ eventarcconfigs.spec.topicName }} + namespace: {{ eventarcconfigs.spec.namespace }} + + # STAGE 4: Wait for the StorageBucket and PubSubTopic to be ready + - type: getter # get-gcs-bucket + version: v0.0.1 + name: get-gcs-bucket + template: "" + configref: + name: gcs-bucket-getter + namespace: config-control + + - type: getter # get-pubsub-topic + version: v0.0.1 + name: get-pubsub-topic + template: "" + configref: + name: pubsub-topic-getter + namespace: config-control + + # STAGE 5: Create the EventarcTrigger and StorageNotification using extracted values + - type: jinja2 + name: create-eventarc-trigger + template: | + apiVersion: eventarc.cnrm.cloud.google.com/v1beta1 + kind: EventarcTrigger + metadata: + name: {{ eventarcconfigs.spec.triggerName }} + namespace: {{ context.spec.namespace }} + spec: + destination: + workflowRef: + external: "projects/{{ context.spec.project }}/locations/{{eventarcconfigs.spec.location}}/workflows/{{eventarcconfigs.spec.workflowName}}" + location: {{eventarcconfigs.spec.location}} + serviceAccountRef: + name: eventarctrigger-iam-gsa + transport: + pubsub: + topicRef: + name: {{ values.topic.name }} + namespace: config-control + matchingCriteria: + - attribute: "type" + value: "google.cloud.pubsub.topic.v1.messagePublished" + projectRef: + external: "projects/{{ context.spec.project }}" + + - type: jinja2 # create-storage-notification + name: create-storage-notification + template: | + apiVersion: storage.cnrm.cloud.google.com/v1beta1 + kind: StorageNotification + metadata: + name: bucket-notification + namespace: {{ eventarcconfigs.spec.namespace }} + spec: + bucketRef: + name: {{ values.bucket.name }} + topicRef: + name: {{ values.topic.name }} + eventTypes: + - "OBJECT_FINALIZE" + payloadFormat: JSON_API_V1 \ No newline at end of file diff --git a/samples/Eventarc/eventarcconfigs-crd.yaml b/samples/Eventarc/eventarcconfigs-crd.yaml new file mode 100644 index 0000000..de9249e --- /dev/null +++ b/samples/Eventarc/eventarcconfigs-crd.yaml @@ -0,0 +1,50 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: eventarcconfigs.idp.mycompany.com +spec: + group: idp.mycompany.com + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + triggerName: + type: string + workflowName: + type: string + location: + type: string + topicName: + type: string + bucketName: + type: string + projectNumber: + type: integer + scope: Namespaced + names: + plural: eventarcconfigs + singular: eventarcconfig + kind: EventarcConfig + shortNames: + - eac \ No newline at end of file diff --git a/samples/Eventarc/facade.yaml b/samples/Eventarc/facade.yaml new file mode 100644 index 0000000..c708ce1 --- /dev/null +++ b/samples/Eventarc/facade.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# 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. + +apiVersion: idp.mycompany.com/v1 +kind: EventarcConfig +metadata: + name: ea-test + namespace: config-control +spec: + triggerName: bucket-trigger + workflowName: workflow-1 # Replace with your workflow path + location: us-central1 # Or your desired location + serviceAccount: your-service-account # Replace with your service account name + topicName: eventarc-topic-1 # Replace with your Pub/Sub topic name + bucketName: my-gcs-bucket-101 # Replace with your GCS bucket name + projectNumber: # Replace with your project number \ No newline at end of file diff --git a/samples/FirstCELComposition/README.md b/samples/FirstCELComposition/README.md new file mode 100644 index 0000000..40505ec --- /dev/null +++ b/samples/FirstCELComposition/README.md @@ -0,0 +1,53 @@ +# Team Page + +## Install the CEL expander + +Install from released manifests: +``` +kubectl apply -f experiments/compositions/expanders/cel-expander/release/manifest.yaml +``` + +## Create the composition + +``` +kubectl apply -f composition/teampage.yaml +``` + + +## Create a Namespace and a Team page for `risk` + +The first step is to create a namespace where Team will be created. + +``` +export NAMESPACE=risk-team + +kubectl create namespace $NAMESPACE +``` + +Create a new `Team` CR + +``` +kubectl apply -f - < +

${cteams.metadata.name}

+ +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: team-page-cel +spec: + inputAPIGroup: cteams.facade.compositions.google.com # Facade API + expanders: + - type: cel + version: v0.0.1 + name: landing-page + template: "" + configref: + name: teampage + namespace: default +--- +apiVersion: composition.google.com/v1alpha1 +kind: Facade +metadata: + name: cel-team + namespace: default +spec: + facadeKind: CTeam + openAPIV3Schema: + # Schema for the `spec` field + type: object + required: + - apps + properties: + apps: + type: array + items: + type: object + required: + - description + - name + properties: + contact: + type: string + description: + type: string + name: + type: string +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-g +rules: +- apiGroups: + - facade.compositions.google.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.compositions.google.com + resources: + - "*/status" + verbs: + - get + - update +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-g +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-g +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system diff --git a/samples/FirstComposition/README.md b/samples/FirstComposition/README.md new file mode 100644 index 0000000..ea95d12 --- /dev/null +++ b/samples/FirstComposition/README.md @@ -0,0 +1,45 @@ +# Team Page + +## Create the composition + +``` +kubectl create -f composition/teampage.yaml +``` + + +## Create a Namespace and a Team page + +The first step is to create a namespace for the team: + +``` +export NAMESPACE=my-team +kubectl create namespace $NAMESPACE +``` + +Create a new `TeamPage` CR in the namespace: + +``` +kubectl apply -f - < +

{{ teampages.metadata.name }}

+ + + + + + {% for member in teampages.spec.members %} + + + + + {% endfor %} +
NameRole
{{ member.name }}{{ member.role }}
+ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: teampages.idp.mycompany.com +spec: + group: idp.mycompany.com + names: + categories: + - facade + - facades + kind: TeamPage + listKind: TeamPageList + plural: teampages + singular: teampage + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Create GCS Bucket with CORS and retention + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + required: + - members + properties: + members: + type: array + items: + type: object + required: + - role + - name + properties: + role: + type: string + name: + type: string + type: object + served: true + storage: true + subresources: + status: {} \ No newline at end of file diff --git a/samples/FirstGCPComposition/README.md b/samples/FirstGCPComposition/README.md new file mode 100644 index 0000000..d22c40f --- /dev/null +++ b/samples/FirstGCPComposition/README.md @@ -0,0 +1,32 @@ +# Storage Bucket with CORS and Retention + +## Create the composition + +``` +kubectl create -f composition/cors-bucket.yaml +``` + +## Create a CRBucket instance + +Create a new `CRBucket` CR + +``` +kubectl apply -f - < +

{{ .Values.hteams.metadata.name }}

+ + + + + + {{- range $i, $app := .Values.hteams.spec.apps }} + + + + + {{- end }} +
AppDescription
{{ $app.name }}{{ $app.description }}
+ +--- +apiVersion: composition.google.com/v1alpha1 +kind: Composition +metadata: + name: team-page-helm +spec: + inputAPIGroup: hteams.facade.compositions.google.com # Facade API + expanders: + - type: helm + version: v0.0.1 + name: landing-page + template: "" + configref: + name: teampage + namespace: default +--- +apiVersion: composition.google.com/v1alpha1 +kind: Facade +metadata: + name: helm-team + namespace: default +spec: + facadeKind: HTeam + openAPIV3Schema: + # Schema for the `spec` field + type: object + required: + - apps + properties: + apps: + type: array + items: + type: object + required: + - description + - name + properties: + contact: + type: string + description: + type: string + name: + type: string +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: composition-facade-g +rules: +- apiGroups: + - facade.compositions.google.com + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch + - create + - delete +- apiGroups: + - facade.compositions.google.com + resources: + - "*/status" + verbs: + - get + - update +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: composition-facade-g +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: composition-facade-g +subjects: +- kind: ServiceAccount + name: composition-controller-manager + namespace: composition-system diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..dd8cc06 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,27 @@ +# Using Samples + +## Prerequisites + +A Config Controller cluster with Compositions installed. + +## Personas/Roles + +Platform Admin: +- Responsible for creating compositions as well as the Facade CRDs +- Responsible for the AppTeam Facade CRs +- Assumed to work in `default` namespace when installing Compositions. + +Team/App Owner/Admin: +- Responsible for creating Facade CRs. +- Assumed to work in their own namespace. + +## AppTeam: Setting up New Teams + +This recipe creates a GCP project for the team as well as sets up KCC in +namespace mode to manage the team project. + +## SQLHA: Create CloudSQL in HA Mode + +This recipe sets up a CloudSQL instance in your project in HA mode. If you'd +like to create the instance in a tenant namespace, do the AppTeam sample first +to create one.