diff --git a/.gitignore b/.gitignore index 531215ec08..1fb99f772e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +go.work +go.work.sum .DS_Store .idea/ *.iml diff --git a/internal/testutil/pkgbuilder/builder_test.go b/internal/testutil/pkgbuilder/builder_test.go index 7e7e0ae871..b59c31f930 100644 --- a/internal/testutil/pkgbuilder/builder_test.go +++ b/internal/testutil/pkgbuilder/builder_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 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 pkgbuilder import ( diff --git a/rollouts/.dockerignore b/rollouts/.dockerignore new file mode 100644 index 0000000000..0f046820f1 --- /dev/null +++ b/rollouts/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/rollouts/.gitignore b/rollouts/.gitignore new file mode 100644 index 0000000000..e917e5cefe --- /dev/null +++ b/rollouts/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/rollouts/Dockerfile b/rollouts/Dockerfile new file mode 100644 index 0000000000..8152c15a87 --- /dev/null +++ b/rollouts/Dockerfile @@ -0,0 +1,48 @@ +# Copyright 2022 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.19 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 main.go main.go +COPY api/ api/ +COPY controllers/ controllers/ +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 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/rollouts/Makefile b/rollouts/Makefile new file mode 100644 index 0000000000..b1f1bdc474 --- /dev/null +++ b/rollouts/Makefile @@ -0,0 +1,181 @@ +# Copyright 2022 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. + + +# Image URL to use all building/pushing image targets +IMG ?= controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.25.0 + +# 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 + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + + +.PHONY: cli +cli: + go build -o ./bin/rollouts ./cli + +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: test ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: dry-run +dry-run: 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 > bin/all.yaml + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v3.8.7 +CONTROLLER_TOOLS_VERSION ?= v0.10.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.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 || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/rollouts/PROJECT b/rollouts/PROJECT new file mode 100644 index 0000000000..19cd7d0938 --- /dev/null +++ b/rollouts/PROJECT @@ -0,0 +1,34 @@ +domain: kpt.dev +layout: +- go.kubebuilder.io/v3 +projectName: rollouts +repo: github.com/GoogleContainerTools/kpt/rollouts +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kpt.dev + group: gitops + kind: Rollout + path: github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kpt.dev + group: gitops + kind: RemoteRootSync + path: github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kpt.dev + group: gitops + kind: ProgressiveRolloutStrategy + path: github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/rollouts/README.md b/rollouts/README.md new file mode 100644 index 0000000000..71160aea6b --- /dev/null +++ b/rollouts/README.md @@ -0,0 +1,5 @@ +# Rollouts + +Rollouts is a component for deploying KRM configuration to multiple kubernetes clusters. + +More details coming soon. \ No newline at end of file diff --git a/rollouts/api/v1alpha1/groupversion_info.go b/rollouts/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000000..7fe4e678e6 --- /dev/null +++ b/rollouts/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the gitops v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=gitops.kpt.dev +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: "gitops.kpt.dev", 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/rollouts/api/v1alpha1/progressiverolloutstrategy_types.go b/rollouts/api/v1alpha1/progressiverolloutstrategy_types.go new file mode 100644 index 0000000000..b9fa1c2ac5 --- /dev/null +++ b/rollouts/api/v1alpha1/progressiverolloutstrategy_types.go @@ -0,0 +1,79 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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" +) + +// 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. + +// ProgressiveRolloutStrategySpec defines the desired state of ProgressiveRolloutStrategy +type ProgressiveRolloutStrategySpec struct { + // Description is a user friendly description of this rollout strategy. + Description string `json:"description,omitempty"` + + // Waves defines an order set of waves of rolling updates. + Waves []Wave `json:"waves"` +} + +// Wave represents a group of rolling updates in a progressive rollout. It is also referred as steps, stages or phases +// of a progressive rollout. +type Wave struct { + // Name identifies the wave. + Name string `json:"name"` + + Description string `json:"description,omitempty"` + + // MaxConcurrent specifies maximum number of concurrent updates to be performed in this wave. + MaxConcurrent int64 `json:"maxConcurrent"` + + // Targets specifies the clusters that are part of this wave. + Targets ClusterTargetSelector `json:"targets,omitempty"` +} + +// ProgressiveRolloutStrategyStatus defines the observed state of ProgressiveRolloutStrategy +type ProgressiveRolloutStrategyStatus 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 + +// ProgressiveRolloutStrategy is the Schema for the progressiverolloutstrategies API +type ProgressiveRolloutStrategy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProgressiveRolloutStrategySpec `json:"spec,omitempty"` + Status ProgressiveRolloutStrategyStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ProgressiveRolloutStrategyList contains a list of ProgressiveRolloutStrategy +type ProgressiveRolloutStrategyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ProgressiveRolloutStrategy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ProgressiveRolloutStrategy{}, &ProgressiveRolloutStrategyList{}) +} diff --git a/rollouts/api/v1alpha1/remoterootsync_types.go b/rollouts/api/v1alpha1/remoterootsync_types.go new file mode 100644 index 0000000000..ea65dd9c91 --- /dev/null +++ b/rollouts/api/v1alpha1/remoterootsync_types.go @@ -0,0 +1,92 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// 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. + +// RemoteRootSyncSpec defines the desired state of RemoteRootSync +type RemoteRootSyncSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + ClusterRef ClusterRef `json:"clusterRef,omitempty"` + Template *RootSyncInfo `json:"template,omitempty"` +} + +type RootSyncInfo struct { + Spec *RootSyncSpec `json:"spec,omitempty"` +} + +type RootSyncSpec struct { + SourceFormat string `json:"sourceFormat,omitempty"` + Git *GitInfo `json:"git,omitempty"` +} + +type GitInfo struct { + Repo string `json:"repo"` + Branch string `json:"branch,omitempty"` + Revision string `json:"revision,omitempty"` + Dir string `json:"dir,omitempty"` + Period metav1.Duration `json:"period,omitempty"` + Auth string `json:"auth"` + GCPServiceAccountEmail string `json:"gcpServiceAccountEmail,omitempty"` + Proxy string `json:"proxy,omitempty"` + SecretRef SecretReference `json:"secretRef,omitempty"` + NoSSLVerify bool `json:"noSSLVerify,omitempty"` +} + +// RemoteRootSyncStatus defines the observed state of RemoteRootSync +type RemoteRootSyncStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions describes the reconciliation state of the object. + Conditions []metav1.Condition `json:"conditions,omitempty"` + + SyncStatus string `json:"syncStatus,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// RemoteRootSync is the Schema for the remoterootsyncs API +type RemoteRootSync struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RemoteRootSyncSpec `json:"spec,omitempty"` + Status RemoteRootSyncStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// RemoteRootSyncList contains a list of RemoteRootSync +type RemoteRootSyncList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RemoteRootSync `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RemoteRootSync{}, &RemoteRootSyncList{}) +} diff --git a/rollouts/api/v1alpha1/rollout_types.go b/rollouts/api/v1alpha1/rollout_types.go new file mode 100644 index 0000000000..736328f2f3 --- /dev/null +++ b/rollouts/api/v1alpha1/rollout_types.go @@ -0,0 +1,246 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +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. + +// RolloutSpec defines the desired state of Rollout +type RolloutSpec struct { + // Description is a user friendly description of this Rollout. + Description string `json:"description,omitempty"` + + // Clusters specifies the source for discovering the clusters. + Clusters ClusterDiscovery `json:"clusters"` + + // Packages source for this Rollout. + Packages PackagesConfig `json:"packages"` + + // Targets specifies the clusters that will receive the KRM config packages. + Targets ClusterTargetSelector `json:"targets,omitempty"` + + // PackageToTargetMatcher specifies the clusters that will receive a specific package. + PackageToTargetMatcher PackageToClusterMatcher `json:"packageToTargetMatcher"` + + // SyncTemplate defines the type and attributes for the RSync object used to syncing the packages. + SyncTemplate *SyncTemplate `json:"syncTemplate,omitempty"` + + // Strategy specifies the rollout strategy to use for this rollout. + Strategy RolloutStrategy `json:"strategy"` +} + +type ClusterTargetSelector struct { + Selector *metav1.LabelSelector `json:"selector,omitempty"` +} + +// ClusterReference contains the identify information +// need to refer a cluster. +type ClusterRef struct { + Name string `json:"name"` +} + +// different types of cluster sources +const ( + KCC ClusterSourceType = "KCC" + GCPFleet ClusterSourceType = "GCPFleet" +) + +// +kubebuilder:validation:Enum=KCC;GCPFleet +type ClusterSourceType string + +// ClusterDiscovery represents configuration needed to discover clusters. +type ClusterDiscovery struct { + SourceType ClusterSourceType `json:"sourceType"` + GCPFleet *ClusterSourceGCPFleet `json:"gcpFleet,omitempty"` +} + +// ClusterSourceGCPFleet represents configuration needed to discover gcp fleet clusters. +type ClusterSourceGCPFleet struct { + ProjectIds []string `json:"projectIds"` +} + +const ( + GitHub PackageSourceType = "GitHub" +) + +// +kubebuilder:validation:Enum=GitHub +type PackageSourceType string + +// PackagesConfig defines the packages the Rollout should deploy. +type PackagesConfig struct { + SourceType PackageSourceType `json:"sourceType"` + + GitHub GitHubSource `json:"github"` +} + +// GitHubSource defines the packages source in Git. +type GitHubSource struct { + Selector GitHubSelector `json:"selector"` +} + +// GitHubSelector defines the selector to apply to Git. +type GitHubSelector struct { + Org string `json:"org"` + Repo string `json:"repo"` + Directory string `json:"directory,omitempty"` + Revision string `json:"revision"` + SecretRef SecretReference `json:"secretRef,omitempty"` +} + +// SecretReference contains the reference to the secret +type SecretReference struct { + // Name represents the secret name + Name string `json:"name,omitempty"` +} + +// different types of sync templates. +const ( + TemplateTypeRootSync SyncTemplateType = "RootSync" + TemplateTypeRepoSync SyncTemplateType = "RepoSync" +) + +// +kubebuilder:validation:Enum=RootSync;RepoSync +type SyncTemplateType string + +// SyncTemplate defines the configuration for RSync templates. +type SyncTemplate struct { + Type SyncTemplateType `json:"type"` + RootSync *RootSyncTemplate `json:"rootSync,omitempty"` + RepoSync *RepoSyncTemplate `json:"repoSync,omitempty"` +} + +// RootSyncTemplate represent the sync template for RootSync. +type RootSyncTemplate struct { + SourceFormat string `json:"sourceFormat,omitempty"` + Git *GitInfo `json:"git,omitempty"` +} + +type RepoSyncTemplate struct { + SourceFormat string `json:"sourceFormat,omitempty"` + Git *GitInfo `json:"git,omitempty"` +} + +// +kubebuilder:validation:Enum=AllClusters;Custom +type MatcherType string + +const ( + MatchAllClusters MatcherType = "AllClusters" + CustomMatcher MatcherType = "Custom" +) + +type PackageToClusterMatcher struct { + Type MatcherType `json:"type"` + MatchExpression string `json:"matchExpression,omitempty"` +} + +// +kubebuilder:validation:Enum=AllAtOnce;RollingUpdate;Progressive +type StrategyType string + +const ( + AllAtOnce StrategyType = "AllAtOnce" + RollingUpdate StrategyType = "RollingUpdate" + Progressive StrategyType = "Progressive" +) + +type StrategyAllAtOnce struct{} + +type StrategyRollingUpdate struct { + MaxConcurrent int64 `json:"maxConcurrent"` +} + +// StrategyProgressive defines the progressive rollout strategy to use. +type StrategyProgressive struct { + // Name of the ProgressiveRolloutStrategy to use. + Name string `json:"name"` + + // Namespace of the ProgressiveRolloutStrategy to use. + Namespace string `json:"namespace"` + + // PauseAfterWave represents the highest wave the strategy will deploy. + PauseAfterWave PauseAfterWave `json:"pauseAfterWave,omitempty"` +} + +type PauseAfterWave struct { + // WaveName represents name of the wave defined in the ProgressiveRolloutStrategy. + WaveName string `json:"waveName"` +} + +type RolloutStrategy struct { + Type StrategyType `json:"type"` + AllAtOnce *StrategyAllAtOnce `json:"allAtOnce,omitempty"` + RollingUpdate *StrategyRollingUpdate `json:"rollingUpdate,omitempty"` + Progressive *StrategyProgressive `json:"progressive,omitempty"` +} + +// RolloutStatus defines the observed state of Rollout +type RolloutStatus struct { + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions describes the reconciliation state of the object. + Conditions []metav1.Condition `json:"conditions,omitempty"` + + Overall string `json:"overall,omitempty"` + WaveStatuses []WaveStatus `json:"waveStatuses,omitempty"` + + ClusterStatuses []ClusterStatus `json:"clusterStatuses,omitempty"` +} + +type WaveStatus struct { + Name string `json:"name"` + Status string `json:"status"` + Paused bool `json:"paused,omitempty"` + ClusterStatuses []ClusterStatus `json:"clusterStatuses,omitempty"` +} + +type ClusterStatus struct { + Name string `json:"name"` + PackageStatus PackageStatus `json:"packageStatus"` +} + +type PackageStatus struct { + PackageID string `json:"packageId"` + SyncStatus string `json:"syncStatus"` + Status string `json:"status"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Rollout is the Schema for the rollouts API +type Rollout struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RolloutSpec `json:"spec,omitempty"` + Status RolloutStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// RolloutList contains a list of Rollout +type RolloutList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Rollout `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Rollout{}, &RolloutList{}) +} diff --git a/rollouts/api/v1alpha1/zz_generated.deepcopy.go b/rollouts/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..bac94d0c9b --- /dev/null +++ b/rollouts/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,776 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterDiscovery) DeepCopyInto(out *ClusterDiscovery) { + *out = *in + if in.GCPFleet != nil { + in, out := &in.GCPFleet, &out.GCPFleet + *out = new(ClusterSourceGCPFleet) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterDiscovery. +func (in *ClusterDiscovery) DeepCopy() *ClusterDiscovery { + if in == nil { + return nil + } + out := new(ClusterDiscovery) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRef) DeepCopyInto(out *ClusterRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRef. +func (in *ClusterRef) DeepCopy() *ClusterRef { + if in == nil { + return nil + } + out := new(ClusterRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSourceGCPFleet) DeepCopyInto(out *ClusterSourceGCPFleet) { + *out = *in + if in.ProjectIds != nil { + in, out := &in.ProjectIds, &out.ProjectIds + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSourceGCPFleet. +func (in *ClusterSourceGCPFleet) DeepCopy() *ClusterSourceGCPFleet { + if in == nil { + return nil + } + out := new(ClusterSourceGCPFleet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { + *out = *in + out.PackageStatus = in.PackageStatus +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. +func (in *ClusterStatus) DeepCopy() *ClusterStatus { + if in == nil { + return nil + } + out := new(ClusterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterTargetSelector) DeepCopyInto(out *ClusterTargetSelector) { + *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterTargetSelector. +func (in *ClusterTargetSelector) DeepCopy() *ClusterTargetSelector { + if in == nil { + return nil + } + out := new(ClusterTargetSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubSelector) DeepCopyInto(out *GitHubSelector) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubSelector. +func (in *GitHubSelector) DeepCopy() *GitHubSelector { + if in == nil { + return nil + } + out := new(GitHubSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubSource) DeepCopyInto(out *GitHubSource) { + *out = *in + out.Selector = in.Selector +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubSource. +func (in *GitHubSource) DeepCopy() *GitHubSource { + if in == nil { + return nil + } + out := new(GitHubSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitInfo) DeepCopyInto(out *GitInfo) { + *out = *in + out.Period = in.Period + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitInfo. +func (in *GitInfo) DeepCopy() *GitInfo { + if in == nil { + return nil + } + out := new(GitInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageStatus) DeepCopyInto(out *PackageStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus. +func (in *PackageStatus) DeepCopy() *PackageStatus { + if in == nil { + return nil + } + out := new(PackageStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageToClusterMatcher) DeepCopyInto(out *PackageToClusterMatcher) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageToClusterMatcher. +func (in *PackageToClusterMatcher) DeepCopy() *PackageToClusterMatcher { + if in == nil { + return nil + } + out := new(PackageToClusterMatcher) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackagesConfig) DeepCopyInto(out *PackagesConfig) { + *out = *in + out.GitHub = in.GitHub +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackagesConfig. +func (in *PackagesConfig) DeepCopy() *PackagesConfig { + if in == nil { + return nil + } + out := new(PackagesConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PauseAfterWave) DeepCopyInto(out *PauseAfterWave) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PauseAfterWave. +func (in *PauseAfterWave) DeepCopy() *PauseAfterWave { + if in == nil { + return nil + } + out := new(PauseAfterWave) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProgressiveRolloutStrategy) DeepCopyInto(out *ProgressiveRolloutStrategy) { + *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 ProgressiveRolloutStrategy. +func (in *ProgressiveRolloutStrategy) DeepCopy() *ProgressiveRolloutStrategy { + if in == nil { + return nil + } + out := new(ProgressiveRolloutStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProgressiveRolloutStrategy) 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 *ProgressiveRolloutStrategyList) DeepCopyInto(out *ProgressiveRolloutStrategyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ProgressiveRolloutStrategy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProgressiveRolloutStrategyList. +func (in *ProgressiveRolloutStrategyList) DeepCopy() *ProgressiveRolloutStrategyList { + if in == nil { + return nil + } + out := new(ProgressiveRolloutStrategyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProgressiveRolloutStrategyList) 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 *ProgressiveRolloutStrategySpec) DeepCopyInto(out *ProgressiveRolloutStrategySpec) { + *out = *in + if in.Waves != nil { + in, out := &in.Waves, &out.Waves + *out = make([]Wave, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProgressiveRolloutStrategySpec. +func (in *ProgressiveRolloutStrategySpec) DeepCopy() *ProgressiveRolloutStrategySpec { + if in == nil { + return nil + } + out := new(ProgressiveRolloutStrategySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProgressiveRolloutStrategyStatus) DeepCopyInto(out *ProgressiveRolloutStrategyStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProgressiveRolloutStrategyStatus. +func (in *ProgressiveRolloutStrategyStatus) DeepCopy() *ProgressiveRolloutStrategyStatus { + if in == nil { + return nil + } + out := new(ProgressiveRolloutStrategyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteRootSync) DeepCopyInto(out *RemoteRootSync) { + *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 RemoteRootSync. +func (in *RemoteRootSync) DeepCopy() *RemoteRootSync { + if in == nil { + return nil + } + out := new(RemoteRootSync) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RemoteRootSync) 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 *RemoteRootSyncList) DeepCopyInto(out *RemoteRootSyncList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RemoteRootSync, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteRootSyncList. +func (in *RemoteRootSyncList) DeepCopy() *RemoteRootSyncList { + if in == nil { + return nil + } + out := new(RemoteRootSyncList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RemoteRootSyncList) 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 *RemoteRootSyncSpec) DeepCopyInto(out *RemoteRootSyncSpec) { + *out = *in + out.ClusterRef = in.ClusterRef + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(RootSyncInfo) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteRootSyncSpec. +func (in *RemoteRootSyncSpec) DeepCopy() *RemoteRootSyncSpec { + if in == nil { + return nil + } + out := new(RemoteRootSyncSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteRootSyncStatus) DeepCopyInto(out *RemoteRootSyncStatus) { + *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 RemoteRootSyncStatus. +func (in *RemoteRootSyncStatus) DeepCopy() *RemoteRootSyncStatus { + if in == nil { + return nil + } + out := new(RemoteRootSyncStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepoSyncTemplate) DeepCopyInto(out *RepoSyncTemplate) { + *out = *in + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(GitInfo) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepoSyncTemplate. +func (in *RepoSyncTemplate) DeepCopy() *RepoSyncTemplate { + if in == nil { + return nil + } + out := new(RepoSyncTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rollout) DeepCopyInto(out *Rollout) { + *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 Rollout. +func (in *Rollout) DeepCopy() *Rollout { + if in == nil { + return nil + } + out := new(Rollout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Rollout) 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 *RolloutList) DeepCopyInto(out *RolloutList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Rollout, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutList. +func (in *RolloutList) DeepCopy() *RolloutList { + if in == nil { + return nil + } + out := new(RolloutList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RolloutList) 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 *RolloutSpec) DeepCopyInto(out *RolloutSpec) { + *out = *in + in.Clusters.DeepCopyInto(&out.Clusters) + out.Packages = in.Packages + in.Targets.DeepCopyInto(&out.Targets) + out.PackageToTargetMatcher = in.PackageToTargetMatcher + if in.SyncTemplate != nil { + in, out := &in.SyncTemplate, &out.SyncTemplate + *out = new(SyncTemplate) + (*in).DeepCopyInto(*out) + } + in.Strategy.DeepCopyInto(&out.Strategy) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutSpec. +func (in *RolloutSpec) DeepCopy() *RolloutSpec { + if in == nil { + return nil + } + out := new(RolloutSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStatus) DeepCopyInto(out *RolloutStatus) { + *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.WaveStatuses != nil { + in, out := &in.WaveStatuses, &out.WaveStatuses + *out = make([]WaveStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ClusterStatuses != nil { + in, out := &in.ClusterStatuses, &out.ClusterStatuses + *out = make([]ClusterStatus, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStatus. +func (in *RolloutStatus) DeepCopy() *RolloutStatus { + if in == nil { + return nil + } + out := new(RolloutStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStrategy) DeepCopyInto(out *RolloutStrategy) { + *out = *in + if in.AllAtOnce != nil { + in, out := &in.AllAtOnce, &out.AllAtOnce + *out = new(StrategyAllAtOnce) + **out = **in + } + if in.RollingUpdate != nil { + in, out := &in.RollingUpdate, &out.RollingUpdate + *out = new(StrategyRollingUpdate) + **out = **in + } + if in.Progressive != nil { + in, out := &in.Progressive, &out.Progressive + *out = new(StrategyProgressive) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStrategy. +func (in *RolloutStrategy) DeepCopy() *RolloutStrategy { + if in == nil { + return nil + } + out := new(RolloutStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RootSyncInfo) DeepCopyInto(out *RootSyncInfo) { + *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(RootSyncSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootSyncInfo. +func (in *RootSyncInfo) DeepCopy() *RootSyncInfo { + if in == nil { + return nil + } + out := new(RootSyncInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RootSyncSpec) DeepCopyInto(out *RootSyncSpec) { + *out = *in + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(GitInfo) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootSyncSpec. +func (in *RootSyncSpec) DeepCopy() *RootSyncSpec { + if in == nil { + return nil + } + out := new(RootSyncSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RootSyncTemplate) DeepCopyInto(out *RootSyncTemplate) { + *out = *in + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(GitInfo) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootSyncTemplate. +func (in *RootSyncTemplate) DeepCopy() *RootSyncTemplate { + if in == nil { + return nil + } + out := new(RootSyncTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretReference) DeepCopyInto(out *SecretReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference. +func (in *SecretReference) DeepCopy() *SecretReference { + if in == nil { + return nil + } + out := new(SecretReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StrategyAllAtOnce) DeepCopyInto(out *StrategyAllAtOnce) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StrategyAllAtOnce. +func (in *StrategyAllAtOnce) DeepCopy() *StrategyAllAtOnce { + if in == nil { + return nil + } + out := new(StrategyAllAtOnce) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StrategyProgressive) DeepCopyInto(out *StrategyProgressive) { + *out = *in + out.PauseAfterWave = in.PauseAfterWave +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StrategyProgressive. +func (in *StrategyProgressive) DeepCopy() *StrategyProgressive { + if in == nil { + return nil + } + out := new(StrategyProgressive) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StrategyRollingUpdate) DeepCopyInto(out *StrategyRollingUpdate) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StrategyRollingUpdate. +func (in *StrategyRollingUpdate) DeepCopy() *StrategyRollingUpdate { + if in == nil { + return nil + } + out := new(StrategyRollingUpdate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SyncTemplate) DeepCopyInto(out *SyncTemplate) { + *out = *in + if in.RootSync != nil { + in, out := &in.RootSync, &out.RootSync + *out = new(RootSyncTemplate) + (*in).DeepCopyInto(*out) + } + if in.RepoSync != nil { + in, out := &in.RepoSync, &out.RepoSync + *out = new(RepoSyncTemplate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncTemplate. +func (in *SyncTemplate) DeepCopy() *SyncTemplate { + if in == nil { + return nil + } + out := new(SyncTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Wave) DeepCopyInto(out *Wave) { + *out = *in + in.Targets.DeepCopyInto(&out.Targets) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Wave. +func (in *Wave) DeepCopy() *Wave { + if in == nil { + return nil + } + out := new(Wave) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WaveStatus) DeepCopyInto(out *WaveStatus) { + *out = *in + if in.ClusterStatuses != nil { + in, out := &in.ClusterStatuses, &out.ClusterStatuses + *out = make([]ClusterStatus, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WaveStatus. +func (in *WaveStatus) DeepCopy() *WaveStatus { + if in == nil { + return nil + } + out := new(WaveStatus) + in.DeepCopyInto(out) + return out +} diff --git a/rollouts/cli/advance/advance.go b/rollouts/cli/advance/advance.go new file mode 100644 index 0000000000..96c10a0106 --- /dev/null +++ b/rollouts/cli/advance/advance.go @@ -0,0 +1,103 @@ +// Copyright 2023 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 advance + +import ( + "context" + "fmt" + + "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/rolloutsclient" + "github.com/spf13/cobra" +) + +func newRunner(ctx context.Context) *runner { + r := &runner{ + ctx: ctx, + } + c := &cobra.Command{ + Use: "advance rollout-name wave-name", + Short: "advances the wave of a progressive rollout", + Long: "advances the wave of a progressive rollout", + Example: "advances the wave of a progressive rollout", + RunE: r.runE, + } + r.Command = c + return r +} + +func NewCommand(ctx context.Context) *cobra.Command { + return newRunner(ctx).Command +} + +type runner struct { + ctx context.Context + Command *cobra.Command +} + +func (r *runner) runE(cmd *cobra.Command, args []string) error { + rlc, err := rolloutsclient.New() + if err != nil { + fmt.Printf("%s\n", err) + return err + } + + if len(args) == 0 { + return fmt.Errorf("must provide rollout name\n") + } + + if len(args) == 1 { + return fmt.Errorf("must provide wave name\n") + } + + rolloutName := args[0] + waveName := args[1] + + rollout, err := rlc.Get(r.ctx, rolloutName) + if err != nil { + fmt.Printf("%s\n", err) + return err + } + + if rollout.Spec.Strategy.Type != v1alpha1.Progressive { + return fmt.Errorf("rollout must be using the progressive strategy to use this command\n") + } + + if rollout.Status.WaveStatuses != nil { + waveFound := false + + for _, waveStatus := range rollout.Status.WaveStatuses { + if waveStatus.Name == waveName { + waveFound = true + break + } + } + + if !waveFound { + return fmt.Errorf("wave %q not found in this rollout\n", waveName) + } + } + + rollout.Spec.Strategy.Progressive.PauseAfterWave.WaveName = waveName + + err = rlc.Update(r.ctx, rollout) + if err != nil { + fmt.Printf("%s\n", err) + return err + } + + fmt.Println("done") + return nil +} diff --git a/rollouts/cli/get/get.go b/rollouts/cli/get/get.go new file mode 100644 index 0000000000..0a411c81cf --- /dev/null +++ b/rollouts/cli/get/get.go @@ -0,0 +1,87 @@ +// Copyright 2023 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 get + +import ( + "context" + "fmt" + + rolloutsapi "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/rolloutsclient" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +func NewCommand(ctx context.Context) *cobra.Command { + return newRunner(ctx).Command +} + +func newRunner(ctx context.Context) *runner { + r := &runner{ + ctx: ctx, + } + c := &cobra.Command{ + Use: "get", + Short: "lists rollouts", + Long: "lists rollouts", + Example: "lists rollouts", + RunE: r.runE, + } + r.Command = c + return r +} + +type runner struct { + ctx context.Context + Command *cobra.Command +} + +func (r *runner) runE(cmd *cobra.Command, args []string) error { + rlc, err := rolloutsclient.New() + if err != nil { + fmt.Printf("%s\n", err) + return err + } + + rollouts, err := rlc.List(r.ctx, "") + if err != nil { + fmt.Printf("%s\n", err) + return err + } + renderRolloutsAsTable(cmd, rollouts) + return nil +} + +func renderRolloutsAsTable(cmd *cobra.Command, rollouts *rolloutsapi.RolloutList) { + t := table.NewWriter() + t.SetOutputMirror(cmd.OutOrStdout()) + t.AppendHeader(table.Row{"ROLLOUT", "STATUS", "CLUSTERS (READY/TOTAL)"}) + for _, rollout := range rollouts.Items { + readyCount := 0 + for _, cluster := range rollout.Status.ClusterStatuses { + if cluster.PackageStatus.Status == "Synced" { + readyCount++ + } + } + t.AppendRow([]interface{}{ + rollout.Name, + rollout.Status.Overall, + fmt.Sprintf("%d/%d", readyCount, len(rollout.Status.ClusterStatuses))}) + } + t.AppendSeparator() + // t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + // t.AppendFooter(table.Row{"", "", "Total", 10000}) + t.Render() +} diff --git a/rollouts/cli/main.go b/rollouts/cli/main.go new file mode 100644 index 0000000000..386944ba6a --- /dev/null +++ b/rollouts/cli/main.go @@ -0,0 +1,55 @@ +// Copyright 2022 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" + "os" + + "github.com/GoogleContainerTools/kpt/rollouts/cli/advance" + "github.com/GoogleContainerTools/kpt/rollouts/cli/get" + "github.com/GoogleContainerTools/kpt/rollouts/cli/status" + "github.com/spf13/cobra" + _ "k8s.io/client-go/plugin/pkg/client/auth" +) + +func main() { + ctx := context.Background() + + rolloutsCmd := &cobra.Command{ + Use: "cli", + Short: "cli ", + Long: "cli", + RunE: func(cmd *cobra.Command, args []string) error { + h, err := cmd.Flags().GetBool("help") + if err != nil { + return err + } + if h { + return cmd.Help() + } + return cmd.Usage() + }, + } + + rolloutsCmd.AddCommand( + advance.NewCommand(ctx), + get.NewCommand(ctx), + status.NewCommand(ctx), + ) + if err := rolloutsCmd.Execute(); err != nil { + os.Exit(-1) + } +} diff --git a/rollouts/cli/status/status.go b/rollouts/cli/status/status.go new file mode 100644 index 0000000000..dc07b75945 --- /dev/null +++ b/rollouts/cli/status/status.go @@ -0,0 +1,110 @@ +// Copyright 2023 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 status + +import ( + "context" + "fmt" + + rolloutsapi "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/rolloutsclient" + "github.com/spf13/cobra" + + "github.com/jedib0t/go-pretty/v6/table" +) + +func newRunner(ctx context.Context) *runner { + r := &runner{ + ctx: ctx, + } + c := &cobra.Command{ + Use: "status", + Short: "displays status of a rollout", + Long: "displays status of a rollout", + Example: "displays status of a rollout", + RunE: r.runE, + } + r.Command = c + return r +} + +func NewCommand(ctx context.Context) *cobra.Command { + return newRunner(ctx).Command +} + +type runner struct { + ctx context.Context + Command *cobra.Command +} + +func (r *runner) runE(cmd *cobra.Command, args []string) error { + rlc, err := rolloutsclient.New() + if err != nil { + fmt.Printf("%s\n", err) + return err + } + + if len(args) == 0 { + fmt.Printf("must provide rollout name") + return nil + } + + rollout, err := rlc.Get(r.ctx, args[0]) + if err != nil { + fmt.Printf("%s\n", err) + return err + } + + if len(rollout.Status.WaveStatuses) > 0 { + renderWaveStatusAsTable(cmd, rollout) + return nil + } + renderStatusAsTable(cmd, rollout) + return nil +} + +func renderStatusAsTable(cmd *cobra.Command, rollout *rolloutsapi.Rollout) { + t := table.NewWriter() + t.SetOutputMirror(cmd.OutOrStdout()) + t.AppendHeader(table.Row{"CLUSTER", "PACKAGE ID", "PACKAGE STATUS", "SYNC STATUS"}) + for _, cluster := range rollout.Status.ClusterStatuses { + pkgStatus := cluster.PackageStatus + t.AppendRow([]interface{}{cluster.Name, pkgStatus.PackageID, pkgStatus.Status, pkgStatus.SyncStatus}) + } + t.AppendSeparator() + // t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + // t.AppendFooter(table.Row{"", "", "Total", 10000}) + t.Render() +} + +func renderWaveStatusAsTable(cmd *cobra.Command, rollout *rolloutsapi.Rollout) { + t := table.NewWriter() + t.SetOutputMirror(cmd.OutOrStdout()) + t.AppendHeader(table.Row{"WAVE", "CLUSTER", "PACKAGE ID", "PACKAGE STATUS", "SYNC STATUS"}) + for _, wave := range rollout.Status.WaveStatuses { + for i, cluster := range wave.ClusterStatuses { + pkgStatus := cluster.PackageStatus + waveName := "" + if i == 0 { + waveName = wave.Name + } + t.AppendRow([]interface{}{waveName, cluster.Name, pkgStatus.PackageID, pkgStatus.Status, pkgStatus.SyncStatus}) + } + t.AppendSeparator() + } + // t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + // t.AppendFooter(table.Row{"", "", "Total", 10000}) + t.Render() +} diff --git a/rollouts/config/crd/bases/gitops.kpt.dev_progressiverolloutstrategies.yaml b/rollouts/config/crd/bases/gitops.kpt.dev_progressiverolloutstrategies.yaml new file mode 100644 index 0000000000..6167112dd0 --- /dev/null +++ b/rollouts/config/crd/bases/gitops.kpt.dev_progressiverolloutstrategies.yaml @@ -0,0 +1,145 @@ +# Copyright 2023 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.10.0 + creationTimestamp: null + name: progressiverolloutstrategies.gitops.kpt.dev +spec: + group: gitops.kpt.dev + names: + kind: ProgressiveRolloutStrategy + listKind: ProgressiveRolloutStrategyList + plural: progressiverolloutstrategies + singular: progressiverolloutstrategy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ProgressiveRolloutStrategy is the Schema for the progressiverolloutstrategies + 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: ProgressiveRolloutStrategySpec defines the desired state + of ProgressiveRolloutStrategy + properties: + description: + description: Description is a user friendly description of this rollout + strategy. + type: string + waves: + description: Waves defines an order set of waves of rolling updates. + items: + description: Wave represents a group of rolling updates in a progressive + rollout. It is also referred as steps, stages or phases of a progressive + rollout. + properties: + description: + type: string + maxConcurrent: + description: MaxConcurrent specifies maximum number of concurrent + updates to be performed in this wave. + format: int64 + type: integer + name: + description: Name identifies the wave. + type: string + targets: + description: Targets specifies the clusters that are part of + this wave. + properties: + selector: + description: A label selector is a label query over a set + of resources. The result of matchLabels and matchExpressions + are ANDed. An empty label selector matches all objects. + A null label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + required: + - maxConcurrent + - name + type: object + type: array + required: + - waves + type: object + status: + description: ProgressiveRolloutStrategyStatus defines the observed state + of ProgressiveRolloutStrategy + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/rollouts/config/crd/bases/gitops.kpt.dev_remoterootsyncs.yaml b/rollouts/config/crd/bases/gitops.kpt.dev_remoterootsyncs.yaml new file mode 100644 index 0000000000..00a424b235 --- /dev/null +++ b/rollouts/config/crd/bases/gitops.kpt.dev_remoterootsyncs.yaml @@ -0,0 +1,188 @@ +# Copyright 2023 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.10.0 + creationTimestamp: null + name: remoterootsyncs.gitops.kpt.dev +spec: + group: gitops.kpt.dev + names: + kind: RemoteRootSync + listKind: RemoteRootSyncList + plural: remoterootsyncs + singular: remoterootsync + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: RemoteRootSync is the Schema for the remoterootsyncs 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: RemoteRootSyncSpec defines the desired state of RemoteRootSync + properties: + clusterRef: + description: ClusterReference contains the identify information need + to refer a cluster. + properties: + name: + type: string + required: + - name + type: object + template: + properties: + spec: + properties: + git: + properties: + auth: + type: string + branch: + type: string + dir: + type: string + gcpServiceAccountEmail: + type: string + noSSLVerify: + type: boolean + period: + type: string + proxy: + type: string + repo: + type: string + revision: + type: string + secretRef: + description: SecretReference contains the reference to + the secret + properties: + name: + description: Name represents the secret name + type: string + type: object + required: + - auth + - repo + type: object + sourceFormat: + type: string + type: object + type: object + type: object + status: + description: RemoteRootSyncStatus defines the observed state of RemoteRootSync + properties: + conditions: + description: Conditions describes the reconciliation state of the + object. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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 + observedGeneration: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + format: int64 + type: integer + syncStatus: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/rollouts/config/crd/bases/gitops.kpt.dev_rollouts.yaml b/rollouts/config/crd/bases/gitops.kpt.dev_rollouts.yaml new file mode 100644 index 0000000000..7e77a08e42 --- /dev/null +++ b/rollouts/config/crd/bases/gitops.kpt.dev_rollouts.yaml @@ -0,0 +1,472 @@ +# Copyright 2023 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.10.0 + creationTimestamp: null + name: rollouts.gitops.kpt.dev +spec: + group: gitops.kpt.dev + names: + kind: Rollout + listKind: RolloutList + plural: rollouts + singular: rollout + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Rollout is the Schema for the rollouts 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: RolloutSpec defines the desired state of Rollout + properties: + clusters: + description: Clusters specifies the source for discovering the clusters. + properties: + gcpFleet: + description: ClusterSourceGCPFleet represents configuration needed + to discover gcp fleet clusters. + properties: + projectIds: + items: + type: string + type: array + required: + - projectIds + type: object + sourceType: + enum: + - KCC + - GCPFleet + type: string + required: + - sourceType + type: object + description: + description: Description is a user friendly description of this Rollout. + type: string + packageToTargetMatcher: + description: PackageToTargetMatcher specifies the clusters that will + receive a specific package. + properties: + matchExpression: + type: string + type: + enum: + - AllClusters + - Custom + type: string + required: + - type + type: object + packages: + description: Packages source for this Rollout. + properties: + github: + description: GitHubSource defines the packages source in Git. + properties: + selector: + description: GitHubSelector defines the selector to apply + to Git. + properties: + directory: + type: string + org: + type: string + repo: + type: string + revision: + type: string + secretRef: + description: SecretReference contains the reference to + the secret + properties: + name: + description: Name represents the secret name + type: string + type: object + required: + - org + - repo + - revision + type: object + required: + - selector + type: object + sourceType: + enum: + - GitHub + type: string + required: + - github + - sourceType + type: object + strategy: + description: Strategy specifies the rollout strategy to use for this + rollout. + properties: + allAtOnce: + type: object + progressive: + description: StrategyProgressive defines the progressive rollout + strategy to use. + properties: + name: + description: Name of the ProgressiveRolloutStrategy to use. + type: string + namespace: + description: Namespace of the ProgressiveRolloutStrategy to + use. + type: string + pauseAfterWave: + description: PauseAfterWave represents the highest wave the + strategy will deploy. + properties: + waveName: + description: WaveName represents name of the wave defined + in the ProgressiveRolloutStrategy. + type: string + required: + - waveName + type: object + required: + - name + - namespace + type: object + rollingUpdate: + properties: + maxConcurrent: + format: int64 + type: integer + required: + - maxConcurrent + type: object + type: + enum: + - AllAtOnce + - RollingUpdate + - Progressive + type: string + required: + - type + type: object + syncTemplate: + description: SyncTemplate defines the type and attributes for the + RSync object used to syncing the packages. + properties: + repoSync: + properties: + git: + properties: + auth: + type: string + branch: + type: string + dir: + type: string + gcpServiceAccountEmail: + type: string + noSSLVerify: + type: boolean + period: + type: string + proxy: + type: string + repo: + type: string + revision: + type: string + secretRef: + description: SecretReference contains the reference to + the secret + properties: + name: + description: Name represents the secret name + type: string + type: object + required: + - auth + - repo + type: object + sourceFormat: + type: string + type: object + rootSync: + description: RootSyncTemplate represent the sync template for + RootSync. + properties: + git: + properties: + auth: + type: string + branch: + type: string + dir: + type: string + gcpServiceAccountEmail: + type: string + noSSLVerify: + type: boolean + period: + type: string + proxy: + type: string + repo: + type: string + revision: + type: string + secretRef: + description: SecretReference contains the reference to + the secret + properties: + name: + description: Name represents the secret name + type: string + type: object + required: + - auth + - repo + type: object + sourceFormat: + type: string + type: object + type: + enum: + - RootSync + - RepoSync + type: string + required: + - type + type: object + targets: + description: Targets specifies the clusters that will receive the + KRM config packages. + properties: + selector: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An + empty label selector matches all objects. A null label selector + matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If + the operator is In or NotIn, the values array must + be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A + single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is "key", + the operator is "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + required: + - clusters + - packageToTargetMatcher + - packages + - strategy + type: object + status: + description: RolloutStatus defines the observed state of Rollout + properties: + clusterStatuses: + items: + properties: + name: + type: string + packageStatus: + properties: + packageId: + type: string + status: + type: string + syncStatus: + type: string + required: + - packageId + - status + - syncStatus + type: object + required: + - name + - packageStatus + type: object + type: array + conditions: + description: Conditions describes the reconciliation state of the + object. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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 + observedGeneration: + format: int64 + type: integer + overall: + type: string + waveStatuses: + items: + properties: + clusterStatuses: + items: + properties: + name: + type: string + packageStatus: + properties: + packageId: + type: string + status: + type: string + syncStatus: + type: string + required: + - packageId + - status + - syncStatus + type: object + required: + - name + - packageStatus + type: object + type: array + name: + type: string + paused: + type: boolean + status: + type: string + required: + - name + - status + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/rollouts/config/crd/kustomization.yaml b/rollouts/config/crd/kustomization.yaml new file mode 100644 index 0000000000..2b9931e14c --- /dev/null +++ b/rollouts/config/crd/kustomization.yaml @@ -0,0 +1,41 @@ +# Copyright 2022 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/gitops.kpt.dev_rollouts.yaml +- bases/gitops.kpt.dev_remoterootsyncs.yaml +- bases/gitops.kpt.dev_progressiverolloutstrategies.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_rollouts.yaml +#- patches/webhook_in_remoterootsyncs.yaml +#- patches/webhook_in_progressiverolloutstrategies.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_rollouts.yaml +#- patches/cainjection_in_remoterootsyncs.yaml +#- patches/cainjection_in_progressiverolloutstrategies.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/rollouts/config/crd/kustomizeconfig.yaml b/rollouts/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000000..6eff1bf1c4 --- /dev/null +++ b/rollouts/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 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/rollouts/config/crd/patches/cainjection_in_progressiverolloutstrategies.yaml b/rollouts/config/crd/patches/cainjection_in_progressiverolloutstrategies.yaml new file mode 100644 index 0000000000..85301ee3db --- /dev/null +++ b/rollouts/config/crd/patches/cainjection_in_progressiverolloutstrategies.yaml @@ -0,0 +1,21 @@ +# Copyright 2023 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 following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: progressiverolloutstrategies.gitops.kpt.dev diff --git a/rollouts/config/crd/patches/cainjection_in_remoterootsyncs.yaml b/rollouts/config/crd/patches/cainjection_in_remoterootsyncs.yaml new file mode 100644 index 0000000000..4130f70a92 --- /dev/null +++ b/rollouts/config/crd/patches/cainjection_in_remoterootsyncs.yaml @@ -0,0 +1,21 @@ +# Copyright 2023 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 following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: remoterootsyncs.gitops.kpt.dev diff --git a/rollouts/config/crd/patches/cainjection_in_rollouts.yaml b/rollouts/config/crd/patches/cainjection_in_rollouts.yaml new file mode 100644 index 0000000000..45064190eb --- /dev/null +++ b/rollouts/config/crd/patches/cainjection_in_rollouts.yaml @@ -0,0 +1,21 @@ +# Copyright 2022 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 following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: rollouts.gitops.kpt.dev diff --git a/rollouts/config/crd/patches/webhook_in_progressiverolloutstrategies.yaml b/rollouts/config/crd/patches/webhook_in_progressiverolloutstrategies.yaml new file mode 100644 index 0000000000..3db721765a --- /dev/null +++ b/rollouts/config/crd/patches/webhook_in_progressiverolloutstrategies.yaml @@ -0,0 +1,30 @@ +# Copyright 2023 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 following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: progressiverolloutstrategies.gitops.kpt.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/rollouts/config/crd/patches/webhook_in_remoterootsyncs.yaml b/rollouts/config/crd/patches/webhook_in_remoterootsyncs.yaml new file mode 100644 index 0000000000..8924ef9bac --- /dev/null +++ b/rollouts/config/crd/patches/webhook_in_remoterootsyncs.yaml @@ -0,0 +1,30 @@ +# Copyright 2023 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 following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: remoterootsyncs.gitops.kpt.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/rollouts/config/crd/patches/webhook_in_rollouts.yaml b/rollouts/config/crd/patches/webhook_in_rollouts.yaml new file mode 100644 index 0000000000..8df995f5b1 --- /dev/null +++ b/rollouts/config/crd/patches/webhook_in_rollouts.yaml @@ -0,0 +1,30 @@ +# Copyright 2022 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 following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: rollouts.gitops.kpt.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/rollouts/config/default/kustomization.yaml b/rollouts/config/default/kustomization.yaml new file mode 100644 index 0000000000..8e3174823f --- /dev/null +++ b/rollouts/config/default/kustomization.yaml @@ -0,0 +1,86 @@ +# Copyright 2022 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: rollouts-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: rollouts- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager +# [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 + +patchesStrategicMerge: +# 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. +- manager_auth_proxy_patch.yaml + + + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- 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 +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/rollouts/config/default/manager_auth_proxy_patch.yaml b/rollouts/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000000..6cf4a30a8f --- /dev/null +++ b/rollouts/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,69 @@ +# Copyright 2022 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: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + 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: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/rollouts/config/default/manager_config_patch.yaml b/rollouts/config/default/manager_config_patch.yaml new file mode 100644 index 0000000000..f07552ed3e --- /dev/null +++ b/rollouts/config/default/manager_config_patch.yaml @@ -0,0 +1,24 @@ +# Copyright 2022 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/rollouts/config/manager/kustomization.yaml b/rollouts/config/manager/kustomization.yaml new file mode 100644 index 0000000000..4e0ae586c9 --- /dev/null +++ b/rollouts/config/manager/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2022 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: controller + newTag: latest diff --git a/rollouts/config/manager/manager.yaml b/rollouts/config/manager/manager.yaml new file mode 100644 index 0000000000..38945b15c8 --- /dev/null +++ b/rollouts/config/manager/manager.yaml @@ -0,0 +1,116 @@ +# Copyright 2022 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: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + 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: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(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 + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + 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 + # TODO(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: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/rollouts/config/prometheus/kustomization.yaml b/rollouts/config/prometheus/kustomization.yaml new file mode 100644 index 0000000000..26122b33eb --- /dev/null +++ b/rollouts/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright 2022 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/rollouts/config/prometheus/monitor.yaml b/rollouts/config/prometheus/monitor.yaml new file mode 100644 index 0000000000..1bbac5de89 --- /dev/null +++ b/rollouts/config/prometheus/monitor.yaml @@ -0,0 +1,40 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/auth_proxy_client_clusterrole.yaml b/rollouts/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 0000000000..3892367780 --- /dev/null +++ b/rollouts/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,30 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/rollouts/config/rbac/auth_proxy_role.yaml b/rollouts/config/rbac/auth_proxy_role.yaml new file mode 100644 index 0000000000..cc9cd5d61b --- /dev/null +++ b/rollouts/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,38 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/auth_proxy_role_binding.yaml b/rollouts/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 0000000000..862d6bd10a --- /dev/null +++ b/rollouts/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/auth_proxy_service.yaml b/rollouts/config/rbac/auth_proxy_service.yaml new file mode 100644 index 0000000000..7b368eb291 --- /dev/null +++ b/rollouts/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,35 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/kustomization.yaml b/rollouts/config/rbac/kustomization.yaml new file mode 100644 index 0000000000..5171454a23 --- /dev/null +++ b/rollouts/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright 2022 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 +# 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 diff --git a/rollouts/config/rbac/leader_election_role.yaml b/rollouts/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000000..1a008d5c09 --- /dev/null +++ b/rollouts/config/rbac/leader_election_role.yaml @@ -0,0 +1,58 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/leader_election_role_binding.yaml b/rollouts/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000000..8413ac6da1 --- /dev/null +++ b/rollouts/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/progressiverolloutstrategy_editor_role.yaml b/rollouts/config/rbac/progressiverolloutstrategy_editor_role.yaml new file mode 100644 index 0000000000..62a9604b51 --- /dev/null +++ b/rollouts/config/rbac/progressiverolloutstrategy_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2023 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 progressiverolloutstrategies. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: progressiverolloutstrategy-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: progressiverolloutstrategy-editor-role +rules: +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies/status + verbs: + - get diff --git a/rollouts/config/rbac/progressiverolloutstrategy_viewer_role.yaml b/rollouts/config/rbac/progressiverolloutstrategy_viewer_role.yaml new file mode 100644 index 0000000000..9af3a11fdc --- /dev/null +++ b/rollouts/config/rbac/progressiverolloutstrategy_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2023 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 progressiverolloutstrategies. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: progressiverolloutstrategy-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: progressiverolloutstrategy-viewer-role +rules: +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies + verbs: + - get + - list + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies/status + verbs: + - get diff --git a/rollouts/config/rbac/remoterootsync_editor_role.yaml b/rollouts/config/rbac/remoterootsync_editor_role.yaml new file mode 100644 index 0000000000..dd7ecbccc5 --- /dev/null +++ b/rollouts/config/rbac/remoterootsync_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2023 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 remoterootsyncs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: remoterootsync-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: remoterootsync-editor-role +rules: +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs/status + verbs: + - get diff --git a/rollouts/config/rbac/remoterootsync_viewer_role.yaml b/rollouts/config/rbac/remoterootsync_viewer_role.yaml new file mode 100644 index 0000000000..172f5bd1d8 --- /dev/null +++ b/rollouts/config/rbac/remoterootsync_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2023 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 remoterootsyncs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: remoterootsync-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: remoterootsync-viewer-role +rules: +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs + verbs: + - get + - list + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs/status + verbs: + - get diff --git a/rollouts/config/rbac/role.yaml b/rollouts/config/rbac/role.yaml new file mode 100644 index 0000000000..cb19f9c756 --- /dev/null +++ b/rollouts/config/rbac/role.yaml @@ -0,0 +1,107 @@ +# Copyright 2023 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: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - container.cnrm.cloud.google.com + resources: + - containerclusters + verbs: + - get + - list + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies/finalizers + verbs: + - update +- apiGroups: + - gitops.kpt.dev + resources: + - progressiverolloutstrategies/status + verbs: + - get + - patch + - update +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs/finalizers + verbs: + - update +- apiGroups: + - gitops.kpt.dev + resources: + - remoterootsyncs/status + verbs: + - get + - patch + - update +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts/finalizers + verbs: + - update +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts/status + verbs: + - get + - patch + - update diff --git a/rollouts/config/rbac/role_binding.yaml b/rollouts/config/rbac/role_binding.yaml new file mode 100644 index 0000000000..afc6db2b62 --- /dev/null +++ b/rollouts/config/rbac/role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 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: rollouts + app.kubernetes.io/part-of: rollouts + 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/rollouts/config/rbac/rollout_editor_role.yaml b/rollouts/config/rbac/rollout_editor_role.yaml new file mode 100644 index 0000000000..1a6da05f72 --- /dev/null +++ b/rollouts/config/rbac/rollout_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright 2022 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 rollouts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: rollout-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: rollout-editor-role +rules: +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts/status + verbs: + - get diff --git a/rollouts/config/rbac/rollout_viewer_role.yaml b/rollouts/config/rbac/rollout_viewer_role.yaml new file mode 100644 index 0000000000..fdba6c7290 --- /dev/null +++ b/rollouts/config/rbac/rollout_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright 2022 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 rollouts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: rollout-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: rollout-viewer-role +rules: +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts + verbs: + - get + - list + - watch +- apiGroups: + - gitops.kpt.dev + resources: + - rollouts/status + verbs: + - get diff --git a/rollouts/config/rbac/service_account.yaml b/rollouts/config/rbac/service_account.yaml new file mode 100644 index 0000000000..316b3d03ce --- /dev/null +++ b/rollouts/config/rbac/service_account.yaml @@ -0,0 +1,26 @@ +# Copyright 2022 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 + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: rollouts + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/rollouts/config/samples/container_cluster_catalina.yaml b/rollouts/config/samples/container_cluster_catalina.yaml new file mode 100644 index 0000000000..58bfd80222 --- /dev/null +++ b/rollouts/config/samples/container_cluster_catalina.yaml @@ -0,0 +1,29 @@ +# Copyright 2023 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: container.cnrm.cloud.google.com/v1beta1 +kind: ContainerCluster +metadata: + name: catalina-dev + namespace: config-control + labels: + env: dev + location/island: catalina + location/state: california +spec: + description: Catalina dev autopilot cluster + enableAutopilot: true + location: us-central1 + releaseChannel: + channel: REGULAR diff --git a/rollouts/config/samples/container_cluster_kauai.yaml b/rollouts/config/samples/container_cluster_kauai.yaml new file mode 100644 index 0000000000..6da653402d --- /dev/null +++ b/rollouts/config/samples/container_cluster_kauai.yaml @@ -0,0 +1,29 @@ +# Copyright 2023 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: container.cnrm.cloud.google.com/v1beta1 +kind: ContainerCluster +metadata: + name: kauai + namespace: config-control + labels: + env: dev + location/island: kauai + location/state: hawaii +spec: + description: Kauai autopilot cluster + enableAutopilot: true + location: us-central1 + releaseChannel: + channel: REGULAR diff --git a/rollouts/config/samples/container_cluster_lanai.yaml b/rollouts/config/samples/container_cluster_lanai.yaml new file mode 100644 index 0000000000..6c9c4438f7 --- /dev/null +++ b/rollouts/config/samples/container_cluster_lanai.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 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: container.cnrm.cloud.google.com/v1beta1 +kind: ContainerCluster +metadata: + name: lanai + namespace: config-control + labels: + env: dev + location/island: lanai + location/state: hawaii +spec: + description: Lanai autopilot cluster + enableAutopilot: true + location: us-central1 + releaseChannel: + channel: REGULAR diff --git a/rollouts/config/samples/container_cluster_maui.yaml b/rollouts/config/samples/container_cluster_maui.yaml new file mode 100644 index 0000000000..a1d2d63794 --- /dev/null +++ b/rollouts/config/samples/container_cluster_maui.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 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: container.cnrm.cloud.google.com/v1beta1 +kind: ContainerCluster +metadata: + name: maui + namespace: config-control + labels: + env: prod + location/island: maui + location/state: hawaii +spec: + description: Maui autopilot cluster + enableAutopilot: true + location: us-central1 + releaseChannel: + channel: REGULAR diff --git a/rollouts/config/samples/container_cluster_oahu.yaml b/rollouts/config/samples/container_cluster_oahu.yaml new file mode 100644 index 0000000000..f32a9662da --- /dev/null +++ b/rollouts/config/samples/container_cluster_oahu.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 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: container.cnrm.cloud.google.com/v1beta1 +kind: ContainerCluster +metadata: + name: oahu + namespace: config-control + labels: + env: staging + location/island: oahu + location/state: hawaii +spec: + description: Oahu autopilot cluster + enableAutopilot: true + location: us-central1 + releaseChannel: + channel: REGULAR diff --git a/rollouts/config/samples/gitops_rollout_cert_manager.yaml b/rollouts/config/samples/gitops_rollout_cert_manager.yaml new file mode 100644 index 0000000000..560d73699d --- /dev/null +++ b/rollouts/config/samples/gitops_rollout_cert_manager.yaml @@ -0,0 +1,38 @@ +# Copyright 2022 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: gitops.kpt.dev/v1alpha1 +kind: Rollout +metadata: + name: cert-manager-rollout +spec: + description: kpt samples rollout + clusters: + sourceType: KCC + packages: + sourceType: GitHub + github: + selector: + org: droot + repo: kpt-packages + directory: cert-manager + revision: main + targets: + selector: + matchExpressions: + - {key: env, operator: In, values: [dev, staging]} + packageToTargetMatcher: + type: AllClusters + strategy: + type: AllAtOnce diff --git a/rollouts/config/samples/gitops_rollout_cert_manager_progressive.yaml b/rollouts/config/samples/gitops_rollout_cert_manager_progressive.yaml new file mode 100644 index 0000000000..9d83d28fff --- /dev/null +++ b/rollouts/config/samples/gitops_rollout_cert_manager_progressive.yaml @@ -0,0 +1,47 @@ +# Copyright 2022 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: gitops.kpt.dev/v1alpha1 +kind: Rollout +metadata: + name: cm-progressive +spec: + description: kpt samples rollout + clusters: + sourceType: KCC + packages: + sourceType: GitHub + github: + selector: + org: droot + repo: kpt-packages + directory: cert-manager + revision: main + targets: + selector: + matchExpressions: + - {key: env, operator: In, values: [dev, staging]} + packageToTargetMatcher: + type: AllClusters + syncTemplate: + type: RootSync + rootSync: + sourceFormat: unstructured + strategy: + type: Progressive + progressive: + name: cluster-addons-default-rollout + namespace: default + pauseAfterWave: + waveName: dev clusters diff --git a/rollouts/config/samples/gitops_rollout_kpt_samples.yaml b/rollouts/config/samples/gitops_rollout_kpt_samples.yaml new file mode 100644 index 0000000000..ec88fbedd3 --- /dev/null +++ b/rollouts/config/samples/gitops_rollout_kpt_samples.yaml @@ -0,0 +1,38 @@ +# Copyright 2022 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: gitops.kpt.dev/v1alpha1 +kind: Rollout +metadata: + name: rollout-kpt-samples +spec: + description: kpt samples rollout + clusters: + sourceType: KCC + packages: + sourceType: GitHub + github: + selector: + org: GoogleContainerTools + repo: kpt-samples + directory: "*" + revision: main + targets: + selector: + matchExpressions: + - {key: location/island, operator: In, values: [oahu, maui]} + packageToTargetMatcher: + type: AllClusters + strategy: + type: AllAtOnce diff --git a/rollouts/config/samples/gitops_rollout_oahu.yaml b/rollouts/config/samples/gitops_rollout_oahu.yaml new file mode 100644 index 0000000000..50a20d6194 --- /dev/null +++ b/rollouts/config/samples/gitops_rollout_oahu.yaml @@ -0,0 +1,40 @@ +# Copyright 2022 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: gitops.kpt.dev/v1alpha1 +kind: Rollout +metadata: + name: rollout-oahu +spec: + description: oahu rollout + clusters: + sourceType: KCC + packages: + sourceType: GitHub + github: + selector: + org: droot + repo: oahu + directory: "/" + revision: main + packageToTargetMatcher: + type: AllClusters + targets: + selector: + matchLabels: + location/state: hawaii + strategy: + type: RollingUpdate + rollingUpdate: + maxConcurrent: 2 diff --git a/rollouts/config/samples/gitops_rollout_repo_selector.yaml b/rollouts/config/samples/gitops_rollout_repo_selector.yaml new file mode 100644 index 0000000000..b3bedb709a --- /dev/null +++ b/rollouts/config/samples/gitops_rollout_repo_selector.yaml @@ -0,0 +1,39 @@ +# Copyright 2022 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: gitops.kpt.dev/v1alpha1 +kind: Rollout +metadata: + name: rollout-repo-selector +spec: + description: repo selector rollout + clusters: + sourceType: KCC + packages: + sourceType: GitHub + github: + selector: + org: droot + repo: "*-apps" + directory: "*" + revision: main + targets: + selector: + matchExpressions: + - {key: location/state, operator: In, values: [hawaii]} + packageToTargetMatcher: + type: Custom + matchExpression: rolloutPackage.repo == cluster.labels['location/island'] + '-apps' + strategy: + type: AllAtOnce diff --git a/rollouts/config/samples/gitops_v1alpha1_progressiverolloutstrategy.yaml b/rollouts/config/samples/gitops_v1alpha1_progressiverolloutstrategy.yaml new file mode 100644 index 0000000000..556572434d --- /dev/null +++ b/rollouts/config/samples/gitops_v1alpha1_progressiverolloutstrategy.yaml @@ -0,0 +1,32 @@ +# Copyright 2023 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: gitops.kpt.dev/v1alpha1 +kind: ProgressiveRolloutStrategy +metadata: + name: cluster-addons-default-rollout +spec: + waves: + - name: "dev clusters" + targets: + selector: + matchLabels: + env: dev + maxConcurrent: 2 + - name: "staging clusters" + targets: + selector: + matchLabels: + env: staging + maxConcurrent: 1 diff --git a/rollouts/config/samples/gitops_v1alpha1_remoterootsync.yaml b/rollouts/config/samples/gitops_v1alpha1_remoterootsync.yaml new file mode 100644 index 0000000000..a610e0e084 --- /dev/null +++ b/rollouts/config/samples/gitops_v1alpha1_remoterootsync.yaml @@ -0,0 +1,36 @@ +# Copyright 2023 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: gitops.kpt.dev/v1alpha1 +kind: RemoteRootSync +metadata: + labels: + app.kubernetes.io/name: remoterootsync + app.kubernetes.io/instance: remoterootsync-sample + app.kubernetes.io/part-of: rollouts + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: rollouts + name: remoterootsync-sample +spec: + # TODO(user): Add fields here + clusterRef: + name: dev-1 + template: + spec: + sourceFormat: unstructured + git: + repo: https://github.com/droot/oahu.git + branch: main + revision: main + auth: none diff --git a/rollouts/controllers/progressiverolloutstrategy_controller.go b/rollouts/controllers/progressiverolloutstrategy_controller.go new file mode 100644 index 0000000000..a5516a84e6 --- /dev/null +++ b/rollouts/controllers/progressiverolloutstrategy_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "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" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" +) + +// ProgressiveRolloutStrategyReconciler reconciles a ProgressiveRolloutStrategy object +type ProgressiveRolloutStrategyReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=progressiverolloutstrategies,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=progressiverolloutstrategies/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=progressiverolloutstrategies/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 ProgressiveRolloutStrategy 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.13.1/pkg/reconcile +func (r *ProgressiveRolloutStrategyReconciler) 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 *ProgressiveRolloutStrategyReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&gitopsv1alpha1.ProgressiveRolloutStrategy{}). + Complete(r) +} diff --git a/rollouts/controllers/remoterootsync_controller.go b/rollouts/controllers/remoterootsync_controller.go new file mode 100644 index 0000000000..5a6be06797 --- /dev/null +++ b/rollouts/controllers/remoterootsync_controller.go @@ -0,0 +1,343 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + 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/dynamic" + "k8s.io/klog" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/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" + + gkeclusterapis "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/container/v1beta1" + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/clusterstore" +) + +var ( + rootSyncNamespace = "config-management-system" + rootSyncGVK = schema.GroupVersionKind{ + Group: "configsync.gke.io", + Version: "v1beta1", + Kind: "RootSync", + } + rootSyncGVR = schema.GroupVersionResource{ + Group: "configsync.gke.io", + Version: "v1beta1", + Resource: "rootsyncs", + } + + remoteRootSyncNameLabel = "gitops.kpt.dev/remoterootsync-name" + remoteRootSyncNamespaceLabel = "gitops.kpt.dev/remoterootsync-namespace" +) + +// RemoteRootSyncReconciler reconciles a RemoteRootSync object +type RemoteRootSyncReconciler struct { + client.Client + Scheme *runtime.Scheme + + store *clusterstore.ClusterStore + + // channel is where watchers put events to trigger new reconcilations based + // on watch events from target clusters. + channel chan event.GenericEvent + + mutex sync.Mutex + + watchers map[gitopsv1alpha1.ClusterRef]*watcher +} + +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=remoterootsyncs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=remoterootsyncs/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=remoterootsyncs/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 RemoteRootSync 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.13.1/pkg/reconcile +func (r *RemoteRootSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("reconciling", "key", req.NamespacedName) + + var remoterootsync gitopsv1alpha1.RemoteRootSync + if err := r.Get(ctx, req.NamespacedName, &remoterootsync); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + myFinalizerName := "remoterootsync.gitops.kpt.dev/finalizer" + if remoterootsync.ObjectMeta.DeletionTimestamp.IsZero() { + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + if !controllerutil.ContainsFinalizer(&remoterootsync, myFinalizerName) { + controllerutil.AddFinalizer(&remoterootsync, myFinalizerName) + if err := r.Update(ctx, &remoterootsync); err != nil { + return ctrl.Result{}, fmt.Errorf("error adding finalizer: %w", err) + } + } + } else { + // The object is being deleted + if controllerutil.ContainsFinalizer(&remoterootsync, myFinalizerName) { + // our finalizer is present, so lets handle any external dependency + if err := r.deleteExternalResources(ctx, &remoterootsync); err != nil { + // if fail to delete the external dependency here, return with error + // so that it can be retried + return ctrl.Result{}, fmt.Errorf("have problem to delete external resource: %w", err) + } + // Make sure we stop any watches that are no longer needed. + r.pruneWatches(req.NamespacedName, &remoterootsync.Spec.ClusterRef) + // remove our finalizer from the list and update it. + controllerutil.RemoveFinalizer(&remoterootsync, myFinalizerName) + if err := r.Update(ctx, &remoterootsync); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update %s after delete finalizer: %w", req.Name, err) + } + } + // Stop reconciliation as the item is being deleted + return ctrl.Result{}, nil + } + + clusterRef := &remoterootsync.Spec.ClusterRef + dynCl, err := r.getDynamicClientForCluster(ctx, clusterRef) + if err != nil { + return ctrl.Result{}, err + } + r.setupWatches(ctx, dynCl, remoterootsync.Name, remoterootsync.Namespace, remoterootsync.Spec.ClusterRef) + + if err := r.patchRootSync(ctx, dynCl, req.Name, &remoterootsync); err != nil { + return ctrl.Result{}, err + } + + syncStatus, err := checkSyncStatus(ctx, dynCl, req.Name) + if err != nil { + return ctrl.Result{}, err + } + + if err := r.updateStatus(ctx, &remoterootsync, syncStatus); err != nil { + klog.Errorf("failed to update status: %v", err) + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *RemoteRootSyncReconciler) updateStatus(ctx context.Context, rrs *gitopsv1alpha1.RemoteRootSync, syncStatus string) error { + logger := log.FromContext(ctx) + + // Don't update if there are no changes. + if rrs.Status.SyncStatus == syncStatus && rrs.Generation == rrs.Status.ObservedGeneration { + return nil + } + logger.Info("updating the status") + rrs.Status.SyncStatus = syncStatus + rrs.Status.ObservedGeneration = rrs.Generation + return r.Client.Status().Update(ctx, rrs) +} + +// patchRootSync patches the RootSync in the remote clusters targeted by +// the clusterRefs based on the latest revision of the template in the RemoteRootSync. +func (r *RemoteRootSyncReconciler) patchRootSync(ctx context.Context, client dynamic.Interface, name string, rrs *gitopsv1alpha1.RemoteRootSync) error { + newRootSync, err := BuildObjectsToApply(rrs) + if err != nil { + return err + } + data, err := json.Marshal(newRootSync) + if err != nil { + return fmt.Errorf("failed to encode root sync to JSON: %w", err) + } + _, err = client.Resource(rootSyncGVR).Namespace(rootSyncNamespace).Patch(ctx, name, types.ApplyPatchType, data, metav1.PatchOptions{FieldManager: name}) + if err != nil { + return fmt.Errorf("failed to patch RootSync: %w", err) + } + klog.Infof("Create/Update resource %s as", name) + return nil +} + +// setupWatches makes sure we have the necessary watches running against +// the remote clusters we care about. +func (r *RemoteRootSyncReconciler) setupWatches(ctx context.Context, client dynamic.Interface, rrsName, ns string, clusterRef gitopsv1alpha1.ClusterRef) { + r.mutex.Lock() + defer r.mutex.Unlock() + nn := types.NamespacedName{ + Namespace: ns, + Name: rrsName, + } + + // If we already have a watch running, make sure we have the current RootSyncSet + // listed in the liens map. + if w, found := r.watchers[clusterRef]; found { + w.liens[nn] = struct{}{} + return + } + + // Since we don't currently have a watch running, create a new watcher + // and add it to the map of watchers. + watcherCtx, cancelFunc := context.WithCancel(context.Background()) + w := &watcher{ + clusterRef: clusterRef, + ctx: watcherCtx, + cancelFunc: cancelFunc, + client: client, + channel: r.channel, + liens: map[types.NamespacedName]struct{}{ + nn: {}, + }, + } + klog.Infof("Creating watcher for %v", clusterRef) + go w.watch() + r.watchers[clusterRef] = w +} + +// pruneWatches removes the current RootSyncSet from the liens map of all watchers +// that it no longer needs. If any of the watchers are no longer used by any RootSyncSets, +// they are shut down. +func (r *RemoteRootSyncReconciler) pruneWatches(rrsnn types.NamespacedName, clusterRef *gitopsv1alpha1.ClusterRef) { + r.mutex.Lock() + defer r.mutex.Unlock() + klog.Infof("Pruning watches for %s which has %v clusterRef", rrsnn.String(), clusterRef) + + // Look through all watchers to check if it used to be needed by the RootSyncSet + // but is no longer. + w, found := r.watchers[*clusterRef] + if !found { + return + } + + // Delete the current RootSyncSet from the list of liens (it it exists) + delete(w.liens, rrsnn) + // If no other RootSyncSets need the watch, stop it and remove the watcher from the map. + if len(w.liens) == 0 { + w.cancelFunc() + delete(r.watchers, *clusterRef) + } +} + +// BuildObjectsToApply config root sync +func BuildObjectsToApply(remoterootsync *gitopsv1alpha1.RemoteRootSync) (*unstructured.Unstructured, error) { + newRootSync, err := runtime.DefaultUnstructuredConverter.ToUnstructured(remoterootsync.Spec.Template) + if err != nil { + return nil, err + } + u := unstructured.Unstructured{Object: newRootSync} + u.SetGroupVersionKind(rootSyncGVK) + u.SetName(remoterootsync.Name) + u.SetNamespace(rootSyncNamespace) + u.SetLabels(map[string]string{ + remoteRootSyncNameLabel: remoterootsync.Name, + remoteRootSyncNamespaceLabel: remoterootsync.Namespace, + }) + if err != nil { + return nil, fmt.Errorf("failed to convert to unstructured type: %w", err) + } + return &u, nil +} + +func (r *RemoteRootSyncReconciler) deleteExternalResources(ctx context.Context, remoterootsync *gitopsv1alpha1.RemoteRootSync) error { + clusterRef := &remoterootsync.Spec.ClusterRef + dynCl, err := r.getDynamicClientForCluster(ctx, clusterRef) + if err != nil { + return err + } + + klog.Infof("deleting external resource %s ...", remoterootsync.Name) + err = dynCl.Resource(rootSyncGVR).Namespace("config-management-system").Delete(ctx, remoterootsync.Name, metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + klog.Infof("external resource %s delete Done!", remoterootsync.Name) + return err +} + +func (r *RemoteRootSyncReconciler) getDynamicClientForCluster(ctx context.Context, clusterRef *gitopsv1alpha1.ClusterRef) (dynamic.Interface, error) { + restConfig, err := r.store.GetRESTConfig(ctx, clusterRef.Name) + if err != nil { + return nil, err + } + + dynamicClient, err := dynamic.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + return dynamicClient, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RemoteRootSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.channel = make(chan event.GenericEvent, 10) + r.watchers = make(map[gitopsv1alpha1.ClusterRef]*watcher) + r.Client = mgr.GetClient() + gkeclusterapis.AddToScheme(mgr.GetScheme()) + + if err := gitopsv1alpha1.AddToScheme(mgr.GetScheme()); err != nil { + return err + } + + clusterStore, err := clusterstore.NewClusterStore(r.Client, mgr.GetConfig()) + if err != nil { + return err + } + r.store = clusterStore + + return ctrl.NewControllerManagedBy(mgr). + For(&gitopsv1alpha1.RemoteRootSync{}). + Watches( + &source.Channel{Source: r.channel}, + handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request { + var rrsName string + var rrsNamespace string + if o.GetLabels() != nil { + rrsName = o.GetLabels()[remoteRootSyncNameLabel] + rrsNamespace = o.GetLabels()[remoteRootSyncNamespaceLabel] + } + if rrsName == "" || rrsNamespace == "" { + return []reconcile.Request{} + } + klog.Infof("Resource %s contains a label for %s", o.GetName(), rrsName) + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: rrsNamespace, + Name: rrsName, + }, + }, + } + }), + ). + Complete(r) +} diff --git a/rollouts/controllers/rollout_controller.go b/rollouts/controllers/rollout_controller.go new file mode 100644 index 0000000000..9e1f676f7d --- /dev/null +++ b/rollouts/controllers/rollout_controller.go @@ -0,0 +1,898 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "flag" + "fmt" + "math" + "sort" + "strings" + "sync" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + gkeclusterapis "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/container/v1beta1" + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/clusterstore" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/packageclustermatcher" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/packagediscovery" +) + +type Options struct { +} + +func (o *Options) InitDefaults() { +} + +func (o *Options) BindFlags(prefix string, flags *flag.FlagSet) { +} + +const ( + rolloutLabel = "gitops.kpt.dev/rollout-name" +) + +var ( + kccClusterGVK = schema.GroupVersionKind{ + Group: "container.cnrm.cloud.google.com", + Version: "v1beta1", + Kind: "ContainerCluster", + } +) + +// RolloutReconciler reconciles a Rollout object +type RolloutReconciler struct { + client.Client + + store *clusterstore.ClusterStore + + Scheme *runtime.Scheme + + mutex sync.Mutex + packageDiscoveryCache map[types.NamespacedName]*packagediscovery.PackageDiscovery +} + +//+kubebuilder:rbac:groups=container.cnrm.cloud.google.com,resources=containerclusters,verbs=get;list;watch +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=rollouts,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=rollouts/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=gitops.kpt.dev,resources=rollouts/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 Rollout 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.13.1/pkg/reconcile +// +// Dumb reconciliation of Rollout API includes the following: +// Fetch the READY kcc clusters. +// For each kcc cluster, fetch RootSync objects in each of the KCC clusters. +func (r *RolloutReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + var rollout gitopsv1alpha1.Rollout + + logger.Info("reconciling", "key", req.NamespacedName) + + if err := r.Get(ctx, req.NamespacedName, &rollout); err != nil { + // we'll ignore not-found errors, since they can't be fixed by an immediate + // requeue (we'll need to wait for a new notification), and we can get them + // on deleted requests. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + myFinalizerName := "gitops.kpt.dev/rollouts" + if rollout.ObjectMeta.DeletionTimestamp.IsZero() { + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + if !controllerutil.ContainsFinalizer(&rollout, myFinalizerName) { + controllerutil.AddFinalizer(&rollout, myFinalizerName) + if err := r.Update(ctx, &rollout); err != nil { + return ctrl.Result{}, fmt.Errorf("error adding finalizer: %w", err) + } + } + } else { + // The object is being deleted + if controllerutil.ContainsFinalizer(&rollout, myFinalizerName) { + func() { + r.mutex.Lock() + defer r.mutex.Unlock() + + // clean cache + delete(r.packageDiscoveryCache, req.NamespacedName) + }() + + // remove our finalizer from the list and update it. + controllerutil.RemoveFinalizer(&rollout, myFinalizerName) + if err := r.Update(ctx, &rollout); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update %s after delete finalizer: %w", req.Name, err) + } + } + // Stop reconciliation as the item is being deleted + return ctrl.Result{}, nil + } + + strategy, err := r.getStrategy(ctx, &rollout) + if err != nil { + return ctrl.Result{}, err + } + + packageDiscoveryClient := r.getPackageDiscoveryClient(req.NamespacedName) + + err = r.reconcileRollout(ctx, &rollout, strategy, packageDiscoveryClient) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *RolloutReconciler) getStrategy(ctx context.Context, rollout *gitopsv1alpha1.Rollout) (*gitopsv1alpha1.ProgressiveRolloutStrategy, error) { + logger := log.FromContext(ctx) + + progressiveStrategy := gitopsv1alpha1.ProgressiveRolloutStrategy{} + progressiveStrategy.Spec = gitopsv1alpha1.ProgressiveRolloutStrategySpec{Waves: []gitopsv1alpha1.Wave{}} + + // validate the strategy as early as possible + switch typ := rollout.Spec.Strategy.Type; typ { + case gitopsv1alpha1.AllAtOnce: + wave := gitopsv1alpha1.Wave{MaxConcurrent: math.MaxInt, Targets: rollout.Spec.Targets} + progressiveStrategy.Spec.Waves = append(progressiveStrategy.Spec.Waves, wave) + + case gitopsv1alpha1.RollingUpdate: + wave := gitopsv1alpha1.Wave{MaxConcurrent: rollout.Spec.Strategy.RollingUpdate.MaxConcurrent, Targets: rollout.Spec.Targets} + progressiveStrategy.Spec.Waves = append(progressiveStrategy.Spec.Waves, wave) + + case gitopsv1alpha1.Progressive: + strategyRef := types.NamespacedName{ + Namespace: rollout.Spec.Strategy.Progressive.Namespace, + Name: rollout.Spec.Strategy.Progressive.Name, + } + if err := r.Get(ctx, strategyRef, &progressiveStrategy); err != nil { + logger.Error(err, "unable to fetch progressive rollout strategy", "strategyref", strategyRef) + // TODO (droot): signal this as a condition in the rollout status + return nil, err + } + + err := r.validateProgressiveRolloutStrategy(ctx, rollout, &progressiveStrategy) + if err != nil { + logger.Error(err, "progressive rollout strategy failed validation", "strategyref", strategyRef) + // TODO (cfry): signal this as a condition in the rollout status + return nil, err + } + + default: + // TODO (droot): signal this as a condition in the rollout status + return nil, fmt.Errorf("%v strategy not supported yet", typ) + } + + return &progressiveStrategy, nil +} + +func (r *RolloutReconciler) validateProgressiveRolloutStrategy(ctx context.Context, rollout *gitopsv1alpha1.Rollout, strategy *gitopsv1alpha1.ProgressiveRolloutStrategy) error { + allClusters, err := r.store.ListClusters(ctx, &rollout.Spec.Clusters, rollout.Spec.Targets.Selector) + if err != nil { + return err + } + + clusterWaveMap := make(map[string]string) + for _, cluster := range allClusters { + clusterWaveMap[cluster.Name] = "" + } + + pauseAfterWaveName := "" + pauseWaveNameFound := false + + if rollout.Spec.Strategy.Progressive != nil { + pauseAfterWaveName = rollout.Spec.Strategy.Progressive.PauseAfterWave.WaveName + } + + for _, wave := range strategy.Spec.Waves { + waveClusters, err := filterClusters(allClusters, wave.Targets.Selector) + if err != nil { + return err + } + + if len(waveClusters) == 0 { + return fmt.Errorf("wave %q does not target any clusters", wave.Name) + } + + for _, cluster := range waveClusters { + currentClusterWave, found := clusterWaveMap[cluster.Name] + if !found { + // this should never happen + return fmt.Errorf("wave %q references cluster %s not selected by the rollout", wave.Name, cluster.Name) + } + + if currentClusterWave != "" { + return fmt.Errorf("a cluster cannot be selected by more than one wave - cluster %s is selected by waves %q and %q", cluster.Name, currentClusterWave, wave.Name) + } + + clusterWaveMap[cluster.Name] = wave.Name + } + + pauseWaveNameFound = pauseWaveNameFound || pauseAfterWaveName == wave.Name + } + + for _, cluster := range allClusters { + wave, _ := clusterWaveMap[cluster.Name] + if wave == "" { + return fmt.Errorf("waves should cover all clusters selected by the rollout - cluster %s is not covered by any waves", cluster.Name) + } + } + + if pauseAfterWaveName != "" && !pauseWaveNameFound { + return fmt.Errorf("%q pause wave not found in progressive rollout strategy", pauseAfterWaveName) + } + + return nil +} + +func (r *RolloutReconciler) getPackageDiscoveryClient(rolloutNamespacedName types.NamespacedName) *packagediscovery.PackageDiscovery { + r.mutex.Lock() + defer r.mutex.Unlock() + + client, found := r.packageDiscoveryCache[rolloutNamespacedName] + if !found { + client = packagediscovery.NewPackageDiscovery(r.Client, rolloutNamespacedName.Namespace) + r.packageDiscoveryCache[rolloutNamespacedName] = client + } + + return client +} + +func (r *RolloutReconciler) reconcileRollout(ctx context.Context, rollout *gitopsv1alpha1.Rollout, strategy *gitopsv1alpha1.ProgressiveRolloutStrategy, packageDiscoveryClient *packagediscovery.PackageDiscovery) error { + logger := log.FromContext(ctx) + + targetClusters, err := r.store.ListClusters(ctx, &rollout.Spec.Clusters, rollout.Spec.Targets.Selector) + discoveredPackages, err := packageDiscoveryClient.GetPackages(ctx, rollout.Spec.Packages) + if err != nil { + logger.Error(err, "failed to discover packages") + return client.IgnoreNotFound(err) + } + logger.Info("discovered packages", "count", len(discoveredPackages), "packages", discoveredPackages) + + packageClusterMatcherClient := packageclustermatcher.NewPackageClusterMatcher(targetClusters, discoveredPackages) + clusterPackages, err := packageClusterMatcherClient.GetClusterPackages(rollout.Spec.PackageToTargetMatcher) + if err != nil { + return err + } + + targets, err := r.computeTargets(ctx, rollout, clusterPackages) + if err != nil { + return err + } + + allClusterStatuses := []gitopsv1alpha1.ClusterStatus{} + waveStatuses := []gitopsv1alpha1.WaveStatus{} + + allWaveTargets, err := r.getWaveTargets(ctx, rollout, targets, targetClusters, strategy.Spec.Waves) + if err != nil { + return err + } + + pauseFutureWaves := false + pauseAfterWaveName := "" + afterPauseAfterWave := false + + if rollout.Spec.Strategy.Progressive != nil { + pauseAfterWaveName = rollout.Spec.Strategy.Progressive.PauseAfterWave.WaveName + } + + for i := range allWaveTargets { + thisWaveTargets := allWaveTargets[i] + waveTargets := thisWaveTargets.Targets + wave := thisWaveTargets.Wave + + thisWaveInProgress, clusterStatuses, err := r.rolloutTargets(ctx, rollout, wave, waveTargets, pauseFutureWaves) + if err != nil { + return err + } + + if thisWaveInProgress { + pauseFutureWaves = true + } + + allClusterStatuses = append(allClusterStatuses, clusterStatuses...) + + waveStatuses = append(waveStatuses, getWaveStatus(wave, clusterStatuses, afterPauseAfterWave)) + + if wave.Name == pauseAfterWaveName { + pauseFutureWaves = true + afterPauseAfterWave = true + } + } + + sortClusterStatuses(allClusterStatuses) + + if err := r.updateStatus(ctx, rollout, waveStatuses, allClusterStatuses); err != nil { + return err + } + return nil +} + +func (r *RolloutReconciler) updateStatus(ctx context.Context, rollout *gitopsv1alpha1.Rollout, waveStatuses []gitopsv1alpha1.WaveStatus, clusterStatuses []gitopsv1alpha1.ClusterStatus) error { + logger := log.FromContext(ctx) + logger.Info("updating the status", "cluster statuses", len(clusterStatuses)) + + rollout.Status.Overall = getOverallStatus(clusterStatuses) + + if len(waveStatuses) > 1 { + rollout.Status.WaveStatuses = waveStatuses + } + + rollout.Status.ClusterStatuses = clusterStatuses + rollout.Status.ObservedGeneration = rollout.Generation + return r.Client.Status().Update(ctx, rollout) +} + +/* +so we compute targets where each target consists of (cluster, package) +compute the RRS corresponding to each (cluster, package) pair +For RRS, name has to be function of cluster-id and package-id. +For RRS, make rootSyncTemplate +*/ +func (r *RolloutReconciler) computeTargets(ctx context.Context, + rollout *gitopsv1alpha1.Rollout, + clusterPackages []packageclustermatcher.ClusterPackages) (*Targets, error) { + + RRSkeysToBeDeleted := map[client.ObjectKey]*gitopsv1alpha1.RemoteRootSync{} + // let's take a look at existing remoterootsyncs + existingRRSs, err := r.listRemoteRootSyncs(ctx, rollout.Name, rollout.Namespace) + if err != nil { + return nil, err + } + + // initially assume all the keys to be deleted + for _, rrs := range existingRRSs { + RRSkeysToBeDeleted[client.ObjectKeyFromObject(rrs)] = rrs + } + + klog.Infof("Found remoterootsyncs: %s", toRemoteRootSyncNames(existingRRSs)) + targets := &Targets{} + // track keys of all the desired remote rootsyncs + for idx, clusterPkg := range clusterPackages { + // TODO: figure out multiple packages per cluster story + if len(clusterPkg.Packages) < 1 { + continue + } + cluster := &clusterPackages[idx].Cluster + clusterName := cluster.Name[strings.LastIndex(cluster.Name, "/")+1:] + pkg := &clusterPkg.Packages[0] + rrs := gitopsv1alpha1.RemoteRootSync{} + key := client.ObjectKey{ + Namespace: rollout.Namespace, + Name: fmt.Sprintf("%s-%s", pkgID(pkg), clusterName), + } + // since this RRS need to exist, remove it from the deletion list + delete(RRSkeysToBeDeleted, key) + // check if this remoterootsync for this package exists or not ? + err := r.Client.Get(ctx, key, &rrs) + if err != nil { + if apierrors.IsNotFound(err) { // rrs is missing + targets.ToBeCreated = append(targets.ToBeCreated, &clusterPackagePair{ + cluster: cluster, + packageRef: pkg, + }) + } else { + // some other error encountered + return nil, err + } + } else { + // remoterootsync already exists + if pkg.Revision != rrs.Spec.Template.Spec.Git.Revision { + rrs.Spec.Template.Spec.Git.Revision = pkg.Revision + // revision has been updated + targets.ToBeUpdated = append(targets.ToBeUpdated, &rrs) + } else { + targets.Unchanged = append(targets.Unchanged, &rrs) + } + } + } + + for _, rrs := range RRSkeysToBeDeleted { + targets.ToBeDeleted = append(targets.ToBeDeleted, rrs) + } + + return targets, nil +} + +func (r *RolloutReconciler) getWaveTargets(ctx context.Context, rollout *gitopsv1alpha1.Rollout, allTargets *Targets, allClusters []clusterstore.Cluster, + allWaves []gitopsv1alpha1.Wave) ([]WaveTarget, error) { + allWaveTargets := []WaveTarget{} + + clusterNameToWaveTarget := make(map[string]*WaveTarget) + + for i := range allWaves { + wave := allWaves[i] + thisWaveTarget := WaveTarget{Wave: &wave, Targets: &Targets{}} + + waveClusters, err := filterClusters(allClusters, wave.Targets.Selector) + if err != nil { + return nil, err + } + + for _, cluster := range waveClusters { + clusterNameToWaveTarget[cluster.Name] = &thisWaveTarget + } + + allWaveTargets = append(allWaveTargets, thisWaveTarget) + } + + for _, toCreate := range allTargets.ToBeCreated { + wavetTargets := clusterNameToWaveTarget[toCreate.cluster.Name].Targets + wavetTargets.ToBeCreated = append(wavetTargets.ToBeCreated, toCreate) + } + + for _, rrs := range allTargets.ToBeUpdated { + wavetTargets := clusterNameToWaveTarget[rrs.Spec.ClusterRef.Name].Targets + wavetTargets.ToBeUpdated = append(wavetTargets.ToBeUpdated, rrs) + } + + for _, rrs := range allTargets.Unchanged { + wavetTargets := clusterNameToWaveTarget[rrs.Spec.ClusterRef.Name].Targets + wavetTargets.Unchanged = append(wavetTargets.Unchanged, rrs) + } + + for _, rrs := range allTargets.ToBeDeleted { + // The remote root sync will be associated back to it's previous wave and then removed as part + // of that wave. If the previous wave the remote root sync cannot be determined, then the remote + // root sync will be removed with the last wave of the rollout. + + waveName, found := findWaveNameForCluster(rollout, rrs.Spec.ClusterRef.Name) + + if !found { + waveName = allWaveTargets[len(allWaveTargets)-1].Wave.Name + } + + for _, waveTarget := range allWaveTargets { + if waveTarget.Wave.Name == waveName { + wavetTargets := waveTarget.Targets + wavetTargets.ToBeDeleted = append(wavetTargets.ToBeDeleted, rrs) + } + } + } + + return allWaveTargets, nil +} + +func (r *RolloutReconciler) rolloutTargets(ctx context.Context, rollout *gitopsv1alpha1.Rollout, wave *gitopsv1alpha1.Wave, targets *Targets, pauseWave bool) (bool, []gitopsv1alpha1.ClusterStatus, error) { + clusterStatuses := []gitopsv1alpha1.ClusterStatus{} + + concurrentUpdates := 0 + maxConcurrent := int(wave.MaxConcurrent) + waiting := "Waiting" + + if pauseWave { + maxConcurrent = 0 + waiting = "Waiting (Upcoming Wave)" + } + + for _, target := range targets.Unchanged { + if !isRRSSynced(target) { + concurrentUpdates++ + } + } + + for _, target := range targets.ToBeCreated { + rootSyncSpec := toRootSyncSpec(target.packageRef) + rrs := newRemoteRootSync(rollout, + gitopsv1alpha1.ClusterRef{Name: target.cluster.Name}, + rootSyncSpec, + pkgID(target.packageRef), + wave.Name, + ) + + if maxConcurrent > concurrentUpdates { + if err := r.Create(ctx, rrs); err != nil { + klog.Warningf("Error creating RemoteRootSync %s: %v", rrs.Name, err) + return false, nil, err + } + concurrentUpdates++ + clusterStatuses = append(clusterStatuses, gitopsv1alpha1.ClusterStatus{ + Name: rrs.Spec.ClusterRef.Name, + PackageStatus: gitopsv1alpha1.PackageStatus{ + PackageID: rrs.Name, + SyncStatus: rrs.Status.SyncStatus, + Status: "Progressing", + }, + }) + } else { + clusterStatuses = append(clusterStatuses, gitopsv1alpha1.ClusterStatus{ + Name: rrs.Spec.ClusterRef.Name, + PackageStatus: gitopsv1alpha1.PackageStatus{ + PackageID: rrs.Name, + SyncStatus: "", + Status: waiting, + }, + }) + } + } + + for _, target := range targets.ToBeUpdated { + if maxConcurrent > concurrentUpdates { + if err := r.Update(ctx, target); err != nil { + klog.Warningf("Error updating RemoteRootSync %s: %v", target.Name, err) + return false, nil, err + } + concurrentUpdates++ + clusterStatuses = append(clusterStatuses, gitopsv1alpha1.ClusterStatus{ + Name: target.Spec.ClusterRef.Name, + PackageStatus: gitopsv1alpha1.PackageStatus{ + PackageID: target.Name, + SyncStatus: target.Status.SyncStatus, + Status: "Progressing", + }, + }) + } else { + clusterStatuses = append(clusterStatuses, gitopsv1alpha1.ClusterStatus{ + Name: target.Spec.ClusterRef.Name, + PackageStatus: gitopsv1alpha1.PackageStatus{ + PackageID: target.Name, + SyncStatus: "OutOfSync", + Status: waiting, + }, + }) + } + } + + for _, target := range targets.ToBeDeleted { + if maxConcurrent > concurrentUpdates { + if err := r.Delete(ctx, target); err != nil { + klog.Warningf("Error deleting RemoteRootSync %s: %v", target.Name, err) + return false, nil, err + } + concurrentUpdates++ + } else { + clusterStatuses = append(clusterStatuses, gitopsv1alpha1.ClusterStatus{ + Name: target.Spec.ClusterRef.Name, + PackageStatus: gitopsv1alpha1.PackageStatus{ + PackageID: target.Name, + SyncStatus: "OutOfSync", + Status: waiting, + }, + }) + } + } + + for _, target := range targets.Unchanged { + status := "Progressing" + + if isRRSSynced(target) { + status = "Synced" + } else if isRRSErrored(target) { + status = "Stalled" + } + + clusterStatuses = append(clusterStatuses, gitopsv1alpha1.ClusterStatus{ + Name: target.Spec.ClusterRef.Name, + PackageStatus: gitopsv1alpha1.PackageStatus{ + PackageID: target.Name, + SyncStatus: target.Status.SyncStatus, + Status: status, + }, + }) + } + + thisWaveInProgress := concurrentUpdates > 0 + + sortClusterStatuses(clusterStatuses) + + return thisWaveInProgress, clusterStatuses, nil +} + +type WaveTarget struct { + Wave *gitopsv1alpha1.Wave + Targets *Targets +} + +type Targets struct { + ToBeCreated []*clusterPackagePair + ToBeUpdated []*gitopsv1alpha1.RemoteRootSync + ToBeDeleted []*gitopsv1alpha1.RemoteRootSync + Unchanged []*gitopsv1alpha1.RemoteRootSync +} + +type clusterPackagePair struct { + cluster *clusterstore.Cluster + packageRef *packagediscovery.DiscoveredPackage +} + +func toRemoteRootSyncNames(rsss []*gitopsv1alpha1.RemoteRootSync) []string { + var names []string + for _, rss := range rsss { + names = append(names, rss.Name) + } + return names +} + +func (r *RolloutReconciler) testClusterClient(ctx context.Context, cl client.Client) error { + logger := log.FromContext(ctx) + podList := &v1.PodList{} + err := cl.List(context.Background(), podList, client.InNamespace("kube-system")) + if err != nil { + return err + } + logger.Info("found podlist", "number of pods", len(podList.Items)) + return nil +} + +func (r *RolloutReconciler) listRemoteRootSyncs(ctx context.Context, rsdName, rsdNamespace string) ([]*gitopsv1alpha1.RemoteRootSync, error) { + var list gitopsv1alpha1.RemoteRootSyncList + if err := r.List(ctx, &list, client.MatchingLabels{rolloutLabel: rsdName}, client.InNamespace(rsdNamespace)); err != nil { + return nil, err + } + var remoterootsyncs []*gitopsv1alpha1.RemoteRootSync + for i := range list.Items { + item := &list.Items[i] + remoterootsyncs = append(remoterootsyncs, item) + } + return remoterootsyncs, nil +} + +func (r *RolloutReconciler) listAllRollouts(ctx context.Context) ([]gitopsv1alpha1.Rollout, error) { + var rolloutsList gitopsv1alpha1.RolloutList + if err := r.List(ctx, &rolloutsList); err != nil { + return nil, err + } + + return rolloutsList.Items, nil +} + +func isRRSSynced(rss *gitopsv1alpha1.RemoteRootSync) bool { + if rss.Generation != rss.Status.ObservedGeneration { + return false + } + + if rss.Status.SyncStatus == "Synced" { + return true + } + return false +} + +func isRRSErrored(rss *gitopsv1alpha1.RemoteRootSync) bool { + if rss.Generation != rss.Status.ObservedGeneration { + return false + } + + if rss.Status.SyncStatus == "Error" { + return true + } + return false +} + +// Given a package identifier and cluster, create a RemoteRootSync object. +func newRemoteRootSync(rollout *gitopsv1alpha1.Rollout, clusterRef gitopsv1alpha1.ClusterRef, rssSpec *gitopsv1alpha1.RootSyncSpec, pkgID string, waveName string) *gitopsv1alpha1.RemoteRootSync { + t := true + clusterName := clusterRef.Name[strings.LastIndex(clusterRef.Name, "/")+1:] + + return &gitopsv1alpha1.RemoteRootSync{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", pkgID, clusterName), + Namespace: rollout.Namespace, + Labels: map[string]string{ + rolloutLabel: rollout.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: rollout.APIVersion, + Kind: rollout.Kind, + Name: rollout.Name, + UID: rollout.UID, + Controller: &t, + }, + }, + }, + Spec: gitopsv1alpha1.RemoteRootSyncSpec{ + ClusterRef: clusterRef, + Template: &gitopsv1alpha1.RootSyncInfo{ + Spec: rssSpec, + }, + }, + } +} + +func toRootSyncSpec(dpkg *packagediscovery.DiscoveredPackage) *gitopsv1alpha1.RootSyncSpec { + return &gitopsv1alpha1.RootSyncSpec{ + SourceFormat: "unstructured", + Git: &gitopsv1alpha1.GitInfo{ + Repo: fmt.Sprintf("https://github.com/%s/%s.git", dpkg.Org, dpkg.Repo), + Revision: dpkg.Revision, + Dir: dpkg.Directory, + Branch: "main", + Auth: "none", + }, + } +} + +func pkgID(dpkg *packagediscovery.DiscoveredPackage) string { + if dpkg.Directory == "" || dpkg.Directory == "." || dpkg.Directory == "/" { + return fmt.Sprintf("%s-%s", dpkg.Org, dpkg.Repo) + } + + return fmt.Sprintf("%s-%s-%s", dpkg.Org, dpkg.Repo, dpkg.Directory) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RolloutReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := gkeclusterapis.AddToScheme(mgr.GetScheme()); err != nil { + return err + } + if err := gitopsv1alpha1.AddToScheme(mgr.GetScheme()); err != nil { + return err + } + r.Client = mgr.GetClient() + + r.packageDiscoveryCache = make(map[types.NamespacedName]*packagediscovery.PackageDiscovery) + + // setup the clusterstore + clusterStore, err := clusterstore.NewClusterStore(r.Client, mgr.GetConfig()) + if err != nil { + return err + } + r.store = clusterStore + + var containerCluster gkeclusterapis.ContainerCluster + return ctrl.NewControllerManagedBy(mgr). + For(&gitopsv1alpha1.Rollout{}). + Owns(&gitopsv1alpha1.RemoteRootSync{}). + Watches( + &source.Kind{Type: &containerCluster}, + handler.EnqueueRequestsFromMapFunc(r.mapClusterUpdateToRequest), + ). + Complete(r) +} + +func (r *RolloutReconciler) mapClusterUpdateToRequest(cluster client.Object) []reconcile.Request { + logger := log.FromContext(context.Background()) + + var requests []reconcile.Request + + allRollouts, err := r.listAllRollouts(context.Background()) + if err != nil { + logger.Error(err, "failed to list rollouts") + return []reconcile.Request{} + } + + for _, rollout := range allRollouts { + selector, err := metav1.LabelSelectorAsSelector(rollout.Spec.Targets.Selector) + if err != nil { + logger.Error(err, "failed to create label selector", "rolloutName", rollout.Name) + continue + } + + rolloutDeploysToCluster := rolloutIncludesCluster(&rollout, cluster.GetName()) + clusterInTargetSet := selector.Matches(labels.Set(cluster.GetLabels())) + + // Rollouts will be reconciled for cluster updates when + // 1) a cluster is added to the rollout target set (clusterInTargetSet will be true) + // 2) a cluster is removed from the rollout target saet (rolloutDeploysToCluster will be true) + // 3) an cluster in the rollout target set is being updated where the package matching logic may produce different results (both variables will be true) + reconcileRollout := rolloutDeploysToCluster || clusterInTargetSet + + if reconcileRollout { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: rollout.Name, Namespace: rollout.Namespace}}) + } + } + + return requests +} + +func sortClusterStatuses(clusterStatuses []gitopsv1alpha1.ClusterStatus) { + sort.Slice(clusterStatuses, func(i, j int) bool { + return strings.Compare(clusterStatuses[i].Name, clusterStatuses[j].Name) == -1 + }) +} + +func getWaveStatus(wave *gitopsv1alpha1.Wave, clusterStatuses []gitopsv1alpha1.ClusterStatus, wavePaused bool) gitopsv1alpha1.WaveStatus { + return gitopsv1alpha1.WaveStatus{ + Name: wave.Name, + Status: getOverallStatus(clusterStatuses), + Paused: wavePaused, + ClusterStatuses: clusterStatuses, + } +} + +func getOverallStatus(clusterStatuses []gitopsv1alpha1.ClusterStatus) string { + overall := "Completed" + + anyProgressing := false + anyStalled := false + anyWaiting := false + + for _, clusterStatus := range clusterStatuses { + switch { + case clusterStatus.PackageStatus.Status == "Progressing": + anyProgressing = true + + case clusterStatus.PackageStatus.Status == "Stalled": + anyStalled = true + + case strings.HasPrefix(clusterStatus.PackageStatus.Status, "Waiting"): + anyWaiting = true + } + } + + switch { + case anyProgressing: + overall = "Progressing" + case anyStalled: + overall = "Stalled" + case anyWaiting: + overall = "Waiting" + } + + return overall +} + +func findWaveNameForCluster(rollout *gitopsv1alpha1.Rollout, clusterName string) (string, bool) { + for _, waveStatus := range rollout.Status.WaveStatuses { + for _, clusterStatus := range waveStatus.ClusterStatuses { + if clusterStatus.Name == clusterName { + return waveStatus.Name, true + } + } + } + + return "", false +} + +func rolloutIncludesCluster(rollout *gitopsv1alpha1.Rollout, clusterName string) bool { + for _, clusterStatus := range rollout.Status.ClusterStatuses { + if clusterStatus.Name == clusterName { + return true + } + } + + return false +} + +func filterClusters(allClusters []clusterstore.Cluster, labelSelector *metav1.LabelSelector) ([]clusterstore.Cluster, error) { + clusters := []clusterstore.Cluster{} + + for _, cluster := range allClusters { + clusterLabelSet := labels.Set(cluster.Labels) + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + if selector.Matches(clusterLabelSet) { + clusters = append(clusters, cluster) + } + } + + return clusters, nil +} diff --git a/rollouts/controllers/status.go b/rollouts/controllers/status.go new file mode 100644 index 0000000000..f08d5d6149 --- /dev/null +++ b/rollouts/controllers/status.go @@ -0,0 +1,149 @@ +// Copyright 2022 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 controllers + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" +) + +// checkSyncStatus fetches the RootSync using the provided client and computes the sync status. The rules +// for computing status here mirrors the one used in the status command in the nomos cli. +func checkSyncStatus(ctx context.Context, client dynamic.Interface, rrsName string) (string, error) { + // TODO: Change this to use the RootSync type instead of Unstructured. + rs, err := client.Resource(rootSyncGVR).Namespace(rootSyncNamespace).Get(ctx, rrsName, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("failed to get RootSync: %w", err) + } + + generation, _, err := unstructured.NestedInt64(rs.Object, "metadata", "generation") + if err != nil { + return "", fmt.Errorf("failed to read generation from RootSync: %w", err) + } + + observedGeneration, _, err := unstructured.NestedInt64(rs.Object, "status", "observedGeneration") + if err != nil { + return "", fmt.Errorf("failed to read observedGeneration from RootSync: %w", err) + } + + if generation != observedGeneration { + return "Pending", nil + } + + conditions, _, err := unstructured.NestedSlice(rs.Object, "status", "conditions") + if err != nil { + return "", fmt.Errorf("failed to extract conditions from RootSync: %w", err) + } + + val, found, err := getConditionStatus(conditions, "Stalled") + if err != nil { + return "", fmt.Errorf("error fetching condition 'Stalled' from conditions slice: %w", err) + } + if found && val == "True" { + return "Stalled", nil + } + + val, found, err = getConditionStatus(conditions, "Reconciling") + if err != nil { + return "", fmt.Errorf("error fetching condition 'Reconciling' from conditions slice: %w", err) + } + if found && val == "True" { + return "Reconciling", nil + } + + cond, found, err := getCondition(conditions, "Syncing") + if err != nil { + return "", fmt.Errorf("error fetching condition 'Syncing' from conditions slice: %w", err) + } + if !found { + return "Reconciling", nil + } + + errCount, err := extractErrorCount(cond) + if err != nil { + return "", fmt.Errorf("error extracting error count from 'Syncing' condition: %w", err) + } + if errCount > 0 { + return "Error", nil + } + + val, err = extractStringField(cond, "status") + if err != nil { + return "", fmt.Errorf("error extracting status of 'Syncing' condition: %w", err) + } + if val == "True" { + return "Pending", nil + } + + return "Synced", nil +} + +func getConditionStatus(conditions []interface{}, condType string) (string, bool, error) { + cond, found, err := getCondition(conditions, condType) + if err != nil { + return "", false, err + } + if !found { + return "", false, nil + } + s, err := extractStringField(cond, "status") + if err != nil { + return "", false, err + } + return s, true, nil +} + +func getCondition(conditions []interface{}, condType string) (map[string]interface{}, bool, error) { + for i := range conditions { + cond, ok := conditions[i].(map[string]interface{}) + if !ok { + return map[string]interface{}{}, false, fmt.Errorf("failed to extract condition %d from slice", i) + } + t, err := extractStringField(cond, "type") + if err != nil { + return map[string]interface{}{}, false, err + } + + if t != condType { + continue + } + return cond, true, nil + } + return map[string]interface{}{}, false, nil +} + +func extractStringField(condition map[string]interface{}, field string) (string, error) { + t, ok := condition[field] + if !ok { + return "", fmt.Errorf("condition does not have a type field") + } + condVal, ok := t.(string) + if !ok { + return "", fmt.Errorf("value of '%s' condition is not of type 'string'", field) + } + return condVal, nil +} + +func extractErrorCount(cond map[string]interface{}) (int64, error) { + count, found, err := unstructured.NestedInt64(cond, "errorSummary", "totalCount") + if err != nil || !found { + return 0, err + } + return count, nil +} diff --git a/rollouts/controllers/suite_test.go b/rollouts/controllers/suite_test.go new file mode 100644 index 0000000000..9ca6384b19 --- /dev/null +++ b/rollouts/controllers/suite_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/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 TestAPIs(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, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = gitopsv1alpha1.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/rollouts/controllers/watcher.go b/rollouts/controllers/watcher.go new file mode 100644 index 0000000000..8c49414cbf --- /dev/null +++ b/rollouts/controllers/watcher.go @@ -0,0 +1,146 @@ +// Copyright 2022 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 controllers + +import ( + "context" + "time" + + "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +const ( + minReconnectDelay = 1 * time.Second + maxReconnectDelay = 30 * time.Second +) + +type watcher struct { + clusterRef v1alpha1.ClusterRef + + ctx context.Context + + cancelFunc context.CancelFunc + + client dynamic.Interface + + channel chan event.GenericEvent + + liens map[types.NamespacedName]struct{} +} + +func (w watcher) watch() { + clusterRefName := w.clusterRef.Name + var events <-chan watch.Event + var watcher watch.Interface + var bookmark string + defer func() { + if watcher != nil { + watcher.Stop() + } + }() + + reconnect := newBackoffTimer(minReconnectDelay, maxReconnectDelay) + defer reconnect.Stop() + +loop: + for { + select { + case <-reconnect.channel(): + var err error + klog.Infof("Starting watch for %s... ", clusterRefName) + watcher, err = w.client.Resource(rootSyncGVR).Watch(w.ctx, v1.ListOptions{}) + if err != nil { + klog.Errorf("Cannot start watch for %s: %v; will retry", clusterRefName, err) + reconnect.backoff() + } else { + klog.Infof("Watch successfully started for %s.", clusterRefName) + events = watcher.ResultChan() + } + case e, ok := <-events: + if !ok { + klog.Errorf("Watch event stream closed for cluster %s. Will restart watch from bookmark %q", clusterRefName, bookmark) + watcher.Stop() + events = nil + watcher = nil + + // Initiate reconnect + reconnect.reset() + } else if obj, ok := e.Object.(*unstructured.Unstructured); ok { + if e.Type == watch.Bookmark { + bookmark = obj.GetResourceVersion() + klog.Infof("Watch bookmark for %s: %q", clusterRefName, bookmark) + } else { + bookmark = obj.GetResourceVersion() + klog.Infof("Got Watch event for %s: rv: %q", clusterRefName, bookmark) + w.channel <- event.GenericEvent{ + Object: obj, + } + } + } else { + klog.V(5).Infof("Received unexpected watch event Object from %s: %T", e.Object, clusterRefName) + } + case <-w.ctx.Done(): + if w.ctx.Err() != nil { + klog.V(2).Infof("exiting watcher for %s, because context is done: %v", clusterRefName, w.ctx.Err()) + } else { + klog.Infof("Watch background routine exiting for %s; context done", clusterRefName) + } + break loop + } + } +} + +// TODO: This comes from Porch. Find a place to put code that can be shared. +type backoffTimer struct { + min, max, curr time.Duration + timer *time.Timer +} + +func newBackoffTimer(min, max time.Duration) *backoffTimer { + return &backoffTimer{ + min: min, + max: max, + timer: time.NewTimer(min), + } +} + +func (t *backoffTimer) Stop() bool { + return t.timer.Stop() +} + +func (t *backoffTimer) channel() <-chan time.Time { + return t.timer.C +} + +func (t *backoffTimer) reset() bool { + t.curr = t.min + return t.timer.Reset(t.curr) +} + +func (t *backoffTimer) backoff() bool { + curr := t.curr * 2 + if curr > t.max { + curr = t.max + } + t.curr = curr + return t.timer.Reset(curr) +} diff --git a/rollouts/go.mod b/rollouts/go.mod new file mode 100644 index 0000000000..3ed65c9b0e --- /dev/null +++ b/rollouts/go.mod @@ -0,0 +1,100 @@ +module github.com/GoogleContainerTools/kpt/rollouts + +go 1.19 + +require ( + cloud.google.com/go/iam v0.7.0 + github.com/GoogleCloudPlatform/k8s-config-connector v1.98.0 + github.com/golang/protobuf v1.5.2 + github.com/google/cel-go v0.13.0 + github.com/google/go-cmp v0.5.9 + github.com/google/go-github/v48 v48.2.0 + github.com/jedib0t/go-pretty/v6 v6.4.4 + github.com/onsi/ginkgo/v2 v2.2.0 + github.com/onsi/gomega v1.20.2 + github.com/spf13/cobra v1.5.0 + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 + google.golang.org/api v0.103.0 + google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c + k8s.io/api v0.25.3 + k8s.io/apimachinery v0.25.4 + k8s.io/client-go v0.25.3 + k8s.io/klog v1.0.0 + k8s.io/klog/v2 v2.80.1 + sigs.k8s.io/cli-utils v0.34.0 + sigs.k8s.io/controller-runtime v0.13.1 +) + +require ( + cloud.google.com/go/compute v1.12.1 // indirect + cloud.google.com/go/compute/metadata v0.2.1 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.27 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/zapr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/swag v0.21.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/gnostic v0.6.9 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // 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/mattn/go-runewidth v0.0.13 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect + golang.org/x/net v0.2.0 // indirect + golang.org/x/sys v0.2.0 // indirect + golang.org/x/term v0.2.0 // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/grpc v1.50.1 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.25.3 // indirect + k8s.io/component-base v0.25.3 // indirect + k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/rollouts/go.sum b/rollouts/go.sum new file mode 100644 index 0000000000..b989fc039c --- /dev/null +++ b/rollouts/go.sum @@ -0,0 +1,736 @@ +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= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= +github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.20 h1:gJ3E98kMpFB1MFqQCvA1yFab8vthOeD4VlFRQULxahg= +github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +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/GoogleCloudPlatform/k8s-config-connector v1.98.0 h1:miM3EQkFWEN19vz15jKaWFDgGo945dfSQd6Zt9ItnYI= +github.com/GoogleCloudPlatform/k8s-config-connector v1.98.0/go.mod h1:eBXtpWdABpt8RDX851XtCjjxpAWPksldqnFcE23P4cA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU= +github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE= +github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +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/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jedib0t/go-pretty/v6 v6.4.4 h1:N+gz6UngBPF4M288kiMURPHELDMIhF/Em35aYuKrsSc= +github.com/jedib0t/go-pretty/v6 v6.4.4/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/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/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY= +github.com/onsi/gomega v1.20.2/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +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/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +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/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 h1:O8uGbHCqlTp2P6QJSLmCojM4mN6UemYv8K+dCnmHmu0= +golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +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.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.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.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +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.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/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-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/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= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +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/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c h1:S34D59DS2GWOEwWNt4fYmTcFrtlOgukG2k9WsomZ7tg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +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.3/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.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.3 h1:Q1v5UFfYe87vi5H7NU0p4RXC26PPMT8KOpr1TLQbCMQ= +k8s.io/api v0.25.3/go.mod h1:o42gKscFrEVjHdQnyRenACrMtbuJsVdP+WVjqejfzmI= +k8s.io/apiextensions-apiserver v0.25.3 h1:bfI4KS31w2f9WM1KLGwnwuVlW3RSRPuIsfNF/3HzR0k= +k8s.io/apiextensions-apiserver v0.25.3/go.mod h1:ZJqwpCkxIx9itilmZek7JgfUAM0dnTsA48I4krPqRmo= +k8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc= +k8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= +k8s.io/cli-runtime v0.25.3 h1:Zs7P7l7db/5J+KDePOVtDlArAa9pZXaDinGWGZl0aM8= +k8s.io/client-go v0.25.3 h1:oB4Dyl8d6UbfDHD8Bv8evKylzs3BXzzufLiO27xuPs0= +k8s.io/client-go v0.25.3/go.mod h1:t39LPczAIMwycjcXkVc+CB+PZV69jQuNx4um5ORDjQA= +k8s.io/component-base v0.25.3 h1:UrsxciGdrCY03ULT1h/S/gXFCOPnLhUVwSyx+hM/zq4= +k8s.io/component-base v0.25.3/go.mod h1:WYoS8L+IlTZgU7rhAl5Ctpw0WdMxDfCC5dkxcEFa/TI= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/kubectl v0.25.3 h1:HnWJziEtmsm4JaJiKT33kG0kadx68MXxUE8UEbXnN4U= +k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 h1:GfD9OzL11kvZN5iArC6oTS7RTj7oJOIfnislxYlqTj8= +k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/cli-utils v0.34.0 h1:zCUitt54f0/MYj/ajVFnG6XSXMhpZ72O/3RewIchW8w= +sigs.k8s.io/cli-utils v0.34.0/go.mod h1:EXyMwPMu9OL+LRnj0JEMsGG/fRvbgFadcVlSnE8RhFs= +sigs.k8s.io/controller-runtime v0.13.1 h1:tUsRCSJVM1QQOOeViGeX3GMT3dQF1eePPw6sEE3xSlg= +sigs.k8s.io/controller-runtime v0.13.1/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= +sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/rollouts/hack/boilerplate.go.txt b/rollouts/hack/boilerplate.go.txt new file mode 100644 index 0000000000..29c55ecda3 --- /dev/null +++ b/rollouts/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/rollouts/main.go b/rollouts/main.go new file mode 100644 index 0000000000..876736c3dc --- /dev/null +++ b/rollouts/main.go @@ -0,0 +1,129 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "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/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" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/controllers" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(gitopsv1alpha1.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, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "23e227a5.kpt.dev", + // 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 = (&controllers.RolloutReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Rollout") + os.Exit(1) + } + if err = (&controllers.RemoteRootSyncReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RemoteRootSync") + os.Exit(1) + } + if err = (&controllers.ProgressiveRolloutStrategyReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ProgressiveRolloutStrategy") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/rollouts/pkg/clusterstore/clusterstore.go b/rollouts/pkg/clusterstore/clusterstore.go new file mode 100644 index 0000000000..b37f88a1bc --- /dev/null +++ b/rollouts/pkg/clusterstore/clusterstore.go @@ -0,0 +1,78 @@ +// Copyright 2023 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 clusterstore + +import ( + "context" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" +) + +type ClusterStore struct { + containerClusterStore *ContainerClusterStore + gcpFleetClusterStore *GCPFleetClusterStore +} +type Cluster struct { + Name string + Labels map[string]string +} + +func NewClusterStore(client client.Client, config *rest.Config) (*ClusterStore, error) { + containerClusterStore := &ContainerClusterStore{ + Config: config, + Client: client, + } + if err := containerClusterStore.Init(); err != nil { + return nil, err + } + + clusterStore := &ClusterStore{ + containerClusterStore: containerClusterStore, + gcpFleetClusterStore: &GCPFleetClusterStore{}, + } + + return clusterStore, nil +} + +func (cs *ClusterStore) ListClusters(ctx context.Context, clusterDiscovery *gitopsv1alpha1.ClusterDiscovery, selector *metav1.LabelSelector) ([]Cluster, error) { + clusterSourceType := clusterDiscovery.SourceType + + switch clusterSourceType { + case gitopsv1alpha1.GCPFleet: + return cs.gcpFleetClusterStore.ListClusters(ctx, clusterDiscovery.GCPFleet, selector) + + case gitopsv1alpha1.KCC: + return cs.containerClusterStore.ListClusters(ctx, selector) + + default: + return nil, fmt.Errorf("%v cluster source not supported", clusterSourceType) + } +} + +func (cs *ClusterStore) GetRESTConfig(ctx context.Context, name string) (*rest.Config, error) { + switch { + case strings.Contains(name, "memberships") || strings.Contains(name, "gkeMemberships"): + return cs.gcpFleetClusterStore.GetRESTConfig(ctx, name) + + default: + return cs.containerClusterStore.GetRESTConfig(ctx, name) + } +} diff --git a/rollouts/pkg/clusterstore/containerclusterstore.go b/rollouts/pkg/clusterstore/containerclusterstore.go new file mode 100644 index 0000000000..3297ebd8cf --- /dev/null +++ b/rollouts/pkg/clusterstore/containerclusterstore.go @@ -0,0 +1,185 @@ +// Copyright 2022 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 clusterstore + +import ( + "context" + "encoding/base64" + "fmt" + "sort" + "strings" + + "golang.org/x/oauth2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + gkeclusterapis "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/container/v1beta1" + 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" + "k8s.io/client-go/rest" +) + +// ContainerClusterStore represents a store of kubernetes cluster. +type ContainerClusterStore struct { + // Config/Client points to the config + // pointing to where the rollout controller is running + Config *rest.Config + client.Client + WorkloadIdentityHelper +} + +func (cs *ContainerClusterStore) Init() error { + if err := cs.WorkloadIdentityHelper.Init(cs.Config); err != nil { + return err + } + return nil +} + +func (cs *ContainerClusterStore) ListClusters(ctx context.Context, selector *metav1.LabelSelector) ([]Cluster, error) { + gkeClusters, err := cs.listClusters(ctx, selector) + if err != nil { + return nil, err + } + + sort.Slice(gkeClusters.Items, func(i, j int) bool { + return strings.Compare(gkeClusters.Items[i].Name, gkeClusters.Items[j].Name) == -1 + }) + + clusters := []Cluster{} + + for _, containerCluster := range gkeClusters.Items { + cluster := cs.toCluster(&containerCluster) + clusters = append(clusters, cluster) + } + + return clusters, nil +} + +func (cs *ContainerClusterStore) getContainerCluster(ctx context.Context, name string) (*gkeclusterapis.ContainerCluster, error) { + gkeCluster := gkeclusterapis.ContainerCluster{} + clusterKey := client.ObjectKey{ + Namespace: "config-control", + Name: name, + } + if err := cs.Get(ctx, clusterKey, &gkeCluster); err != nil { + return nil, err + } + + return &gkeCluster, nil +} + +func (cs *ContainerClusterStore) GetRESTConfig(ctx context.Context, name string) (*rest.Config, error) { + cluster, err := cs.getContainerCluster(ctx, name) + if err != nil { + return nil, err + } + restConfig, err := cs.getRESTConfig(ctx, cluster) + if err != nil { + return nil, err + } + return restConfig, err +} + +func (cs *ContainerClusterStore) listClusters(ctx context.Context, selector *metav1.LabelSelector) (*gkeclusterapis.ContainerClusterList, error) { + gkeClusters := &gkeclusterapis.ContainerClusterList{} + + var opts []client.ListOption + + if selector != nil { + selector, err := metav1.LabelSelectorAsSelector(selector) + if err != nil { + return nil, err + } + opts = append(opts, client.MatchingLabelsSelector{Selector: selector}) + } + + // TODO: make it configurable ? + opts = append(opts, client.InNamespace("config-control")) + if err := cs.List(ctx, gkeClusters, opts...); err != nil { + return nil, err + } + + return gkeClusters, nil +} + +func (cs *ContainerClusterStore) getRESTConfig(ctx context.Context, cluster *gkeclusterapis.ContainerCluster) (*rest.Config, error) { + logger := log.FromContext(ctx) + restConfig := &rest.Config{} + clusterCaCertificate := cluster.Spec.MasterAuth.ClusterCaCertificate + if clusterCaCertificate == nil || *clusterCaCertificate == "" { + return nil, fmt.Errorf("cluster CA certificate data is missing") + } + caData, err := base64.StdEncoding.DecodeString(*clusterCaCertificate) + if err != nil { + return nil, fmt.Errorf("error decoding ca certificate: %w", err) + } + restConfig.CAData = caData + if cluster.Status.Endpoint == "" { + return nil, fmt.Errorf("cluster master endpoint field is empty") + } + restConfig.Host = "https://" + cluster.Status.Endpoint + logger.Info("Host endpoint is", "endpoint", restConfig.Host) + tokenSource, err := cs.getConfigConnectorContextTokenSource(ctx, cluster.GetNamespace()) + if err != nil { + return nil, err + } + token, err := tokenSource.Token() + if err != nil { + return nil, fmt.Errorf("error getting token: %w", err) + } + restConfig.BearerToken = token.AccessToken + return restConfig, nil +} + +// getConfigConnectorContextTokenSource gets and returns the ConfigConnectorContext for the given namespace. +func (cs *ContainerClusterStore) getConfigConnectorContextTokenSource(ctx context.Context, ns string) (oauth2.TokenSource, error) { + // TODO: migrate to it's own Go type and use client.Client instance for it + gvr := schema.GroupVersionResource{ + Group: "core.cnrm.cloud.google.com", + Version: "v1beta1", + Resource: "configconnectorcontexts", + } + + cr, err := cs.dynamicClient.Resource(gvr).Namespace(ns).Get(ctx, "configconnectorcontext.core.cnrm.cloud.google.com", metav1.GetOptions{}) + if err != nil { + return nil, err + } + + googleServiceAccount, _, err := unstructured.NestedString(cr.Object, "spec", "googleServiceAccount") + if err != nil { + return nil, fmt.Errorf("error reading spec.googleServiceAccount from ConfigConnectorContext in %q: %w", ns, err) + } + + if googleServiceAccount == "" { + return nil, fmt.Errorf("could not find spec.googleServiceAccount from ConfigConnectorContext in %q: %w", ns, err) + } + + kubeServiceAccount := types.NamespacedName{ + Namespace: "cnrm-system", + Name: "cnrm-controller-manager-" + ns, + } + return cs.WorkloadIdentityHelper.GetGcloudAccessTokenSource(ctx, kubeServiceAccount, googleServiceAccount) +} + +func (cs *ContainerClusterStore) toCluster(containerCluster *gkeclusterapis.ContainerCluster) Cluster { + cluster := Cluster{ + Name: containerCluster.Name, + Labels: containerCluster.Labels, + } + + return cluster +} diff --git a/rollouts/pkg/clusterstore/gcpfleetclusterstore.go b/rollouts/pkg/clusterstore/gcpfleetclusterstore.go new file mode 100644 index 0000000000..a7dfd1f3f4 --- /dev/null +++ b/rollouts/pkg/clusterstore/gcpfleetclusterstore.go @@ -0,0 +1,206 @@ +// Copyright 2023 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 clusterstore + +import ( + "context" + "fmt" + "strings" + "sync" + + "golang.org/x/oauth2/google" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/rest" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + cloudresourcemanagerv1 "google.golang.org/api/cloudresourcemanager/v1" + gkehubv1 "google.golang.org/api/gkehub/v1" +) + +type GCPFleetClusterStore struct { + projectId string + membershipNameToConfigHostCache sync.Map + projectIdToNumberCache sync.Map +} + +func (cs *GCPFleetClusterStore) ListClusters(ctx context.Context, configuration *gitopsv1alpha1.ClusterSourceGCPFleet, labelSelector *metav1.LabelSelector) ([]Cluster, error) { + err := cs.validateGCPFleetConfiguration(configuration) + if err != nil { + return nil, fmt.Errorf("gcp fleet configuration error: %w", err) + } + + clusters := []Cluster{} + projectId := configuration.ProjectIds[0] + + // TODO: add support for listing meemberships for mulitple projects + memberships, err := cs.listMemberships(ctx, projectId) + if err != nil { + return nil, fmt.Errorf("memberships failed: %w", err) + } + + for _, membership := range memberships.Resources { + cluster := cs.toCluster(membership) + + clusterLabelSet := labels.Set(cluster.Labels) + shouldAdd := true + + if labelSelector != nil { + selector, _ := metav1.LabelSelectorAsSelector(labelSelector) + + if !selector.Matches(clusterLabelSet) { + shouldAdd = false + continue + } + } + + if shouldAdd { + clusters = append(clusters, cluster) + } + } + + return clusters, nil +} + +func (cs *GCPFleetClusterStore) GetRESTConfig(ctx context.Context, name string) (*rest.Config, error) { + accessToken, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + return nil, fmt.Errorf("unable to get access token: %w", err) + } + + token, err := accessToken.Token() + + host, err := cs.getRESTConfigHost(ctx, name) + if err != nil { + return nil, fmt.Errorf("building rest config host failed: %w", err) + } + + restConfig := &rest.Config{} + restConfig.Host = host + restConfig.BearerToken = token.AccessToken + + return restConfig, err +} + +func (cs *GCPFleetClusterStore) getRESTConfigHost(ctx context.Context, name string) (string, error) { + restConfigHost, found := cs.membershipNameToConfigHostCache.Load(name) + + if found { + restConfigHost := restConfigHost.(string) + return restConfigHost, nil + } + + membership, err := cs.getMembership(ctx, name) + if err != nil { + return "", fmt.Errorf("unable to get membership: %w", err) + } + + // name format: projects/:projectId/locations/global/memberships/:membershipName + membershipName := strings.Split(membership.Name, "/")[5] + projectId := strings.Split(membership.Name, "/")[1] + + isGKE := membership.Endpoint.GkeCluster != nil + + projectNumber, err := cs.getProjectNumber(ctx, projectId) + if err != nil { + return "", fmt.Errorf("unable to get project number: %w", err) + } + + membershipUrl := "memberships" + + if isGKE { + membershipUrl = "gkeMemberships" + } + + host := fmt.Sprintf("https://connectgateway.googleapis.com/v1/projects/%d/locations/global/%s/%s", projectNumber, membershipUrl, membershipName) + + cs.membershipNameToConfigHostCache.Store(name, host) + + return host, nil +} + +func (cs *GCPFleetClusterStore) getProjectNumber(ctx context.Context, projectId string) (int64, error) { + projectNumberCache, found := cs.projectIdToNumberCache.Load(projectId) + + if found { + projectNumber := projectNumberCache.(int64) + return projectNumber, nil + } + + crmClient, err := cloudresourcemanagerv1.NewService(ctx) + if err != nil { + return -1, fmt.Errorf("failed to create new cloudresourcemanager client: %w", err) + } + + project, err := crmClient.Projects.Get(projectId).Context(ctx).Do() + if err != nil { + return -1, fmt.Errorf("error querying project %q: %w", projectId, err) + } + + projectNumber := project.ProjectNumber + + cs.projectIdToNumberCache.Store(projectId, projectNumber) + + return projectNumber, nil +} + +func (cs *GCPFleetClusterStore) validateGCPFleetConfiguration(configuration *gitopsv1alpha1.ClusterSourceGCPFleet) error { + if configuration == nil { + return fmt.Errorf("configuration is missing") + } + if len(configuration.ProjectIds) == 0 { + return fmt.Errorf("at least one project id must be listed") + } + + return nil +} + +func (cs *GCPFleetClusterStore) getMembership(ctx context.Context, name string) (*gkehubv1.Membership, error) { + hubClient, err := gkehubv1.NewService(ctx) + if err != nil { + return nil, err + } + + resp, err := hubClient.Projects.Locations.Memberships.Get(name).Do() + if err != nil { + return nil, err + } + + return resp, nil +} + +func (cs *GCPFleetClusterStore) listMemberships(ctx context.Context, projectId string) (*gkehubv1.ListMembershipsResponse, error) { + hubClient, err := gkehubv1.NewService(ctx) + if err != nil { + return nil, err + } + + parent := fmt.Sprintf("projects/%s/locations/global", projectId) + resp, err := hubClient.Projects.Locations.Memberships.List(parent).Do() + if err != nil { + return nil, err + } + + return resp, nil +} + +func (cs *GCPFleetClusterStore) toCluster(membership *gkehubv1.Membership) Cluster { + cluster := Cluster{ + Name: membership.Name, + Labels: membership.Labels, + } + + return cluster +} diff --git a/rollouts/pkg/clusterstore/tokens.go b/rollouts/pkg/clusterstore/tokens.go new file mode 100644 index 0000000000..9abf4cd4fa --- /dev/null +++ b/rollouts/pkg/clusterstore/tokens.go @@ -0,0 +1,203 @@ +// Copyright 2022 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 clusterstore + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "strings" + "sync" + + "github.com/GoogleContainerTools/kpt/rollouts/pkg/tokenexchange/gcptokensource" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/tokenexchange/ksaimpersonationtokensource" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/tokenexchange/ksatokensource" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/tokenexchange/membership" + "golang.org/x/oauth2" + "google.golang.org/api/option" + "google.golang.org/api/sts/v1" + stsv1 "google.golang.org/api/sts/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" +) + +// WorkloadIdentityHelper is a helper class that does the exchanges needed for workload identity. +type WorkloadIdentityHelper struct { + // stsClient holds a client for querying STS + stsClient *stsv1.Service + + // corev1Client is used for kubernetes impersonation + corev1Client corev1client.CoreV1Interface + + // dynamicClient is used to check for the membership resource + dynamicClient dynamic.Interface + + restConfig *rest.Config + + mutex sync.Mutex + tokenCache map[tokenCacheKey]oauth2.TokenSource +} + +type tokenCacheKey struct { + kubeServiceAccount types.NamespacedName + gcpServiceAccount string +} + +// Init should be called before using a WorkloadIdentityHelper +func (r *WorkloadIdentityHelper) Init(restConfig *rest.Config) error { + r.restConfig = restConfig + + // If we want to debug RBAC/Token Exchange locally... + // restConfigImpersonate := *restConfig + // restConfigImpersonate.Impersonate.UserName = "system:serviceaccount:configcontroller-system:rootsyncset-impersonate" + // restConfig = &restConfigImpersonate + + corev1Client, err := corev1client.NewForConfig(restConfig) + if err != nil { + return fmt.Errorf("error building corev1 client: %w", err) + } + r.corev1Client = corev1Client + + dynamicClient, err := dynamic.NewForConfig(restConfig) + if err != nil { + return fmt.Errorf("error building dynamic client: %w", err) + } + r.dynamicClient = dynamicClient + + // option.WithoutAuthentication because we don't want to use our credentials for the exchange + // STS actually gives an error: googleapi: "Error 400: Request contains an invalid argument., badRequest" + stsClient, err := sts.NewService(context.Background(), option.WithoutAuthentication()) + if err != nil { + return fmt.Errorf("error building sts client: %w", err) + } + r.stsClient = stsClient + + r.tokenCache = make(map[tokenCacheKey]oauth2.TokenSource) + + return nil +} + +// GetGcloudAccessTokenSource does the exchange to get a token for the specified GCP ServiceAccount. +func (r *WorkloadIdentityHelper) GetGcloudAccessTokenSource(ctx context.Context, kubeServiceAccount types.NamespacedName, gcpServiceAccount string) (oauth2.TokenSource, error) { + key := tokenCacheKey{ + kubeServiceAccount: kubeServiceAccount, + gcpServiceAccount: gcpServiceAccount, + } + + r.mutex.Lock() + cachedTokenSource := r.tokenCache[key] + r.mutex.Unlock() + + if cachedTokenSource != nil { + return cachedTokenSource, nil + } + + var workloadIdentityPool string + var identityProvider string + + membershipConfig, err := membership.Get(ctx, r.dynamicClient) + if err != nil { + if apierrors.IsNotFound(err) { + // TODO: Cache this? + workloadIdentityPool, identityProvider, err = r.findWorkloadIdentityPool(ctx, kubeServiceAccount) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("error fetching membership: %w", err) + } + } else { + workloadIdentityPool = membershipConfig.Spec.WorkloadIdentityPool + identityProvider = membershipConfig.Spec.IdentityProvider + } + + impersonated := ksaimpersonationtokensource.New(r.corev1Client, kubeServiceAccount, []string{workloadIdentityPool}) + + ksaToken := ksatokensource.New(r.stsClient, impersonated, workloadIdentityPool, identityProvider) + + var scopes []string + gcpTokenSource := gcptokensource.New(gcpServiceAccount, scopes, ksaToken) + + tokenSource := oauth2.ReuseTokenSource(nil, gcpTokenSource) + + r.mutex.Lock() + r.tokenCache[key] = tokenSource + r.mutex.Unlock() + + return tokenSource, nil +} + +func (r *WorkloadIdentityHelper) findWorkloadIdentityPool(ctx context.Context, kubeServiceAccount types.NamespacedName) (string, string, error) { + accessToken := "" + + // First, see if we have a valid token mounted locally in our pod + { + const tokenFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + + tokenBytes, err := ioutil.ReadFile(tokenFilePath) + if err != nil { + if os.IsNotExist(err) { + klog.V(2).Infof("token file not found at %q", tokenFilePath) + } else { + klog.Warningf("error reading token file from %q: %v", tokenFilePath, err) + } + } else { + klog.Infof("found token at %q", tokenFilePath) + accessToken = string(tokenBytes) + } + } + + // We could also query the kube apiserver at /.well-known/openid-configuration + // kubectl get --raw /.well-known/openid-configuration + // {"issuer":"https://container.googleapis.com/v1/projects/example-project-id/locations/us-central1/clusters/krmapihost-control","jwks_uri":"https://172.16.0.130:443/openid/v1/jwks","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"]} + + if accessToken == "" { + // We get a token for our own service account, so we can extract the issuer + klog.Infof("token not found at well-known path, requesting token from apiserver") + impersonated := ksaimpersonationtokensource.New(r.corev1Client, kubeServiceAccount, nil /* unspecified/default audience */) + + token, err := impersonated.Token() + if err != nil { + return "", "", fmt.Errorf("failed to get kube token for %s: %w", kubeServiceAccount, err) + } else { + accessToken = token.AccessToken + } + } + + issuer, err := ksatokensource.ExtractIssuer(accessToken) + if err != nil { + return "", "", err + } + + if strings.HasPrefix(issuer, "https://container.googleapis.com/") { + path := strings.TrimPrefix(issuer, "https://container.googleapis.com/") + tokens := strings.Split(path, "/") + for i := 0; i+1 < len(tokens); i++ { + if tokens[i] == "projects" { + workloadIdentityPool := tokens[i+1] + ".svc.id.goog" + klog.Infof("inferred workloadIdentityPool as %q", workloadIdentityPool) + return workloadIdentityPool, issuer, nil + } + } + return "", "", fmt.Errorf("could not extract project from issue %q", issuer) + } else { + return "", "", fmt.Errorf("unknown issuer %q", issuer) + } +} diff --git a/rollouts/pkg/packageclustermatcher/packageclustermatcher.go b/rollouts/pkg/packageclustermatcher/packageclustermatcher.go new file mode 100644 index 0000000000..71d7302861 --- /dev/null +++ b/rollouts/pkg/packageclustermatcher/packageclustermatcher.go @@ -0,0 +1,129 @@ +// Copyright 2022 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 packageclustermatcher + +import ( + "fmt" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/clusterstore" + "github.com/GoogleContainerTools/kpt/rollouts/pkg/packagediscovery" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" +) + +type PackageClusterMatcher struct { + clusters []clusterstore.Cluster + packages []packagediscovery.DiscoveredPackage +} + +type ClusterPackages struct { + Cluster clusterstore.Cluster + Packages []packagediscovery.DiscoveredPackage +} + +func NewPackageClusterMatcher(clusters []clusterstore.Cluster, packages []packagediscovery.DiscoveredPackage) *PackageClusterMatcher { + return &PackageClusterMatcher{ + clusters: clusters, + packages: packages, + } +} + +func (m *PackageClusterMatcher) GetClusterPackages(matcher gitopsv1alpha1.PackageToClusterMatcher) ([]ClusterPackages, error) { + clusters := m.clusters + packages := m.packages + + allClusterPackages := []ClusterPackages{} + + for _, cluster := range clusters { + matchedPackages := []packagediscovery.DiscoveredPackage{} + + switch matcherType := matcher.Type; matcherType { + case gitopsv1alpha1.MatchAllClusters: + matchedPackages = packages + case gitopsv1alpha1.CustomMatcher: + celCluster := map[string]interface{}{ + "name": cluster.Name, + "labels": cluster.Labels, + } + + for _, discoveredPackage := range packages { + celPackage := map[string]interface{}{ + "org": discoveredPackage.Org, + "repo": discoveredPackage.Repo, + "directory": discoveredPackage.Directory, + } + + isMatch, err := isPackageClusterMatch(matcher.MatchExpression, celCluster, celPackage) + if err != nil { + return nil, fmt.Errorf("unable to execute package cluster matcher expression: %w", err) + } + + if isMatch { + matchedPackages = append(matchedPackages, discoveredPackage) + } + } + default: + return nil, fmt.Errorf("%v matcher is not supported", matcherType) + } + + if len(matchedPackages) > 1 { + return nil, fmt.Errorf("more than one package rollout is not supported yet. Found %d packages for cluster %s", len(matchedPackages), cluster.Name) + } + + clusterPackages := ClusterPackages{ + Cluster: cluster, + Packages: matchedPackages, + } + + allClusterPackages = append(allClusterPackages, clusterPackages) + } + + return allClusterPackages, nil +} + +func isPackageClusterMatch(expr string, cluster, rolloutPackage map[string]interface{}) (bool, error) { + env, err := cel.NewEnv( + cel.Declarations( + decls.NewVar("cluster", decls.Dyn), + decls.NewVar("rolloutPackage", decls.Dyn), + )) + if err != nil { + return false, err + } + + p, issue := env.Parse(expr) + if issue != nil && issue.Err() != nil { + return false, issue.Err() + } + + c, issue := env.Check(p) + if issue != nil && issue.Err() != nil { + return false, issue.Err() + } + + prg, err := env.Program(c) + if err != nil { + return false, err + } + + out, _, err := prg.Eval(map[string]interface{}{ + "cluster": cluster, + "rolloutPackage": rolloutPackage, + }) + + return out.Value().(bool), err +} diff --git a/rollouts/pkg/packagediscovery/packagediscovery.go b/rollouts/pkg/packagediscovery/packagediscovery.go new file mode 100644 index 0000000000..114b555271 --- /dev/null +++ b/rollouts/pkg/packagediscovery/packagediscovery.go @@ -0,0 +1,227 @@ +// Copyright 2022 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 packagediscovery + +import ( + "context" + "fmt" + "net/http" + "regexp" + "strings" + "sync" + "time" + + gitopsv1alpha1 "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v48/github" + "golang.org/x/oauth2" + coreapi "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type PackageDiscovery struct { + client client.Client + namespace string + mutex sync.Mutex + cache *Cache +} + +type DiscoveredPackage struct { + Org string + Repo string + Directory string + Revision string +} + +type Cache struct { + config gitopsv1alpha1.PackagesConfig + packages []DiscoveredPackage + expiration time.Time +} + +func NewPackageDiscovery(client client.Client, namespace string) *PackageDiscovery { + return &PackageDiscovery{ + client: client, + namespace: namespace, + } +} + +func (d *PackageDiscovery) GetPackages(ctx context.Context, config gitopsv1alpha1.PackagesConfig) ([]DiscoveredPackage, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + if d.useCache(config) { + return d.cache.packages, nil + } + + if config.SourceType != gitopsv1alpha1.GitHub { + return nil, fmt.Errorf("%v source type not supported yet", config.SourceType) + } + + gitHubSelector := config.GitHub.Selector + + gitHubClient, err := d.getGitHubClient(ctx, gitHubSelector) + if err != nil { + return nil, fmt.Errorf("unable to create github client: %w", err) + } + + discoveredPackages := []DiscoveredPackage{} + + repositoryNames, err := d.getRepositoryNames(gitHubClient, gitHubSelector, ctx) + if err != nil { + return nil, fmt.Errorf("unable to get repositories: %w", err) + } + + for _, repositoryName := range repositoryNames { + repoPackages, err := d.getPackagesForRepository(gitHubClient, ctx, gitHubSelector, repositoryName) + if err != nil { + return nil, fmt.Errorf("unable to get packages: %w", err) + } + + discoveredPackages = append(discoveredPackages, repoPackages...) + } + + d.cache = &Cache{ + packages: discoveredPackages, + config: config, + expiration: time.Now().Add(1 * time.Minute), + } + + return discoveredPackages, nil +} + +func (d *PackageDiscovery) getRepositoryNames(gitHubClient *github.Client, selector gitopsv1alpha1.GitHubSelector, ctx context.Context) ([]string, error) { + repositoryNames := []string{} + + if isSelectorField(selector.Repo) { + // TOOD: add pagination + listOptions := github.RepositoryListOptions{} + listOptions.PerPage = 150 + + repositories, _, err := gitHubClient.Repositories.List(ctx, selector.Org, &listOptions) + if err != nil { + return nil, err + } + + allRepositoryNames := []string{} + for _, repository := range repositories { + allRepositoryNames = append(allRepositoryNames, *repository.Name) + } + + matchRepositoryNames := filterByPattern(selector.Repo, allRepositoryNames) + + repositoryNames = append(repositoryNames, matchRepositoryNames...) + } else { + repositoryNames = append(repositoryNames, selector.Repo) + } + + return repositoryNames, nil +} + +func (d *PackageDiscovery) getPackagesForRepository(gitHubClient *github.Client, ctx context.Context, selector gitopsv1alpha1.GitHubSelector, repoName string) ([]DiscoveredPackage, error) { + discoveredPackages := []DiscoveredPackage{} + + if isSelectorField(selector.Directory) { + tree, _, err := gitHubClient.Git.GetTree(ctx, selector.Org, repoName, selector.Revision, true) + if err != nil { + return nil, err + } + + allDirectories := []string{} + for _, entry := range tree.Entries { + if *entry.Type == "tree" { + allDirectories = append(allDirectories, *entry.Path) + } + } + + directories := filterByPattern(selector.Directory, allDirectories) + + for _, directory := range directories { + thisDiscoveredPackage := DiscoveredPackage{Org: selector.Org, Repo: repoName, Revision: selector.Revision, Directory: directory} + discoveredPackages = append(discoveredPackages, thisDiscoveredPackage) + } + } else { + thisDiscoveredPackage := DiscoveredPackage{Org: selector.Org, Repo: repoName, Revision: selector.Revision, Directory: selector.Directory} + discoveredPackages = append(discoveredPackages, thisDiscoveredPackage) + } + + return discoveredPackages, nil +} + +func (d *PackageDiscovery) getGitHubClient(ctx context.Context, selector gitopsv1alpha1.GitHubSelector) (*github.Client, error) { + httpClient := &http.Client{} + + if secretName := selector.SecretRef.Name; secretName != "" { + var repositorySecret coreapi.Secret + key := client.ObjectKey{Namespace: d.namespace, Name: secretName} + if err := d.client.Get(ctx, key, &repositorySecret); err != nil { + return nil, fmt.Errorf("cannot retrieve github credentials %s: %v", key, err) + } + + accessToken := string(repositorySecret.Data["password"]) + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: accessToken}, + ) + + httpClient = oauth2.NewClient(ctx, ts) + } + + gitHubClient := github.NewClient(httpClient) + + return gitHubClient, nil +} + +func (d *PackageDiscovery) useCache(config gitopsv1alpha1.PackagesConfig) bool { + return d.cache != nil && cmp.Equal(config, d.cache.config) && time.Now().Before(d.cache.expiration) +} + +func filterByPattern(pattern string, list []string) []string { + matches := []string{} + + regexPattern := getRegexPattern(pattern) + + for _, value := range list { + if isMatch := match(regexPattern, value); isMatch { + matches = append(matches, value) + } + } + + return matches +} + +func getRegexPattern(pattern string) string { + var result strings.Builder + + result.WriteString("^") + for i, literal := range strings.Split(pattern, "*") { + if i > 0 { + result.WriteString("[^/]+") + } + result.WriteString(regexp.QuoteMeta(literal)) + } + result.WriteString("$") + + return result.String() +} + +func match(pattern string, value string) bool { + result, _ := regexp.MatchString(pattern, value) + return result +} + +func isSelectorField(value string) bool { + return strings.Contains(value, "*") +} diff --git a/rollouts/pkg/packagediscovery/packagediscovery_test.go b/rollouts/pkg/packagediscovery/packagediscovery_test.go new file mode 100644 index 0000000000..0d6f49f84b --- /dev/null +++ b/rollouts/pkg/packagediscovery/packagediscovery_test.go @@ -0,0 +1,64 @@ +// Copyright 2022 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 packagediscovery + +import ( + "reflect" + "testing" +) + +var FilterByPattern = filterByPattern + +func TestFilterByPatternForRepositories(t *testing.T) { + tests := []struct { + repositories []string + pattern string + want []string + }{ + {repositories: []string{"apps", "policies", "namespaces"}, pattern: "*", want: []string{"apps", "policies", "namespaces"}}, + {repositories: []string{"cluster1-apps", "cluster1-policies", "cluster2-apps", "cluster2-policies"}, pattern: "*-apps", want: []string{"cluster1-apps", "cluster2-apps"}}, + {repositories: []string{"cluster1-apps", "cluster1-policies", "cluster2-apps", "cluster2-policies"}, pattern: "cluster1-*", want: []string{"cluster1-apps", "cluster1-policies"}}, + {repositories: []string{"cluster1-apps", "cluster1-policies", "cluster2-apps", "cluster2-policies"}, pattern: "cluster1-apps", want: []string{"cluster1-apps"}}, + } + + for _, tc := range tests { + got := FilterByPattern(tc.pattern, tc.repositories) + if !reflect.DeepEqual(tc.want, got) { + t.Fatalf("expected: %v, got: %v", tc.want, got) + } + } +} + +func TestFilterByPatternForDirectories(t *testing.T) { + tests := []struct { + directories []string + pattern string + want []string + }{ + {directories: []string{"dev", "prod", "dev/package-a", "dev/package-b", "prod/package-c"}, pattern: "dev/*", want: []string{"dev/package-a", "dev/package-b"}}, + {directories: []string{"package-a", "package-b", "package-a/dev", "package-a/prod", "package-b/dev"}, pattern: "*/dev", want: []string{"package-a/dev", "package-b/dev"}}, + {directories: []string{"package-a", "package-b", "package-a/dev", "package-a/prod", "package-b/dev"}, pattern: "package-*/prod", want: []string{"package-a/prod"}}, + {directories: []string{"package-a", "package-b", "package-a/dev", "package-a/prod", "package-b/dev"}, pattern: "package-a/dev", want: []string{"package-a/dev"}}, + {directories: []string{"parent", "parent/package-a", "parent/package-b", "parent/package-a/dev", "parent/package-a/prod", "parent/package-b/dev"}, pattern: "parent/*-a/dev", want: []string{"parent/package-a/dev"}}, + {directories: []string{"package-a", "package-b", "package-a/crds", "package-a/crs", "package-b/crds"}, pattern: "package-*", want: []string{"package-a", "package-b"}}, + } + + for _, tc := range tests { + got := FilterByPattern(tc.pattern, tc.directories) + if !reflect.DeepEqual(tc.want, got) { + t.Fatalf("expected: %v, got: %v", tc.want, got) + } + } +} diff --git a/rollouts/pkg/tokenexchange/gcptokensource/gcptokensource.go b/rollouts/pkg/tokenexchange/gcptokensource/gcptokensource.go new file mode 100644 index 0000000000..80de9b8cbe --- /dev/null +++ b/rollouts/pkg/tokenexchange/gcptokensource/gcptokensource.go @@ -0,0 +1,81 @@ +// Copyright 2022 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 gcptokensource + +import ( + "context" + "fmt" + "time" + + iamv1 "cloud.google.com/go/iam/credentials/apiv1" + "github.com/golang/protobuf/ptypes" + "golang.org/x/oauth2" + "google.golang.org/api/option" + iampb "google.golang.org/genproto/googleapis/iam/credentials/v1" + "k8s.io/klog/v2" +) + +// New returns an oauth2.TokenSource that exchanges tokens from ts for tokens +// that authenticate as GCP Service Accounts. +func New(gcpServiceAccount string, scopes []string, tokenSource oauth2.TokenSource) oauth2.TokenSource { + // The cloud-platform scope is always required for the token exchange. + scopes = append(scopes, "https://www.googleapis.com/auth/cloud-platform") + return &gcpTokenSource{ + gcpServiceAccount: gcpServiceAccount, + scopes: scopes, + tokenSource: tokenSource, + } +} + +// gcpTokenSource produces tokens that authenticate as GCP ServiceAccounts. +type gcpTokenSource struct { + gcpServiceAccount string + scopes []string + tokenSource oauth2.TokenSource +} + +// ensure gcpTokenSource implements oauth2.TokenSource +var _ oauth2.TokenSource = &gcpTokenSource{} + +// Token exchanges the input token for a GCP SA token. +func (ts *gcpTokenSource) Token() (*oauth2.Token, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // use the provided token source to make the request + c, err := iamv1.NewIamCredentialsClient(ctx, option.WithTokenSource(ts.tokenSource)) + if err != nil { + return nil, fmt.Errorf("failed to construct IAM client: %w", err) + } + resp, err := c.GenerateAccessToken(ctx, + &iampb.GenerateAccessTokenRequest{ + Name: "projects/-/serviceAccounts/" + ts.gcpServiceAccount, + Scope: ts.scopes, + }) + if err != nil { + return nil, fmt.Errorf("token exchange for GCP serviceaccount %q failed: %w", ts.gcpServiceAccount, err) + } + + klog.Infof("got GCP token for %v", ts.gcpServiceAccount) + + expiry, err := ptypes.Timestamp(resp.ExpireTime) + if err != nil { + return nil, fmt.Errorf("failed to parse expire time on returned token: %w", err) + } + return &oauth2.Token{ + AccessToken: resp.AccessToken, + Expiry: expiry, + }, nil +} diff --git a/rollouts/pkg/tokenexchange/ksaimpersonationtokensource/ksaimpersonation.go b/rollouts/pkg/tokenexchange/ksaimpersonationtokensource/ksaimpersonation.go new file mode 100644 index 0000000000..a012d5d099 --- /dev/null +++ b/rollouts/pkg/tokenexchange/ksaimpersonationtokensource/ksaimpersonation.go @@ -0,0 +1,87 @@ +// Copyright 2022 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 ksaimpersonationtokensource + +import ( + "context" + "fmt" + "time" + + "golang.org/x/oauth2" + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/klog/v2" +) + +// New returns an oauth2.TokenSource that exchanges the KSA token at ksaTokenPath +// for a GCP access token. +func New(corev1Client corev1client.CoreV1Interface, serviceAccount types.NamespacedName, audiences []string) oauth2.TokenSource { + return &ksaImpersonationTokenSource{ + corev1Client: corev1Client, + serviceAccount: serviceAccount, + audiences: audiences, + } +} + +// ksaImpersonationTokenSource implements oauth2.TokenSource for exchanging KSA tokens for +// GCP tokens. It can be wrapped in a ReuseTokenSource to cache tokens until +// expiry. +type ksaImpersonationTokenSource struct { + corev1Client corev1client.CoreV1Interface + + // serviceAccount is the name of the serviceAccount to impersonate + serviceAccount types.NamespacedName + + // audiences is the set of audiences to request + audiences []string +} + +// ksaTokenSource implements oauth2.TokenSource +var _ oauth2.TokenSource = &ksaImpersonationTokenSource{} + +// Token exchanges a KSA token for a GCP access token, returning the GCP token. +func (ts *ksaImpersonationTokenSource) Token() (*oauth2.Token, error) { + tokenRequest := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: ts.audiences, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + klog.V(2).Infof("getting token for kubernetes serviceaccount %v", ts.serviceAccount) + response, err := ts.corev1Client.ServiceAccounts(ts.serviceAccount.Namespace).CreateToken(ctx, ts.serviceAccount.Name, tokenRequest, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to CreateToken for %s: %w", ts.serviceAccount, err) + } + + exchangeTime := time.Now() + + serviceAccountToken := &oauth2.Token{ + AccessToken: response.Status.Token, + TokenType: "Bearer", + } + + if response.Spec.ExpirationSeconds != nil { + serviceAccountToken.Expiry = exchangeTime.Add(time.Duration(*response.Spec.ExpirationSeconds) * time.Second) + } else { + klog.Warningf("service account token did not include expirationSeconds") + serviceAccountToken.Expiry = exchangeTime + } + + return serviceAccountToken, nil +} diff --git a/rollouts/pkg/tokenexchange/ksatokensource/ksatokensource.go b/rollouts/pkg/tokenexchange/ksatokensource/ksatokensource.go new file mode 100644 index 0000000000..c034ddb4d9 --- /dev/null +++ b/rollouts/pkg/tokenexchange/ksatokensource/ksatokensource.go @@ -0,0 +1,130 @@ +// Copyright 2022 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 ksatokensource + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "time" + + "golang.org/x/oauth2" + stsv1 "google.golang.org/api/sts/v1" +) + +// New returns an oauth2.TokenSource that exchanges the KSA token from ksaToken +// for a GCP access token. +func New(stsService *stsv1.Service, ksaToken oauth2.TokenSource, workloadIdentityPool, identityProvider string) oauth2.TokenSource { + return &ksaTokenSource{ + ksaToken: ksaToken, + workloadIdentityPool: workloadIdentityPool, + identityProvider: identityProvider, + stsService: stsService, + } +} + +// ksaTokenSource implements oauth2.TokenSource for exchanging KSA tokens for +// GCP tokens. It can be wrapped in a ReuseTokenSource to cache tokens until +// expiry. +type ksaTokenSource struct { + // ksaToken is the source of the kubernetes serviceaccount token. + ksaToken oauth2.TokenSource + // workloadIdentityPool is the Workload Identity Pool to use when exchanging the KSA + // token for a GCP token. + workloadIdentityPool string + // identityProvider is the Identity Provider to use when exchanging the KSA + // token for a GCP token. + identityProvider string + + stsService *stsv1.Service +} + +// ksaTokenSource implements oauth2.TokenSource +var _ oauth2.TokenSource = &ksaTokenSource{} + +// Token exchanges a KSA token for a GCP access token, returning the GCP token. +func (ts *ksaTokenSource) Token() (*oauth2.Token, error) { + ksaToken, err := ts.ksaToken.Token() + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + exchangeTime := time.Now() + workloadIdentityPool := ts.workloadIdentityPool + identityProvider := ts.identityProvider + + audience := fmt.Sprintf("identitynamespace:%s:%s", workloadIdentityPool, identityProvider) + + request := &stsv1.GoogleIdentityStsV1ExchangeTokenRequest{ + GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + SubjectToken: ksaToken.AccessToken, + RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token", + Audience: audience, + Scope: "https://www.googleapis.com/auth/iam", + } + + response, err := ts.stsService.V1.Token(request).Context(ctx).Do() + if err != nil { + return nil, fmt.Errorf("failed to get federated token from STS: %w", err) + } + + token := &oauth2.Token{ + AccessToken: response.AccessToken, + TokenType: response.TokenType, + Expiry: exchangeTime.Add(time.Duration(response.ExpiresIn) * time.Second), + } + return token, nil + +} + +// ExtractIsssuer will extract the issuer field from the provided JWT token +func ExtractIssuer(jwtToken string) (string, error) { + return ExtractJWTString(jwtToken, "iss") +} + +// ExtractJWTString extracts the named field from the provided JWT token. +func ExtractJWTString(jwtToken string, key string) (string, error) { + tokens := strings.Split(jwtToken, ".") + if len(tokens) != 3 { + // Don't log the token as it may be sensitive + return "", fmt.Errorf("error getting identity provider from JWT (unexpected number of tokens)") + } + b, err := base64.RawURLEncoding.DecodeString(tokens[1]) + if err != nil { + // Don't log the token as it may be sensitive + return "", fmt.Errorf("error getting identity provider from JWT (cannot decode base64)") + } + m := make(map[string]interface{}) + if err := json.Unmarshal(b, &m); err != nil { + // Don't log the token as it may be sensitive + return "", fmt.Errorf("error getting identity provider from JWT (cannot decode json)") + } + val := m[key] + if val == nil { + // Don't log the token as it may be sensitive + return "", fmt.Errorf("error getting identity provider from JWT (key %q not found)", key) + } + s, ok := val.(string) + if !ok { + // Don't log the token as it may be sensitive + return "", fmt.Errorf("error getting identity provider from JWT (key %q was not string)", key) + } + return s, nil +} diff --git a/rollouts/pkg/tokenexchange/membership/membership.go b/rollouts/pkg/tokenexchange/membership/membership.go new file mode 100644 index 0000000000..7c919b65da --- /dev/null +++ b/rollouts/pkg/tokenexchange/membership/membership.go @@ -0,0 +1,67 @@ +// Copyright 2022 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 membership + +import ( + "context" + "encoding/json" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +var ( + gvr = schema.GroupVersionResource{ + Group: "hub.gke.io", + Version: "v1", + Resource: "memberships", + } +) + +// Membership is the object created by hub when a cluster is registered to hub. +type Membership struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec MembershipSpec `json:"spec,omitempty"` +} + +type MembershipSpec struct { + Owner MembershipOwner `json:"owner,omitempty"` + WorkloadIdentityPool string `json:"workload_identity_pool,omitempty"` + IdentityProvider string `json:"identity_provider,omitempty"` +} + +type MembershipOwner struct { + ID string `json:"id,omitempty"` +} + +// Get gets and returns the Membership named "membership" from the cluster. +func Get(ctx context.Context, client dynamic.Interface) (*Membership, error) { + cr, err := client.Resource(gvr).Get(ctx, "membership", metav1.GetOptions{}) + if err != nil { + return nil, err + } + // round-trip through JSON is a convenient way to get at structured content + b, err := cr.MarshalJSON() + if err != nil { + return nil, err + } + membership := &Membership{} + if err := json.Unmarshal(b, membership); err != nil { + return nil, err + } + return membership, nil +} diff --git a/rollouts/rolloutsclient/client.go b/rollouts/rolloutsclient/client.go new file mode 100644 index 0000000000..01d58cbe79 --- /dev/null +++ b/rollouts/rolloutsclient/client.go @@ -0,0 +1,123 @@ +// Copyright 2023 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 rolloutsclient + +import ( + "context" + "fmt" + "time" + + rolloutsapi "github.com/GoogleContainerTools/kpt/rollouts/api/v1alpha1" + coreapi "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/cli-utils/pkg/flowcontrol" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +// Client implments client for the rollouts API. +type Client struct { + client client.Client +} + +func New() (*Client, error) { + scheme, err := createScheme() + if err != nil { + return nil, err + } + + config := useServerSideThrottling(config.GetConfigOrDie()) + cl, err := client.New(config, client.Options{ + Scheme: scheme, + }) + if err != nil { + return nil, err + } + return &Client{client: cl}, nil +} + +func createScheme() (*runtime.Scheme, error) { + scheme := runtime.NewScheme() + + for _, api := range (runtime.SchemeBuilder{ + rolloutsapi.AddToScheme, + coreapi.AddToScheme, + metav1.AddMetaToScheme, + }) { + if err := api(scheme); err != nil { + return nil, err + } + } + return scheme, nil +} + +func (rlc *Client) List(ctx context.Context, ns string) (*rolloutsapi.RolloutList, error) { + if ns == "" { + ns = "default" + } + + rollouts := &rolloutsapi.RolloutList{} + if err := rlc.client.List(context.Background(), rollouts, client.InNamespace(ns)); err != nil { + return nil, err + } + + return rollouts, nil +} + +func (rlc *Client) Get(ctx context.Context, name string) (*rolloutsapi.Rollout, error) { + if name == "" { + return nil, fmt.Errorf("must provide rollout name") + } + + key := types.NamespacedName{ + Namespace: "default", + Name: name, + } + rollout := &rolloutsapi.Rollout{} + if err := rlc.client.Get(context.Background(), key, rollout); err != nil { + return nil, err + } + + return rollout, nil +} + +func (rlc *Client) Update(ctx context.Context, rollout *rolloutsapi.Rollout) error { + if err := rlc.client.Update(context.Background(), rollout); err != nil { + return err + } + + return nil +} + +func useServerSideThrottling(config *rest.Config) *rest.Config { + // Timeout if the query takes too long + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + enabled, err := flowcontrol.IsEnabled(ctx, config) + if err != nil { + fmt.Printf("Failed to query apiserver to check for flow control enablement: %v\n", err) + } + + if enabled { + config.QPS = -1 + config.Burst = -1 + } + + return config +} diff --git a/tools/licensescan/licensedb_test.go b/tools/licensescan/licensedb_test.go index 757735a308..3aeceeb3b8 100644 --- a/tools/licensescan/licensedb_test.go +++ b/tools/licensescan/licensedb_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 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 (