From a381abc33b10cd89f72d591b746ab4cd8698c521 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 6 Dec 2023 14:41:05 +0100 Subject: [PATCH 001/198] init controller --- .gitignore | 2 + controller/Makefile | 74 ++ controller/apis/deployments/v1alpha1/doc.go | 19 + .../deployments/v1alpha1/git_repository.go | 51 ++ .../deployments/v1alpha1/groupversion_info.go | 36 + .../apis/deployments/v1alpha1/register.go | 12 + .../v1alpha1/zz_generated.deepcopy.go | 136 ++++ ...deployments.plural.sh_gitrepositories.yaml | 80 +++ controller/go.mod | 84 +++ controller/go.sum | 649 ++++++++++++++++++ controller/hack/boilerplate.go.txt | 15 + controller/hack/gen-client-mocks.sh | 11 + controller/hack/lib.sh | 67 ++ controller/main.go | 94 +++ controller/pkg/client/console.go | 48 ++ controller/pkg/client/repository.go | 38 + controller/pkg/client/service.go | 28 + controller/pkg/errors/base.go | 52 ++ .../gitrepository_controller/controller.go | 242 +++++++ controller/pkg/kubernetes/helper.go | 102 +++ controller/pkg/log/zap.go | 154 +++++ controller/pkg/test/mocks/ConsoleClient.go | 243 +++++++ 22 files changed, 2237 insertions(+) create mode 100644 controller/Makefile create mode 100644 controller/apis/deployments/v1alpha1/doc.go create mode 100644 controller/apis/deployments/v1alpha1/git_repository.go create mode 100644 controller/apis/deployments/v1alpha1/groupversion_info.go create mode 100644 controller/apis/deployments/v1alpha1/register.go create mode 100644 controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go create mode 100644 controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml create mode 100644 controller/go.mod create mode 100644 controller/go.sum create mode 100644 controller/hack/boilerplate.go.txt create mode 100755 controller/hack/gen-client-mocks.sh create mode 100644 controller/hack/lib.sh create mode 100644 controller/main.go create mode 100644 controller/pkg/client/console.go create mode 100644 controller/pkg/client/repository.go create mode 100644 controller/pkg/client/service.go create mode 100644 controller/pkg/errors/base.go create mode 100644 controller/pkg/gitrepository_controller/controller.go create mode 100644 controller/pkg/kubernetes/helper.go create mode 100644 controller/pkg/log/zap.go create mode 100644 controller/pkg/test/mocks/ConsoleClient.go diff --git a/.gitignore b/.gitignore index 786d3beff..feb75a6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ watchman-*.tar /priv/data +/controller/bin/ + # IDE /.vscode/**/* !/.vscode/settings.json diff --git a/controller/Makefile b/controller/Makefile new file mode 100644 index 000000000..efd8215be --- /dev/null +++ b/controller/Makefile @@ -0,0 +1,74 @@ +# Image URL to use all building/pushing image targets +IMG ?= deployment-controller:latest +CRD_OPTIONS ?= "crd" + +# 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 + +OPENAPI_PATH=$(GOPATH)/src/k8s.io/kube-openapi + +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 + +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) + +CONTROLLER_GEN = $(shell pwd)/bin/controller-gen +controller-gen: ## Download controller-gen locally if necessary. + $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3) + +##@ Build +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + go generate ./pkg/... + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +fmt: ## Run go fmt against code. + go fmt ./... + +vet: ## Run go vet against code. + go vet ./... + +docker-build: ## Build docker image with the driver. + docker build --no-cache -t ${IMG} . + +docker-push: ## Push docker image with the driver. + docker push ${IMG} + +build: manifests generate fmt vet ## Build controller. + go build -o bin/deployment-controller main.go + +genmock: # generates mocks before running tests + hack/gen-client-mocks.sh + +# go-get-tool will 'go get' any package $2 and install it to $1. +PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +define go-get-tool +@[ -f $(1) ] || { \ +set -e ;\ +TMP_DIR=$$(mktemp -d) ;\ +cd $$TMP_DIR ;\ +go mod init tmp ;\ +echo "Downloading $(2)" ;\ +GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ +rm -rf $$TMP_DIR ;\ +} +endef diff --git a/controller/apis/deployments/v1alpha1/doc.go b/controller/apis/deployments/v1alpha1/doc.go new file mode 100644 index 000000000..be42d6244 --- /dev/null +++ b/controller/apis/deployments/v1alpha1/doc.go @@ -0,0 +1,19 @@ +/* +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. +*/ + +// +groupName=deployments.plural.sh + +package v1alpha1 diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go new file mode 100644 index 000000000..e8cef133c --- /dev/null +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -0,0 +1,51 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + SchemeBuilder.Register(&GitRepository{}, &GitRepositoryList{}) +} + +type GitRepositorySpec struct { + Url string `json:"url"` + + // CredentialsRef is a secret reference which should contain privateKey, passphrase, username and password. + // +optional + CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"` +} + +type GitRepositoryStatus struct { + // Health status. + // +optional + Health *string `json:"health,omitempty"` + // Message indicating details about last transition. + // +optional + Message *string `json:"message,omitempty"` + // Id of repo in console. + // +optional + Id *string `json:"id,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Repo health status" +// +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console repo Id" +type GitRepository struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GitRepositorySpec `json:"spec,omitempty"` + Status GitRepositoryStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type GitRepositoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GitRepository `json:"items"` +} diff --git a/controller/apis/deployments/v1alpha1/groupversion_info.go b/controller/apis/deployments/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..42f945438 --- /dev/null +++ b/controller/apis/deployments/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +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 contains API Schema definitions for the bootstrap v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=deployments.plural.sh +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: "deployments.plural.sh", 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/controller/apis/deployments/v1alpha1/register.go b/controller/apis/deployments/v1alpha1/register.go new file mode 100644 index 000000000..2dfd631eb --- /dev/null +++ b/controller/apis/deployments/v1alpha1/register.go @@ -0,0 +1,12 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = GroupVersion + +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..aeb05f139 --- /dev/null +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,136 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/api/core/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 *GitRepository) DeepCopyInto(out *GitRepository) { + *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 GitRepository. +func (in *GitRepository) DeepCopy() *GitRepository { + if in == nil { + return nil + } + out := new(GitRepository) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitRepository) 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 *GitRepositoryList) DeepCopyInto(out *GitRepositoryList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitRepository, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositoryList. +func (in *GitRepositoryList) DeepCopy() *GitRepositoryList { + if in == nil { + return nil + } + out := new(GitRepositoryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitRepositoryList) 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 *GitRepositorySpec) DeepCopyInto(out *GitRepositorySpec) { + *out = *in + if in.CredentialsRef != nil { + in, out := &in.CredentialsRef, &out.CredentialsRef + *out = new(v1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositorySpec. +func (in *GitRepositorySpec) DeepCopy() *GitRepositorySpec { + if in == nil { + return nil + } + out := new(GitRepositorySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitRepositoryStatus) DeepCopyInto(out *GitRepositoryStatus) { + *out = *in + if in.Health != nil { + in, out := &in.Health, &out.Health + *out = new(string) + **out = **in + } + if in.Message != nil { + in, out := &in.Message, &out.Message + *out = new(string) + **out = **in + } + if in.Id != nil { + in, out := &in.Id, &out.Id + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositoryStatus. +func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus { + if in == nil { + return nil + } + out := new(GitRepositoryStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml new file mode 100644 index 000000000..313a4e214 --- /dev/null +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: gitrepositories.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: GitRepository + listKind: GitRepositoryList + plural: gitrepositories + singular: gitrepository + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Repo health status + jsonPath: .status.health + name: Health + type: string + - description: Console repo Id + jsonPath: .status.id + name: Id + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + credentialsRef: + description: CredentialsRef is a secret reference which should contain + privateKey, passphrase, username and password. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + type: string + required: + - url + type: object + status: + properties: + health: + description: Health status. + type: string + id: + description: Id of repo in console. + type: string + message: + description: Message indicating details about last transition. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controller/go.mod b/controller/go.mod new file mode 100644 index 000000000..43ddf3958 --- /dev/null +++ b/controller/go.mod @@ -0,0 +1,84 @@ +module github.com/pluralsh/console/controller + +go 1.21 + +toolchain go1.21.1 + +require ( + github.com/Yamashou/gqlgenc v0.16.0 + github.com/pluralsh/console-client-go v0.0.53 + github.com/pluralsh/deployment-operator v0.4.0 + github.com/spf13/pflag v1.0.5 + go.uber.org/zap v1.24.0 + k8s.io/api v0.26.0 + k8s.io/apimachinery v0.26.0 + k8s.io/client-go v0.26.0 + k8s.io/klog v1.0.0 + sigs.k8s.io/controller-runtime v0.14.1 + +) + +require ( + 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.9.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // 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.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/imdario/mergo v0.3.13 // 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.4 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // 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/onsi/ginkgo/v2 v2.7.0 // indirect + github.com/onsi/gomega v1.24.2 // indirect + github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/schollz/progressbar/v3 v3.8.6 // indirect + github.com/vektah/gqlparser/v2 v2.5.10 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.4.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // 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.26.0 // indirect + k8s.io/component-base v0.26.0 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect + k8s.io/utils v0.0.0-20230115233650-391b47cb4029 // 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/controller/go.sum b/controller/go.sum new file mode 100644 index 000000000..8cf3ca066 --- /dev/null +++ b/controller/go.sum @@ -0,0 +1,649 @@ +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/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/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/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/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/Yamashou/gqlgenc v0.16.0 h1:k1X/dvwnkiDImaeYw+C1j+GDX3MnzB4aONOTE6Mrku4= +github.com/Yamashou/gqlgenc v0.16.0/go.mod h1:yKaNzczoGrIElG3mG8j2Bg3imv4WyIjLSTRBtvhfMtU= +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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.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/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 v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-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-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +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.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +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/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/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.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.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/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/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +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.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +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 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +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.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +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.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +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/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/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= +github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= +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/pluralsh/console-client-go v0.0.53 h1:Vawo9pP/nrXC8kSP7mUazMIo5YEigRNchDi/RZWnpVc= +github.com/pluralsh/console-client-go v0.0.53/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= +github.com/pluralsh/deployment-operator v0.4.0 h1:WIyC6COAhpbkFWRn0EZ3fH8LfqVb3v3Hm6kCkbl5zIE= +github.com/pluralsh/deployment-operator v0.4.0/go.mod h1:Ld4G2IBZ9oGzzyH87pBAUpurlyyPkaReTv+EmoOZ8jE= +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.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +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/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +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/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= +github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= +github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +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= +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.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.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +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.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +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-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +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/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-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +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-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +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/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.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/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-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-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +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/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.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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.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.0/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.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= +k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= +k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= +k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= +k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= +k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= +k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= +k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= +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-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556WaVQylDXswKmX+dE= +k8s.io/utils v0.0.0-20230115233650-391b47cb4029/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/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM= +sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= +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/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/controller/hack/boilerplate.go.txt b/controller/hack/boilerplate.go.txt new file mode 100644 index 000000000..65b862271 --- /dev/null +++ b/controller/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +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. +*/ \ No newline at end of file diff --git a/controller/hack/gen-client-mocks.sh b/controller/hack/gen-client-mocks.sh new file mode 100755 index 000000000..7c979b2a4 --- /dev/null +++ b/controller/hack/gen-client-mocks.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd $(dirname $0)/.. + +source hack/lib.sh + +CONTAINERIZE_IMAGE=golang:1.21.1 containerize ./hack/gen-client-mocks.sh + +go run github.com/vektra/mockery/v2@latest --dir=pkg/client --name=ConsoleClient --output=pkg/test/mocks \ No newline at end of file diff --git a/controller/hack/lib.sh b/controller/hack/lib.sh new file mode 100644 index 000000000..9bc3e8537 --- /dev/null +++ b/controller/hack/lib.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +echodate() { + # do not use -Is to keep this compatible with macOS + echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)]" "$@" +} + +containerize() { + local cmd="$1" + local image="${CONTAINERIZE_IMAGE:-golang:1.18.4}" + local gocache="${CONTAINERIZE_GOCACHE:-/tmp/.gocache}" + local gomodcache="${CONTAINERIZE_GOMODCACHE:-/tmp/.gomodcache}" + local skip="${NO_CONTAINERIZE:-}" + + # short-circuit containerize when in some cases it needs to be avoided + [ -n "$skip" ] && return + + if ! [ -f /.dockerenv ]; then + echodate "Running $cmd in a Docker container using $image..." + mkdir -p "$gocache" + mkdir -p "$gomodcache" + + exec docker run \ + -v "$PWD":/go/src/pluralsh/gqlclient \ + -v "$gocache":"$gocache" \ + -v "$gomodcache":"$gomodcache" \ + -w /go/src/pluralsh/gqlclient \ + -e "GOCACHE=$gocache" \ + -e "GOMODCACHE=$gomodcache" \ + -u "$(id -u):$(id -g)" \ + --entrypoint="$cmd" \ + --rm \ + -it \ + $image $@ + + exit $? + fi +} + +retry() { + # Works only with bash but doesn't fail on other shells + start_time=$(date +%s) + set +e + actual_retry $@ + rc=$? + return $rc +} +actual_retry() { + retries=$1 + shift + + count=0 + delay=1 + until "$@"; do + rc=$? + count=$((count + 1)) + if [ $count -lt "$retries" ]; then + echo "Retry $count/$retries exited $rc, retrying in $delay seconds..." > /dev/stderr + sleep $delay + else + echo "Retry $count/$retries exited $rc, no more retries left." > /dev/stderr + return $rc + fi + delay=$((delay * 2)) + done + return 0 +} diff --git a/controller/main.go b/controller/main.go new file mode 100644 index 000000000..64b20bdd2 --- /dev/null +++ b/controller/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "flag" + "os" + + deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/client" + gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" + "github.com/pluralsh/console/controller/pkg/log" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + scheme = runtime.NewScheme() + setupLog = log.Logger +) + +func init() { + utilruntime.Must(deploymentsv1alpha.AddToScheme(scheme)) + utilruntime.Must(corev1.AddToScheme(scheme)) +} + +type controllerRunOptions struct { + enableLeaderElection bool + metricsAddr string + probeAddr string + consoleUrl string + consoleToken string +} + +func main() { + klog.InitFlags(nil) + + opt := &controllerRunOptions{} + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.StringVar(&opt.metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&opt.probeAddr, "health-probe-bind-address", ":9001", "The address the probe endpoint binds to.") + flag.BoolVar(&opt.enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.StringVar(&opt.consoleUrl, "console-url", "", "the url of the console api to fetch services from") + flag.StringVar(&opt.consoleToken, "console-token", "", "the deploy token to auth to console api with") + + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + if opt.consoleToken == "" { + opt.consoleToken = os.Getenv("CONSOLE_TOKEN") + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + LeaderElection: opt.enableLeaderElection, + LeaderElectionID: "dep344ab8.plural.sh", + HealthProbeBindAddress: opt.probeAddr, + }) + if err != nil { + setupLog.Error(err, "unable to create manager") + os.Exit(1) + } + if err = mgr.AddHealthzCheck("ping", healthz.Ping); err != nil { + setupLog.Error(err, "unable to create health check") + os.Exit(1) + } + + consoleClient := client.New(opt.consoleUrl, opt.consoleToken) + + if err = (&gitrepositorycontroller.Reconciler{ + Client: mgr.GetClient(), + Log: setupLog.Named("gitrepository-operator"), + ConsoleClient: consoleClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "gitrepository") + os.Exit(1) + } + + ctx := ctrl.SetupSignalHandler() + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go new file mode 100644 index 000000000..c2ddd5a13 --- /dev/null +++ b/controller/pkg/client/console.go @@ -0,0 +1,48 @@ +package client + +import ( + "context" + "net/http" + + console "github.com/pluralsh/console-client-go" +) + +type authedTransport struct { + token string + wrapped http.RoundTripper +} + +func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Token "+t.token) + return t.wrapped.RoundTrip(req) +} + +type client struct { + ctx context.Context + consoleClient *console.Client +} + +type ConsoleClient interface { + GetServices() ([]*console.ServiceDeploymentBaseFragment, error) + GetService(id string) (*console.ServiceDeploymentExtended, error) + UpdateComponents(id string, components []*console.ComponentAttributes, errs []*console.ServiceErrorAttributes) error + CreateRepository(url string, privateKey, passphrase, username, password *string) (*console.CreateGitRepository, error) + ListRepositories() (*console.ListGitRepositories, error) + UpdateRepository(id string, attrs console.GitAttributes) (*console.UpdateGitRepository, error) + DeleteRepository(id string) error + GetRepository(url *string) (*console.GetGitRepository, error) +} + +func New(url, token string) ConsoleClient { + httpClient := http.Client{ + Transport: &authedTransport{ + token: token, + wrapped: http.DefaultTransport, + }, + } + + return &client{ + consoleClient: console.NewClient(&httpClient, url), + ctx: context.Background(), + } +} diff --git a/controller/pkg/client/repository.go b/controller/pkg/client/repository.go new file mode 100644 index 000000000..01a1b74b9 --- /dev/null +++ b/controller/pkg/client/repository.go @@ -0,0 +1,38 @@ +package client + +import ( + console "github.com/pluralsh/console-client-go" +) + +func (c *client) CreateRepository(url string, privateKey, passphrase, username, password *string) (*console.CreateGitRepository, error) { + attrs := console.GitAttributes{ + URL: url, + PrivateKey: privateKey, + Passphrase: passphrase, + Username: username, + Password: password, + } + return c.consoleClient.CreateGitRepository(c.ctx, attrs) + +} + +func (c *client) ListRepositories() (*console.ListGitRepositories, error) { + return c.consoleClient.ListGitRepositories(c.ctx, nil, nil, nil) +} + +func (c *client) UpdateRepository(id string, attrs console.GitAttributes) (*console.UpdateGitRepository, error) { + + return c.consoleClient.UpdateGitRepository(c.ctx, id, attrs) +} + +func (c *client) DeleteRepository(id string) error { + + _, err := c.consoleClient.DeleteGitRepository(c.ctx, id) + + return err +} + +func (c *client) GetRepository(url *string) (*console.GetGitRepository, error) { + + return c.consoleClient.GetGitRepository(c.ctx, nil, url) +} diff --git a/controller/pkg/client/service.go b/controller/pkg/client/service.go new file mode 100644 index 000000000..a122d506c --- /dev/null +++ b/controller/pkg/client/service.go @@ -0,0 +1,28 @@ +package client + +import ( + console "github.com/pluralsh/console-client-go" +) + +func (c *client) GetServices() ([]*console.ServiceDeploymentBaseFragment, error) { + resp, err := c.consoleClient.ListClusterServices(c.ctx) + if err != nil { + return nil, err + } + + return resp.ClusterServices, nil +} + +func (c *client) GetService(id string) (*console.ServiceDeploymentExtended, error) { + resp, err := c.consoleClient.GetServiceDeployment(c.ctx, id) + if err != nil { + return nil, err + } + + return resp.ServiceDeployment, nil +} + +func (c *client) UpdateComponents(id string, components []*console.ComponentAttributes, errs []*console.ServiceErrorAttributes) error { + _, err := c.consoleClient.UpdateServiceComponents(c.ctx, id, components, errs) + return err +} diff --git a/controller/pkg/errors/base.go b/controller/pkg/errors/base.go new file mode 100644 index 000000000..ad5714a70 --- /dev/null +++ b/controller/pkg/errors/base.go @@ -0,0 +1,52 @@ +package errors + +import ( + "errors" + "github.com/Yamashou/gqlgenc/client" +) + +var ErrExpected = errors.New("this is a transient, expected error") + +type KnownError string + +const ( + ErrorNotFound KnownError = "could not find resource" +) + +type wrappedErrorResponse struct { + err *client.ErrorResponse +} + +func (er *wrappedErrorResponse) Has(err KnownError) bool { + if er.err.GqlErrors == nil { + return false + } + + for _, g := range *er.err.GqlErrors { + if g.Message == string(err) { + return true + } + } + + return false +} + +func newAPIError(err *client.ErrorResponse) *wrappedErrorResponse { + return &wrappedErrorResponse{ + err: err, + } +} + +func IsNotFound(err error) bool { + if err == nil { + return false + } + + errorResponse := new(client.ErrorResponse) + ok := errors.As(err, &errorResponse) + if !ok { + return false + } + + return newAPIError(errorResponse).Has(ErrorNotFound) +} diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go new file mode 100644 index 000000000..c8e76185a --- /dev/null +++ b/controller/pkg/gitrepository_controller/controller.go @@ -0,0 +1,242 @@ +package gitrepositorycontroller + +import ( + "context" + "fmt" + "reflect" + "time" + + console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/errors" + "github.com/pluralsh/console/controller/pkg/kubernetes" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + RepoFinalizer = "deployments.plural.sh/gitrepo-protection" + privateKey = "privateKey" + passphrase = "passphrase" + username = "username" + password = "password" +) + +type GitRepoCred struct { + PrivateKey *string + Passphrase *string + Username *string + Password *string +} + +// Reconciler reconciles a GitRepository object +type Reconciler struct { + client.Client + ConsoleClient consoleclient.ConsoleClient + Log *zap.SugaredLogger +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + repo := &v1alpha1.GitRepository{} + if err := r.Get(ctx, req.NamespacedName, repo); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + if !repo.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, repo) + } + + existingRepos, err := r.getRepository(repo.Spec.Url) + if err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + if existingRepos == nil { + cred, err := r.getRepositoryCredentials(ctx, repo) + if err != nil { + return ctrl.Result{}, err + } + resp, err := r.ConsoleClient.CreateRepository(repo.Spec.Url, cred.PrivateKey, cred.Passphrase, cred.Username, cred.Password) + if err != nil { + return ctrl.Result{}, err + } + existingRepos = resp.CreateGitRepository + } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { + return ctrl.Result{}, err + } + + if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + r.Status.Message = existingRepos.Error + r.Status.Id = &existingRepos.ID + if existingRepos.Health != nil { + r.Status.Health = (*string)(existingRepos.Health) + } + + }); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{ + // update status + RequeueAfter: 30 * time.Second, + }, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.GitRepository{}). + Complete(r) +} + +func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { + if repo.Spec.CredentialsRef != nil { + secret := &corev1.Secret{} + name := types.NamespacedName{Name: repo.Spec.CredentialsRef.Name, Namespace: repo.Spec.CredentialsRef.Namespace} + err := r.Get(ctx, name, secret) + if err != nil { + if !apierrors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + if secret.Name != "" { + if controllerutil.ContainsFinalizer(secret, RepoFinalizer) { + r.Log.Info("delete credential secret") + err := r.deleteSecret(ctx, repo.Spec.CredentialsRef.Namespace, repo.Spec.CredentialsRef.Name) + if err != nil { + return ctrl.Result{}, err + } + if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, secret, RepoFinalizer); err != nil { + return ctrl.Result{}, err + } + } + } + } + + if controllerutil.ContainsFinalizer(repo, RepoFinalizer) { + r.Log.Info("delete git repository") + if repo.Status.Id == nil { + return ctrl.Result{}, fmt.Errorf("the repoository ID can not be nil") + } + existingRepos, err := r.getRepository(repo.Spec.Url) + if err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + if existingRepos != nil { + if err := r.ConsoleClient.DeleteRepository(*repo.Status.Id); err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + } + if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + +func (r *Reconciler) getRepositoryCredentials(ctx context.Context, repo *v1alpha1.GitRepository) (*GitRepoCred, error) { + cred := &GitRepoCred{} + if repo.Spec.CredentialsRef != nil { + secret := &corev1.Secret{} + name := types.NamespacedName{Name: repo.Spec.CredentialsRef.Name, Namespace: repo.Spec.CredentialsRef.Namespace} + err := r.Get(ctx, name, secret) + if err != nil { + return nil, err + } + + if err := kubernetes.TryAddFinalizer(ctx, r.Client, secret, RepoFinalizer); err != nil { + return nil, err + } + + privateKey := string(secret.Data[privateKey]) + passphrase := string(secret.Data[passphrase]) + username := string(secret.Data[username]) + password := string(secret.Data[password]) + if privateKey != "" { + cred.PrivateKey = &privateKey + } + if passphrase != "" { + cred.Passphrase = &passphrase + } + if username != "" { + cred.Username = &username + } + if password != "" { + cred.Password = &password + } + } + return cred, nil +} + +func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, error) { + existingRepos, err := r.ConsoleClient.GetRepository(&url) + if err != nil { + return nil, err + } + + return existingRepos.GitRepository, nil +} + +func (r *Reconciler) deleteSecret(ctx context.Context, secretNamespace, secretName string) error { + if secretName == "" { + return nil + } + + secret := &corev1.Secret{} + name := types.NamespacedName{Name: secretName, Namespace: secretNamespace} + err := r.Get(ctx, name, secret) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return fmt.Errorf("failed to get Secret %q: %w", name.String(), err) + } + + if err := r.Delete(ctx, secret); err != nil { + return fmt.Errorf("failed to delete Secret %q: %w", name.String(), err) + } + + // We successfully deleted the secret + return nil +} + +type RepoPatchFunc func(repo *v1alpha1.GitRepository) + +func UpdateReposStatus(ctx context.Context, client ctrlruntimeclient.Client, bootstrap *v1alpha1.GitRepository, patch RepoPatchFunc) error { + key := ctrlruntimeclient.ObjectKeyFromObject(bootstrap) + + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + // fetch the current state of the cluster + if err := client.Get(ctx, key, bootstrap); err != nil { + return err + } + + // modify it + original := bootstrap.DeepCopy() + patch(bootstrap) + + // save some work + if reflect.DeepEqual(original.Status, bootstrap.Status) { + return nil + } + + // update the status + return client.Status().Patch(ctx, bootstrap, ctrlruntimeclient.MergeFrom(original)) + }) +} diff --git a/controller/pkg/kubernetes/helper.go b/controller/pkg/kubernetes/helper.go new file mode 100644 index 000000000..c83327ec7 --- /dev/null +++ b/controller/pkg/kubernetes/helper.go @@ -0,0 +1,102 @@ +package kubernetes + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/util/retry" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// RemoveFinalizer removes the given finalizers from the object. +func RemoveFinalizer(obj metav1.Object, toRemove ...string) { + set := sets.NewString(obj.GetFinalizers()...) + set.Delete(toRemove...) + obj.SetFinalizers(set.List()) +} + +func TryRemoveFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj ctrlruntimeclient.Object, finalizers ...string) error { + key := ctrlruntimeclient.ObjectKeyFromObject(obj) + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // fetch the current state of the object + if err := client.Get(ctx, key, obj); err != nil { + // finalizer removal normally happens during object cleanup, so if + // the object is gone already, that is absolutely fine + if apierrors.IsNotFound(err) { + return nil + } + return err + } + + original := obj.DeepCopyObject().(ctrlruntimeclient.Object) + + // modify it + previous := sets.NewString(obj.GetFinalizers()...) + RemoveFinalizer(obj, finalizers...) + current := sets.NewString(obj.GetFinalizers()...) + + // save some work + if previous.Equal(current) { + return nil + } + + // update the object + return client.Patch(ctx, obj, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) + }) + + if err != nil { + kind := obj.GetObjectKind().GroupVersionKind().Kind + return fmt.Errorf("failed to remove finalizers %v from %s %s: %w", finalizers, kind, key, err) + } + + return nil +} + +// AddFinalizer will add the given finalizer to the object. It uses a StringSet to avoid duplicates. +func AddFinalizer(obj metav1.Object, finalizers ...string) { + set := sets.NewString(obj.GetFinalizers()...) + set.Insert(finalizers...) + obj.SetFinalizers(set.List()) +} + +func TryAddFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj ctrlruntimeclient.Object, finalizers ...string) error { + key := ctrlruntimeclient.ObjectKeyFromObject(obj) + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // fetch the current state of the object + if err := client.Get(ctx, key, obj); err != nil { + return err + } + + // cannot add new finalizers to deleted objects + if obj.GetDeletionTimestamp() != nil { + return nil + } + + original := obj.DeepCopyObject().(ctrlruntimeclient.Object) + + // modify it + previous := sets.NewString(obj.GetFinalizers()...) + AddFinalizer(obj, finalizers...) + current := sets.NewString(obj.GetFinalizers()...) + + // save some work + if previous.Equal(current) { + return nil + } + + // update the object + return client.Patch(ctx, obj, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) + }) + + if err != nil { + kind := obj.GetObjectKind().GroupVersionKind().Kind + return fmt.Errorf("failed to add finalizers %v to %s %s: %w", finalizers, kind, key, err) + } + + return nil +} diff --git a/controller/pkg/log/zap.go b/controller/pkg/log/zap.go new file mode 100644 index 000000000..ac0ba9a3a --- /dev/null +++ b/controller/pkg/log/zap.go @@ -0,0 +1,154 @@ +package log + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/spf13/pflag" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func init() { + DefaultOptions = NewDefaultOptions() + Logger = NewFromOptions(DefaultOptions).Sugar() +} + +var ( + Logger *zap.SugaredLogger + DefaultOptions Options +) + +// Options exports options struct to be used by cmd's. +type Options struct { + // Enable debug logs + Debug bool + // Log format (JSON or plain text) + Format Format +} + +func NewDefaultOptions() Options { + return Options{ + Debug: false, + Format: FormatJSON, + } +} + +func (o *Options) AddFlags(fs *flag.FlagSet) { + fs.BoolVar(&o.Debug, "log-debug", o.Debug, "Enables more verbose logging") + fs.Var(&o.Format, "log-format", "Log format, one of "+AvailableFormats.String()) +} + +func (o *Options) AddPFlags(fs *pflag.FlagSet) { + fs.BoolVar(&o.Debug, "log-debug", o.Debug, "Enables more verbose logging") + fs.Var(&o.Format, "log-format", "Log format, one of "+AvailableFormats.String()) +} + +func (o *Options) Validate() error { + if !AvailableFormats.Contains(o.Format) { + return fmt.Errorf("invalid log-format specified %q; available: %s", o.Format, AvailableFormats.String()) + } + return nil +} + +type Format string + +// Type implements the pflag.Value interfaces. +func (f *Format) Type() string { + return "string" +} + +// String implements the cli.Value and flag.Value interfaces. +func (f *Format) String() string { + return string(*f) +} + +// Set implements the cli.Value and flag.Value interfaces. +func (f *Format) Set(s string) error { + switch strings.ToLower(s) { + case "json": + *f = FormatJSON + return nil + case "console": + *f = FormatConsole + return nil + default: + return fmt.Errorf("invalid format '%s'", s) + } +} + +type Formats []Format + +const ( + FormatJSON Format = "JSON" + FormatConsole Format = "Console" +) + +var ( + AvailableFormats = Formats{FormatJSON, FormatConsole} +) + +func (f Formats) String() string { + const separator = ", " + var s string + for _, format := range f { + s = s + separator + string(format) + } + return strings.TrimPrefix(s, separator) +} + +func (f Formats) Contains(s Format) bool { + for _, format := range f { + if s == format { + return true + } + } + return false +} + +func NewFromOptions(o Options) *zap.Logger { + return New(o.Debug, o.Format) +} + +func New(debug bool, format Format) *zap.Logger { + // this basically mimics NewConfig, but with a custom sink + sink := zapcore.AddSync(os.Stderr) + + // Level - We only support setting Info+ or Debug+ + lvl := zap.NewAtomicLevelAt(zap.InfoLevel) + if debug { + lvl = zap.NewAtomicLevelAt(zap.DebugLevel) + } + + encCfg := zap.NewProductionEncoderConfig() + // Having a dateformat makes it easier to look at logs outside of something like Kibana + encCfg.TimeKey = "time" + encCfg.EncodeTime = zapcore.ISO8601TimeEncoder + + // production config encodes durations as a float of the seconds value, but we want a more + // readable, precise representation + encCfg.EncodeDuration = zapcore.StringDurationEncoder + + var enc zapcore.Encoder + if format == FormatJSON { + enc = zapcore.NewJSONEncoder(encCfg) + } else { + enc = zapcore.NewConsoleEncoder(encCfg) + } + + opts := []zap.Option{ + zap.AddCaller(), + zap.ErrorOutput(sink), + } + + // coreLog := zapcore.NewCore(&ctrlruntimelzap.KubeAwareEncoder{Encoder: enc}, sink, lvl) + coreLog := zapcore.NewCore(enc, sink, lvl) + return zap.New(coreLog, opts...) +} + +// NewDefault creates new default logger. +func NewDefault() *zap.Logger { + return New(false, FormatJSON) +} diff --git a/controller/pkg/test/mocks/ConsoleClient.go b/controller/pkg/test/mocks/ConsoleClient.go new file mode 100644 index 000000000..59b4db50b --- /dev/null +++ b/controller/pkg/test/mocks/ConsoleClient.go @@ -0,0 +1,243 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + gqlclient "github.com/pluralsh/console-client-go" + mock "github.com/stretchr/testify/mock" +) + +// ConsoleClient is an autogenerated mock type for the ConsoleClient type +type ConsoleClient struct { + mock.Mock +} + +// CreateRepository provides a mock function with given fields: url, privateKey, passphrase, username, password +func (_m *ConsoleClient) CreateRepository(url string, privateKey *string, passphrase *string, username *string, password *string) (*gqlclient.CreateGitRepository, error) { + ret := _m.Called(url, privateKey, passphrase, username, password) + + if len(ret) == 0 { + panic("no return value specified for CreateRepository") + } + + var r0 *gqlclient.CreateGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) (*gqlclient.CreateGitRepository, error)); ok { + return rf(url, privateKey, passphrase, username, password) + } + if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) *gqlclient.CreateGitRepository); ok { + r0 = rf(url, privateKey, passphrase, username, password) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.CreateGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(string, *string, *string, *string, *string) error); ok { + r1 = rf(url, privateKey, passphrase, username, password) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteRepository provides a mock function with given fields: id +func (_m *ConsoleClient) DeleteRepository(id string) error { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for DeleteRepository") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetRepository provides a mock function with given fields: url +func (_m *ConsoleClient) GetRepository(url *string) (*gqlclient.GetGitRepository, error) { + ret := _m.Called(url) + + if len(ret) == 0 { + panic("no return value specified for GetRepository") + } + + var r0 *gqlclient.GetGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(*string) (*gqlclient.GetGitRepository, error)); ok { + return rf(url) + } + if rf, ok := ret.Get(0).(func(*string) *gqlclient.GetGitRepository); ok { + r0 = rf(url) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.GetGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(*string) error); ok { + r1 = rf(url) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetService provides a mock function with given fields: id +func (_m *ConsoleClient) GetService(id string) (*gqlclient.ServiceDeploymentExtended, error) { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for GetService") + } + + var r0 *gqlclient.ServiceDeploymentExtended + var r1 error + if rf, ok := ret.Get(0).(func(string) (*gqlclient.ServiceDeploymentExtended, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(string) *gqlclient.ServiceDeploymentExtended); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentExtended) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetServices provides a mock function with given fields: +func (_m *ConsoleClient) GetServices() ([]*gqlclient.ServiceDeploymentBaseFragment, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetServices") + } + + var r0 []*gqlclient.ServiceDeploymentBaseFragment + var r1 error + if rf, ok := ret.Get(0).(func() ([]*gqlclient.ServiceDeploymentBaseFragment, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []*gqlclient.ServiceDeploymentBaseFragment); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gqlclient.ServiceDeploymentBaseFragment) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRepositories provides a mock function with given fields: +func (_m *ConsoleClient) ListRepositories() (*gqlclient.ListGitRepositories, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListRepositories") + } + + var r0 *gqlclient.ListGitRepositories + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListGitRepositories, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListGitRepositories); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListGitRepositories) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateComponents provides a mock function with given fields: id, components, errs +func (_m *ConsoleClient) UpdateComponents(id string, components []*gqlclient.ComponentAttributes, errs []*gqlclient.ServiceErrorAttributes) error { + ret := _m.Called(id, components, errs) + + if len(ret) == 0 { + panic("no return value specified for UpdateComponents") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, []*gqlclient.ComponentAttributes, []*gqlclient.ServiceErrorAttributes) error); ok { + r0 = rf(id, components, errs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRepository provides a mock function with given fields: id, attrs +func (_m *ConsoleClient) UpdateRepository(id string, attrs gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error) { + ret := _m.Called(id, attrs) + + if len(ret) == 0 { + panic("no return value specified for UpdateRepository") + } + + var r0 *gqlclient.UpdateGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error)); ok { + return rf(id, attrs) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) *gqlclient.UpdateGitRepository); ok { + r0 = rf(id, attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.UpdateGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.GitAttributes) error); ok { + r1 = rf(id, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewConsoleClient creates a new instance of ConsoleClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewConsoleClient(t interface { + mock.TestingT + Cleanup(func()) +}) *ConsoleClient { + mock := &ConsoleClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 4062b8ce8599542fe7154b4706d2cd4fc928fd9b Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 7 Dec 2023 11:42:47 +0100 Subject: [PATCH 002/198] initialize cluster model --- .../apis/deployments/v1alpha1/cluster.go | 156 +++++++++ .../apis/deployments/v1alpha1/common.go | 17 + .../v1alpha1/zz_generated.deepcopy.go | 313 ++++++++++++++++++ .../bases/deployments.plural.sh_clusters.yaml | 207 ++++++++++++ controller/go.mod | 6 +- controller/go.sum | 9 +- 6 files changed, 702 insertions(+), 6 deletions(-) create mode 100644 controller/apis/deployments/v1alpha1/cluster.go create mode 100644 controller/apis/deployments/v1alpha1/common.go create mode 100644 controller/config/crd/bases/deployments.plural.sh_clusters.yaml diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go new file mode 100644 index 000000000..b61823391 --- /dev/null +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -0,0 +1,156 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +func init() { + SchemeBuilder.Register(&Cluster{}, &ClusterList{}) +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Cluster `json:"items"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Repo health status" +// +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console cluster ID" +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ClusterSpec `json:"spec,omitempty"` + Status ClusterStatus `json:"status,omitempty"` +} + +type ClusterSpec struct { + // Handle is a short, unique human-readable name used to identify this cluster. + // Does not necessarily map to the cloud resource name. + // +kubebuilder:validation:Optional + Handle *string `json:"handle,omitempty"` + + // Version of Kubernetes to use for this cluster. Optional only for BYOK. + // +kubebuilder:validation:Optional + Version *string `json:"version,omitempty"` + + // TODO: ProviderRef + + // Cloud provider to use for this cluster. + // +kubebuilder:validation:Enum=aws;azure;gcp;byok + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Cloud is immutable" + Cloud string `json:"cloud"` + + // Protect cluster from being deleted. + // +kubebuilder:validation:Optional + Protect *bool `json:"protect,omitempty"` + + // TODO: Tags + + // TODO: Bindings + + // CloudSettings contains cloud-specific settings for this cluster. + // +kubebuilder:validation:Optional + CloudSettings *ClusterCloudSettings `json:"cloudSettings,omitempty"` + + // NodePools contains specs of node pools managed by this cluster. + NodePools []ClusterNodePool `json:"nodePools"` +} + +type ClusterCloudSettings struct { + // AWS cluster customizations. + // +kubebuilder:validation:Optional + AWS *ClusterAWSCloudSettings `json:"aws,omitempty"` + + // Azure cluster customizations. + // +kubebuilder:validation:Optional + Azure *ClusterAzureCloudSettings `json:"azure,omitempty"` + + // GCP cluster customizations. + // +kubebuilder:validation:Optional + GCP *ClusterGCPCloudSettings `json:"gcp,omitempty"` + + // BYOK cluster customizations. + // +kubebuilder:validation:Optional + BYOK *ClusterBYOKCloudSettings `json:"byok,omitempty"` +} + +type ClusterAWSCloudSettings struct { + // Region in AWS to deploy this cluster to. + Region string `json:"region"` +} + +type ClusterAzureCloudSettings struct { + // ResourceGroup is a name for the Azure resource group for this cluster. + ResourceGroup string `json:"resourceGroup"` + + // Network is a name for the Azure virtual network for this cluster. + Network string `json:"network"` + + // SubscriptionId is GUID of the Azure subscription to hold this cluster. + SubscriptionId string `json:"subscriptionId"` + + // Location in Azure to deploy this cluster to, i.e. eastus. + Location string `json:"location"` +} + +type ClusterGCPCloudSettings struct { +} + +type ClusterBYOKCloudSettings struct { +} + +type ClusterNodePool struct { + // Name of the node pool. Must be unique. + Name string `json:"name"` + + // InstanceType contains the type of node to use. Usually cloud-specific. + InstanceType string `json:"instanceType"` + + // MinSize is minimum number of instances in this node pool. + // +kubebuilder:validation:Minimum=1 + MinSize int `json:"minSize"` + + // MaxSize is maximum number of instances in this node pool. + // +kubebuilder:validation:Minimum=1 + MaxSize int `json:"maxSize"` + + // Labels to apply to the nodes in this pool. Useful for node selectors. + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` + + // Taints you'd want to apply to a node, i.e. for preventing scheduling on spot instances. + // +kubebuilder:validation:Optional + Taints []Taint `json:"taints,omitempty"` + + // CloudSettings contains cloud-specific settings for this node pool. + // +kubebuilder:validation:Optional + CloudSettings *ClusterNodePoolCloudSettings `json:"cloudSettings,omitempty"` +} + +type ClusterNodePoolCloudSettings struct { + // AWS node pool customizations. + // +kubebuilder:validation:Optional + AWS *ClusterNodePoolAWSCloudSettings `json:"aws,omitempty"` +} + +type ClusterNodePoolAWSCloudSettings struct { + // LaunchTemplateId is an ID of custom launch template for your nodes. Useful for Golden AMI setups. + // +kubebuilder:validation:Optional + LaunchTemplateId *string `json:"launchTemplateId,omitempty"` +} + +type ClusterStatus struct { + // Id from Console. + // +kubebuilder:validation:Optional + Id *string `json:"id,omitempty"` + + // CurrentVersion contains current Kubernetes version this cluster is using. + // +kubebuilder:validation:Optional + CurrentVersion *string `json:"currentVersion,omitempty"` + + // Health status. + // +optional + Health *string `json:"health,omitempty"` +} diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go new file mode 100644 index 000000000..c3df4bfbc --- /dev/null +++ b/controller/apis/deployments/v1alpha1/common.go @@ -0,0 +1,17 @@ +package v1alpha1 + +// Taint represents a Kubernetes taint. +type Taint struct { + // Effect specifies the effect for the taint. + // +kubebuilder:validation:Enum=NoSchedule;NoExecute;PreferNoSchedule + Effect TaintEffect `json:"effect"` + + // Key is the key of the taint. + Key string `json:"key"` + + // Value is the value of the taint. + Value string `json:"value"` +} + +// TaintEffect is the effect for a Kubernetes taint. +type TaintEffect string diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index aeb05f139..2c545a9ad 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,304 @@ import ( 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 *Cluster) DeepCopyInto(out *Cluster) { + *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 Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Cluster) 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 *ClusterAWSCloudSettings) DeepCopyInto(out *ClusterAWSCloudSettings) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAWSCloudSettings. +func (in *ClusterAWSCloudSettings) DeepCopy() *ClusterAWSCloudSettings { + if in == nil { + return nil + } + out := new(ClusterAWSCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAzureCloudSettings) DeepCopyInto(out *ClusterAzureCloudSettings) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAzureCloudSettings. +func (in *ClusterAzureCloudSettings) DeepCopy() *ClusterAzureCloudSettings { + if in == nil { + return nil + } + out := new(ClusterAzureCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterBYOKCloudSettings) DeepCopyInto(out *ClusterBYOKCloudSettings) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterBYOKCloudSettings. +func (in *ClusterBYOKCloudSettings) DeepCopy() *ClusterBYOKCloudSettings { + if in == nil { + return nil + } + out := new(ClusterBYOKCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterCloudSettings) DeepCopyInto(out *ClusterCloudSettings) { + *out = *in + if in.AWS != nil { + in, out := &in.AWS, &out.AWS + *out = new(ClusterAWSCloudSettings) + **out = **in + } + if in.Azure != nil { + in, out := &in.Azure, &out.Azure + *out = new(ClusterAzureCloudSettings) + **out = **in + } + if in.GCP != nil { + in, out := &in.GCP, &out.GCP + *out = new(ClusterGCPCloudSettings) + **out = **in + } + if in.BYOK != nil { + in, out := &in.BYOK, &out.BYOK + *out = new(ClusterBYOKCloudSettings) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCloudSettings. +func (in *ClusterCloudSettings) DeepCopy() *ClusterCloudSettings { + if in == nil { + return nil + } + out := new(ClusterCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterGCPCloudSettings) DeepCopyInto(out *ClusterGCPCloudSettings) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGCPCloudSettings. +func (in *ClusterGCPCloudSettings) DeepCopy() *ClusterGCPCloudSettings { + if in == nil { + return nil + } + out := new(ClusterGCPCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterList) DeepCopyInto(out *ClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Cluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterList. +func (in *ClusterList) DeepCopy() *ClusterList { + if in == nil { + return nil + } + out := new(ClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterList) 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 *ClusterNodePool) DeepCopyInto(out *ClusterNodePool) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]Taint, len(*in)) + copy(*out, *in) + } + if in.CloudSettings != nil { + in, out := &in.CloudSettings, &out.CloudSettings + *out = new(ClusterNodePoolCloudSettings) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNodePool. +func (in *ClusterNodePool) DeepCopy() *ClusterNodePool { + if in == nil { + return nil + } + out := new(ClusterNodePool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterNodePoolAWSCloudSettings) DeepCopyInto(out *ClusterNodePoolAWSCloudSettings) { + *out = *in + if in.LaunchTemplateId != nil { + in, out := &in.LaunchTemplateId, &out.LaunchTemplateId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNodePoolAWSCloudSettings. +func (in *ClusterNodePoolAWSCloudSettings) DeepCopy() *ClusterNodePoolAWSCloudSettings { + if in == nil { + return nil + } + out := new(ClusterNodePoolAWSCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterNodePoolCloudSettings) DeepCopyInto(out *ClusterNodePoolCloudSettings) { + *out = *in + if in.AWS != nil { + in, out := &in.AWS, &out.AWS + *out = new(ClusterNodePoolAWSCloudSettings) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNodePoolCloudSettings. +func (in *ClusterNodePoolCloudSettings) DeepCopy() *ClusterNodePoolCloudSettings { + if in == nil { + return nil + } + out := new(ClusterNodePoolCloudSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { + *out = *in + if in.Handle != nil { + in, out := &in.Handle, &out.Handle + *out = new(string) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + if in.Protect != nil { + in, out := &in.Protect, &out.Protect + *out = new(bool) + **out = **in + } + if in.CloudSettings != nil { + in, out := &in.CloudSettings, &out.CloudSettings + *out = new(ClusterCloudSettings) + (*in).DeepCopyInto(*out) + } + if in.NodePools != nil { + in, out := &in.NodePools, &out.NodePools + *out = make([]ClusterNodePool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. +func (in *ClusterSpec) DeepCopy() *ClusterSpec { + if in == nil { + return nil + } + out := new(ClusterSpec) + 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 + if in.Id != nil { + in, out := &in.Id, &out.Id + *out = new(string) + **out = **in + } + if in.CurrentVersion != nil { + in, out := &in.CurrentVersion, &out.CurrentVersion + *out = new(string) + **out = **in + } + if in.Health != nil { + in, out := &in.Health, &out.Health + *out = new(string) + **out = **in + } +} + +// 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 *GitRepository) DeepCopyInto(out *GitRepository) { *out = *in @@ -134,3 +432,18 @@ func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Taint) DeepCopyInto(out *Taint) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Taint. +func (in *Taint) DeepCopy() *Taint { + if in == nil { + return nil + } + out := new(Taint) + in.DeepCopyInto(out) + return out +} diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml new file mode 100644 index 000000000..8deeb20bf --- /dev/null +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -0,0 +1,207 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: clusters.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: Cluster + listKind: ClusterList + plural: clusters + singular: cluster + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Repo health status + jsonPath: .status.health + name: Health + type: string + - description: Console cluster ID + jsonPath: .status.id + name: Id + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + cloud: + description: Cloud provider to use for this cluster. + enum: + - aws + - azure + - gcp + - byok + type: string + x-kubernetes-validations: + - message: Cloud is immutable + rule: self == oldSelf + cloudSettings: + description: CloudSettings contains cloud-specific settings for this + cluster. + properties: + aws: + description: AWS cluster customizations. + properties: + region: + description: Region in AWS to deploy this cluster to. + type: string + required: + - region + type: object + azure: + description: Azure cluster customizations. + properties: + location: + description: Location in Azure to deploy this cluster to, + i.e. eastus. + type: string + network: + description: Network is a name for the Azure virtual network + for this cluster. + type: string + resourceGroup: + description: ResourceGroup is a name for the Azure resource + group for this cluster. + type: string + subscriptionId: + description: SubscriptionId is GUID of the Azure subscription + to hold this cluster. + type: string + required: + - location + - network + - resourceGroup + - subscriptionId + type: object + byok: + description: BYOK cluster customizations. + type: object + gcp: + description: GCP cluster customizations. + type: object + type: object + handle: + description: Handle is a short, unique human-readable name used to + identify this cluster. Does not necessarily map to the cloud resource + name. + type: string + nodePools: + description: NodePools contains specs of node pools managed by this + cluster. + items: + properties: + cloudSettings: + description: CloudSettings contains cloud-specific settings + for this node pool. + properties: + aws: + description: AWS node pool customizations. + properties: + launchTemplateId: + description: LaunchTemplateId is an ID of custom launch + template for your nodes. Useful for Golden AMI setups. + type: string + type: object + type: object + instanceType: + description: InstanceType contains the type of node to use. + Usually cloud-specific. + type: string + labels: + additionalProperties: + type: string + description: Labels to apply to the nodes in this pool. Useful + for node selectors. + type: object + maxSize: + description: MaxSize is maximum number of instances in this + node pool. + minimum: 1 + type: integer + minSize: + description: MinSize is minimum number of instances in this + node pool. + minimum: 1 + type: integer + name: + description: Name of the node pool. Must be unique. + type: string + taints: + description: Taints you'd want to apply to a node, i.e. for + preventing scheduling on spot instances. + items: + description: Taint represents a Kubernetes taint. + properties: + effect: + description: Effect specifies the effect for the taint. + enum: + - NoSchedule + - NoExecute + - PreferNoSchedule + type: string + key: + description: Key is the key of the taint. + type: string + value: + description: Value is the value of the taint. + type: string + required: + - effect + - key + - value + type: object + type: array + required: + - instanceType + - maxSize + - minSize + - name + type: object + type: array + protect: + description: Protect cluster from being deleted. + type: boolean + version: + description: Version of Kubernetes to use for this cluster. Optional + only for BYOK. + type: string + required: + - cloud + - nodePools + type: object + status: + properties: + currentVersion: + description: CurrentVersion contains current Kubernetes version this + cluster is using. + type: string + health: + description: Health status. + type: string + id: + description: Id from Console. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controller/go.mod b/controller/go.mod index 43ddf3958..165978b1a 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -7,8 +7,8 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 github.com/pluralsh/console-client-go v0.0.53 - github.com/pluralsh/deployment-operator v0.4.0 github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 k8s.io/api v0.26.0 k8s.io/apimachinery v0.26.0 @@ -23,6 +23,7 @@ require ( 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.9.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -49,14 +50,15 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.7.0 // indirect github.com/onsi/gomega v1.24.2 // indirect - github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/schollz/progressbar/v3 v3.8.6 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/vektah/gqlparser/v2 v2.5.10 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect diff --git a/controller/go.sum b/controller/go.sum index 8cf3ca066..70e9f6710 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -224,16 +224,12 @@ github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= -github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= -github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= 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/pluralsh/console-client-go v0.0.53 h1:Vawo9pP/nrXC8kSP7mUazMIo5YEigRNchDi/RZWnpVc= github.com/pluralsh/console-client-go v0.0.53/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= -github.com/pluralsh/deployment-operator v0.4.0 h1:WIyC6COAhpbkFWRn0EZ3fH8LfqVb3v3Hm6kCkbl5zIE= -github.com/pluralsh/deployment-operator v0.4.0/go.mod h1:Ld4G2IBZ9oGzzyH87pBAUpurlyyPkaReTv+EmoOZ8jE= 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= @@ -277,11 +273,16 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= From d697d960505ca54ee3258d6284449cb5140efc4e Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 7 Dec 2023 11:55:32 +0100 Subject: [PATCH 003/198] add tags and bindings to cluster model --- .../apis/deployments/v1alpha1/cluster.go | 13 +++- .../apis/deployments/v1alpha1/common.go | 24 +++++++ .../v1alpha1/zz_generated.deepcopy.go | 71 +++++++++++++++++++ .../bases/deployments.plural.sh_clusters.yaml | 47 +++++++++++- 4 files changed, 151 insertions(+), 4 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index b61823391..01c276535 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -16,7 +16,7 @@ type ClusterList struct { // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Repo health status" +// +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Cluster health status" // +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console cluster ID" type Cluster struct { metav1.TypeMeta `json:",inline"` @@ -46,9 +46,13 @@ type ClusterSpec struct { // +kubebuilder:validation:Optional Protect *bool `json:"protect,omitempty"` - // TODO: Tags + // Tags used to filter clusters. + // +kubebuilder:validation:Optional + Tags map[string]string `json:"tags,omitempty"` - // TODO: Bindings + // Bindings contain read and write policies of this cluster + // +kubebuilder:validation:Optional + Bindings *Bindings `json:"bindings,omitempty"` // CloudSettings contains cloud-specific settings for this cluster. // +kubebuilder:validation:Optional @@ -96,9 +100,12 @@ type ClusterAzureCloudSettings struct { } type ClusterGCPCloudSettings struct { + // Project in GCP to deploy cluster to. + Project string `json:"project"` } type ClusterBYOKCloudSettings struct { + // TODO: Decide how we handle BYOK settings and how we will deploy operator. } type ClusterNodePool struct { diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index c3df4bfbc..b318989b7 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -1,5 +1,29 @@ package v1alpha1 +type Bindings struct { + // Read bindings. + // +kubebuilder:validation:Optional + Read []Binding `json:"read,omitempty"` + + // Write bindings. + // +kubebuilder:validation:Optional + Write []Binding `json:"write,omitempty"` +} + +type Binding struct { + // TODO: Add docs. + // +kubebuilder:validation:Optional + Id *string `json:"id,omitempty"` + + // TODO: Add docs. + // +kubebuilder:validation:Optional + UserId *string `json:"userId,omitempty"` + + // TODO: Add docs. + // +kubebuilder:validation:Optional + GroupId *string `json:"groupId,omitempty"` +} + // Taint represents a Kubernetes taint. type Taint struct { // Effect specifies the effect for the taint. diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 2c545a9ad..74ed6c5e3 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,65 @@ import ( 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 *Binding) DeepCopyInto(out *Binding) { + *out = *in + if in.Id != nil { + in, out := &in.Id, &out.Id + *out = new(string) + **out = **in + } + if in.UserId != nil { + in, out := &in.UserId, &out.UserId + *out = new(string) + **out = **in + } + if in.GroupId != nil { + in, out := &in.GroupId, &out.GroupId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Binding. +func (in *Binding) DeepCopy() *Binding { + if in == nil { + return nil + } + out := new(Binding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Bindings) DeepCopyInto(out *Bindings) { + *out = *in + if in.Read != nil { + in, out := &in.Read, &out.Read + *out = make([]Binding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Write != nil { + in, out := &in.Write, &out.Write + *out = make([]Binding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bindings. +func (in *Bindings) DeepCopy() *Bindings { + if in == nil { + return nil + } + out := new(Bindings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in @@ -270,6 +329,18 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = new(bool) **out = **in } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Bindings != nil { + in, out := &in.Bindings, &out.Bindings + *out = new(Bindings) + (*in).DeepCopyInto(*out) + } if in.CloudSettings != nil { in, out := &in.CloudSettings, &out.CloudSettings *out = new(ClusterCloudSettings) diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 8deeb20bf..6194f8398 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -16,7 +16,7 @@ spec: scope: Cluster versions: - additionalPrinterColumns: - - description: Repo health status + - description: Cluster health status jsonPath: .status.health name: Health type: string @@ -42,6 +42,40 @@ spec: type: object spec: properties: + bindings: + description: Bindings contain read and write policies of this cluster + properties: + read: + description: Read bindings. + items: + properties: + groupId: + description: 'TODO: Add docs.' + type: string + id: + description: 'TODO: Add docs.' + type: string + userId: + description: 'TODO: Add docs.' + type: string + type: object + type: array + write: + description: Write bindings. + items: + properties: + groupId: + description: 'TODO: Add docs.' + type: string + id: + description: 'TODO: Add docs.' + type: string + userId: + description: 'TODO: Add docs.' + type: string + type: object + type: array + type: object cloud: description: Cloud provider to use for this cluster. enum: @@ -96,6 +130,12 @@ spec: type: object gcp: description: GCP cluster customizations. + properties: + project: + description: Project in GCP to deploy cluster to. + type: string + required: + - project type: object type: object handle: @@ -179,6 +219,11 @@ spec: protect: description: Protect cluster from being deleted. type: boolean + tags: + additionalProperties: + type: string + description: Tags used to filter clusters. + type: object version: description: Version of Kubernetes to use for this cluster. Optional only for BYOK. From eed758a90bb047222c20bb6d0930dc72d54ebaaa Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 7 Dec 2023 12:37:54 +0100 Subject: [PATCH 004/198] add provider reference to cluster model --- .../apis/deployments/v1alpha1/cluster.go | 11 +++-- .../v1alpha1/zz_generated.deepcopy.go | 1 + .../bases/deployments.plural.sh_clusters.yaml | 42 ++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 01c276535..44a09a349 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -1,6 +1,9 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) func init() { SchemeBuilder.Register(&Cluster{}, &ClusterList{}) @@ -31,11 +34,13 @@ type ClusterSpec struct { // +kubebuilder:validation:Optional Handle *string `json:"handle,omitempty"` - // Version of Kubernetes to use for this cluster. Optional only for BYOK. + // Version of Kubernetes to use for this cluster. Can be skipped only for BYOK. // +kubebuilder:validation:Optional Version *string `json:"version,omitempty"` - // TODO: ProviderRef + // ProviderRef references provider to use for this cluster. Can be skipped only for BYOK. + // +kubebuilder:validation:Optional + ProviderRef corev1.ObjectReference `json:"providerRef,omitempty"` // Cloud provider to use for this cluster. // +kubebuilder:validation:Enum=aws;azure;gcp;byok diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 74ed6c5e3..95eaec81c 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -324,6 +324,7 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = new(string) **out = **in } + out.ProviderRef = in.ProviderRef if in.Protect != nil { in, out := &in.Protect, &out.Protect *out = new(bool) diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 6194f8398..503596473 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -219,14 +219,52 @@ spec: protect: description: Protect cluster from being deleted. type: boolean + providerRef: + description: ProviderRef references provider to use for this cluster. + Can be skipped only for BYOK. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic tags: additionalProperties: type: string description: Tags used to filter clusters. type: object version: - description: Version of Kubernetes to use for this cluster. Optional - only for BYOK. + description: Version of Kubernetes to use for this cluster. Can be + skipped only for BYOK. type: string required: - cloud From 681699e8ac885a15602549f02398c370cdcfab32 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 7 Dec 2023 13:45:17 +0100 Subject: [PATCH 005/198] add service model --- .../apis/deployments/v1alpha1/common.go | 5 + .../apis/deployments/v1alpha1/service.go | 114 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 controller/apis/deployments/v1alpha1/service.go diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index b318989b7..8b8cef349 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -1,5 +1,10 @@ package v1alpha1 +type NamespacedName struct { + Name string `json:"name"` + Namespace string `json:"namespace"` +} + type Bindings struct { // Read bindings. // +kubebuilder:validation:Optional diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go new file mode 100644 index 000000000..f700d4c13 --- /dev/null +++ b/controller/apis/deployments/v1alpha1/service.go @@ -0,0 +1,114 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ComponentState string + +const ( + ComponentStateRunning ComponentState = "RUNNING" + ComponentStatePending ComponentState = "PENDING" + ComponentStateFailed ComponentState = "FAILED" +) + +type ServiceKustomize struct { + Path string `json:"path"` +} + +type ServiceGit struct { + Folder string `json:"folder"` + Ref string `json:"ref"` +} + +type ServiceHelm struct { + // +optional + Values *string `json:"values,omitempty"` + // +optional + ValuesFiles []string `json:"valuesFiles,omitempty"` + // +optional + Chart *string `json:"chart,omitempty"` + // +optional + Version *string `json:"version,omitempty"` + // +optional + Repository *NamespacedName `json:"repository,omitempty"` +} + +type ServiceSpec struct { + // +optional + DocsPath *string `json:"docsPath,omitempty"` + // +kubebuilder:default:=0.0.1 + Version string `json:"version"` + // +optional + Protect bool `json:"protect,omitempty"` + // +optional + Kustomize *ServiceKustomize `json:"kustomize,omitempty"` + // +optional + Git *ServiceGit `json:"git,omitempty"` + // +optional + Helm *ServiceHelm `json:"helm,omitempty"` + + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Repository is immutable" + RepositoryRef corev1.ObjectReference `json:"repositoryRef"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Cluster is immutable" + ClusterRef corev1.ObjectReference `json:"clusterRef"` + // ConfigurationRef is a secret reference which should contain service configuration. + // +optional + ConfigurationRef *corev1.SecretReference `json:"configurationRef,omitempty"` + // Bindings contain read and write policies of this cluster + // +optional + Bindings *Bindings `json:"bindings,omitempty"` +} + +type ServiceStatus struct { + // Id of service in console. + // +optional + Id *string `json:"id,omitempty"` + // +optional + Errors []ServiceError `json:"errors,omitempty"` + // +optional + Components []ServiceComponent `json:"components,omitempty"` +} + +type ServiceError struct { + Source string `json:"source"` + Message string `json:"message"` +} + +type ServiceComponent struct { + ID string `json:"id"` + Name string `json:"name"` + // +optional + Group *string `json:"group,omitempty"` + Kind string `json:"kind"` + // +optional + Namespace *string `json:"namespace,omitempty"` + // State specifies the component state + // +kubebuilder:validation:Enum:=RUNNING;PENDING;FAILED + // +optional + State *ComponentState `json:"state,omitempty"` + Synced bool `json:"synced"` + // +optional + Version *string `json:"version,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console repo Id" +type Service struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServiceSpec `json:"spec,omitempty"` + Status ServiceStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type ServiceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Service `json:"items"` +} From 7fa9a1cab5b74a606f77f3793259ee4bf035f6e8 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 7 Dec 2023 14:14:13 +0100 Subject: [PATCH 006/198] update service --- .../apis/deployments/v1alpha1/service.go | 6 +- .../v1alpha1/zz_generated.deepcopy.go | 265 +++++++++++++ .../bases/deployments.plural.sh_services.yaml | 352 ++++++++++++++++++ 3 files changed, 620 insertions(+), 3 deletions(-) create mode 100644 controller/config/crd/bases/deployments.plural.sh_services.yaml diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index f700d4c13..b94f7d8b5 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -24,11 +24,11 @@ type ServiceGit struct { type ServiceHelm struct { // +optional - Values *string `json:"values,omitempty"` + ValuesRef corev1.ConfigMapKeySelector `json:"values,omitempty"` // +optional ValuesFiles []string `json:"valuesFiles,omitempty"` // +optional - Chart *string `json:"chart,omitempty"` + ChartRef corev1.ConfigMapKeySelector `json:"chart,omitempty"` // +optional Version *string `json:"version,omitempty"` // +optional @@ -38,7 +38,7 @@ type ServiceHelm struct { type ServiceSpec struct { // +optional DocsPath *string `json:"docsPath,omitempty"` - // +kubebuilder:default:=0.0.1 + // +kubebuilder:default:='0.0.1' Version string `json:"version"` // +optional Protect bool `json:"protect,omitempty"` diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 95eaec81c..edf096ece 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -505,6 +505,271 @@ func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedName) DeepCopyInto(out *NamespacedName) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedName. +func (in *NamespacedName) DeepCopy() *NamespacedName { + if in == nil { + return nil + } + out := new(NamespacedName) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Service) DeepCopyInto(out *Service) { + *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 Service. +func (in *Service) DeepCopy() *Service { + if in == nil { + return nil + } + out := new(Service) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Service) 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 *ServiceComponent) DeepCopyInto(out *ServiceComponent) { + *out = *in + if in.Group != nil { + in, out := &in.Group, &out.Group + *out = new(string) + **out = **in + } + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.State != nil { + in, out := &in.State, &out.State + *out = new(ComponentState) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceComponent. +func (in *ServiceComponent) DeepCopy() *ServiceComponent { + if in == nil { + return nil + } + out := new(ServiceComponent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceError) DeepCopyInto(out *ServiceError) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceError. +func (in *ServiceError) DeepCopy() *ServiceError { + if in == nil { + return nil + } + out := new(ServiceError) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceGit) DeepCopyInto(out *ServiceGit) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceGit. +func (in *ServiceGit) DeepCopy() *ServiceGit { + if in == nil { + return nil + } + out := new(ServiceGit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceHelm) DeepCopyInto(out *ServiceHelm) { + *out = *in + in.ValuesRef.DeepCopyInto(&out.ValuesRef) + if in.ValuesFiles != nil { + in, out := &in.ValuesFiles, &out.ValuesFiles + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.ChartRef.DeepCopyInto(&out.ChartRef) + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + if in.Repository != nil { + in, out := &in.Repository, &out.Repository + *out = new(NamespacedName) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceHelm. +func (in *ServiceHelm) DeepCopy() *ServiceHelm { + if in == nil { + return nil + } + out := new(ServiceHelm) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceKustomize) DeepCopyInto(out *ServiceKustomize) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceKustomize. +func (in *ServiceKustomize) DeepCopy() *ServiceKustomize { + if in == nil { + return nil + } + out := new(ServiceKustomize) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceList) DeepCopyInto(out *ServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Service, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceList. +func (in *ServiceList) DeepCopy() *ServiceList { + if in == nil { + return nil + } + out := new(ServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceList) 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 *ServiceSpec) DeepCopyInto(out *ServiceSpec) { + *out = *in + if in.DocsPath != nil { + in, out := &in.DocsPath, &out.DocsPath + *out = new(string) + **out = **in + } + if in.Kustomize != nil { + in, out := &in.Kustomize, &out.Kustomize + *out = new(ServiceKustomize) + **out = **in + } + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(ServiceGit) + **out = **in + } + if in.Helm != nil { + in, out := &in.Helm, &out.Helm + *out = new(ServiceHelm) + (*in).DeepCopyInto(*out) + } + out.RepositoryRef = in.RepositoryRef + out.ClusterRef = in.ClusterRef + if in.ConfigurationRef != nil { + in, out := &in.ConfigurationRef, &out.ConfigurationRef + *out = new(v1.SecretReference) + **out = **in + } + if in.Bindings != nil { + in, out := &in.Bindings, &out.Bindings + *out = new(Bindings) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. +func (in *ServiceSpec) DeepCopy() *ServiceSpec { + if in == nil { + return nil + } + out := new(ServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { + *out = *in + if in.Id != nil { + in, out := &in.Id, &out.Id + *out = new(string) + **out = **in + } + if in.Errors != nil { + in, out := &in.Errors, &out.Errors + *out = make([]ServiceError, len(*in)) + copy(*out, *in) + } + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = make([]ServiceComponent, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceStatus. +func (in *ServiceStatus) DeepCopy() *ServiceStatus { + if in == nil { + return nil + } + out := new(ServiceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Taint) DeepCopyInto(out *Taint) { *out = *in diff --git a/controller/config/crd/bases/deployments.plural.sh_services.yaml b/controller/config/crd/bases/deployments.plural.sh_services.yaml new file mode 100644 index 000000000..210b00782 --- /dev/null +++ b/controller/config/crd/bases/deployments.plural.sh_services.yaml @@ -0,0 +1,352 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: services.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: Service + listKind: ServiceList + plural: services + singular: service + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Console repo Id + jsonPath: .status.id + name: Id + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + bindings: + description: Bindings contain read and write policies of this cluster + properties: + read: + description: Read bindings. + items: + properties: + groupId: + description: 'TODO: Add docs.' + type: string + id: + description: 'TODO: Add docs.' + type: string + userId: + description: 'TODO: Add docs.' + type: string + type: object + type: array + write: + description: Write bindings. + items: + properties: + groupId: + description: 'TODO: Add docs.' + type: string + id: + description: 'TODO: Add docs.' + type: string + userId: + description: 'TODO: Add docs.' + type: string + type: object + type: array + type: object + clusterRef: + description: "ObjectReference contains enough information to let you + inspect or modify the referred object. --- New uses of this type + are discouraged because of difficulty describing its usage when + embedded in APIs. 1. Ignored fields. It includes many fields which + are not generally honored. For instance, ResourceVersion and FieldPath + are both very rarely valid in actual usage. 2. Invalid usage help. + \ It is impossible to add specific help for individual usage. In + most embedded usages, there are particular restrictions like, \"must + refer only to types A and B\" or \"UID not honored\" or \"name must + be restricted\". Those cannot be well described when embedded. 3. + Inconsistent validation. Because the usages are different, the + validation rules are different by usage, which makes it hard for + users to predict what will happen. 4. The fields are both imprecise + and overly precise. Kind is not a precise mapping to a URL. This + can produce ambiguity during interpretation and require a REST mapping. + \ In most cases, the dependency is on the group,resource tuple and + the version of the actual struct is irrelevant. 5. We cannot easily + change it. Because this type is embedded in many locations, updates + to this type will affect numerous schemas. Don't make new APIs + embed an underspecified API type they do not control. \n Instead + of using this type, create a locally provided and used type that + is well-focused on your reference. For example, ServiceReferences + for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + ." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: Cluster is immutable + rule: self == oldSelf + configurationRef: + description: ConfigurationRef is a secret reference which should contain + service configuration. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + docsPath: + type: string + git: + properties: + folder: + type: string + ref: + type: string + required: + - folder + - ref + type: object + helm: + properties: + chart: + description: Selects a key from a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + repository: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + values: + description: Selects a key from a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + valuesFiles: + items: + type: string + type: array + version: + type: string + type: object + kustomize: + properties: + path: + type: string + required: + - path + type: object + protect: + type: boolean + repositoryRef: + description: "ObjectReference contains enough information to let you + inspect or modify the referred object. --- New uses of this type + are discouraged because of difficulty describing its usage when + embedded in APIs. 1. Ignored fields. It includes many fields which + are not generally honored. For instance, ResourceVersion and FieldPath + are both very rarely valid in actual usage. 2. Invalid usage help. + \ It is impossible to add specific help for individual usage. In + most embedded usages, there are particular restrictions like, \"must + refer only to types A and B\" or \"UID not honored\" or \"name must + be restricted\". Those cannot be well described when embedded. 3. + Inconsistent validation. Because the usages are different, the + validation rules are different by usage, which makes it hard for + users to predict what will happen. 4. The fields are both imprecise + and overly precise. Kind is not a precise mapping to a URL. This + can produce ambiguity during interpretation and require a REST mapping. + \ In most cases, the dependency is on the group,resource tuple and + the version of the actual struct is irrelevant. 5. We cannot easily + change it. Because this type is embedded in many locations, updates + to this type will affect numerous schemas. Don't make new APIs + embed an underspecified API type they do not control. \n Instead + of using this type, create a locally provided and used type that + is well-focused on your reference. For example, ServiceReferences + for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + ." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: Repository is immutable + rule: self == oldSelf + version: + default: '''0.0.1''' + type: string + required: + - clusterRef + - repositoryRef + - version + type: object + status: + properties: + components: + items: + properties: + group: + type: string + id: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + state: + description: State specifies the component state + enum: + - RUNNING + - PENDING + - FAILED + type: string + synced: + type: boolean + version: + type: string + required: + - id + - kind + - name + - synced + type: object + type: array + errors: + items: + properties: + message: + type: string + source: + type: string + required: + - message + - source + type: object + type: array + id: + description: Id of service in console. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} From d644bfaa371eb5b911366b9ca6241e67d7b16bd9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 7 Dec 2023 16:05:42 +0100 Subject: [PATCH 007/198] add more validations --- .../apis/deployments/v1alpha1/cluster.go | 24 ++++++++++--------- .../v1alpha1/zz_generated.deepcopy.go | 20 ---------------- .../bases/deployments.plural.sh_clusters.yaml | 19 ++++++++++++--- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 44a09a349..ee40b7e1b 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -22,10 +22,20 @@ type ClusterList struct { // +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Cluster health status" // +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console cluster ID" type Cluster struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ClusterSpec `json:"spec,omitempty"` - Status ClusterStatus `json:"status,omitempty"` + + // +kubebuilder:validation:XValidation:rule="self.cloud == 'aws' && (has(self.cloudSettings.aws)",message="AWS cloud settings are required" + // +kubebuilder:validation:XValidation:rule="self.cloud == 'azure' && (has(self.cloudSettings.azure)",message="Azure cloud settings are required" + // +kubebuilder:validation:XValidation:rule="self.cloud == 'gcp' && (has(self.cloudSettings.gcp)",message="GCP cloud settings are required" + // +kubebuilder:validation:XValidation:rule="self.cloud == 'aws' && (!has(self.cloudSettings.azure) && !has(self.cloudSettings.gcp)",message="Only AWS cloud settings can be specified" + // +kubebuilder:validation:XValidation:rule="self.cloud == 'azure' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.gcp)",message="Only Azure cloud settings can be specified" + // +kubebuilder:validation:XValidation:rule="self.cloud == 'gcp' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure)",message="Only GCP cloud settings can be specified" + // +kubebuilder:validation:XValidation:rule="self.cloud == 'byok' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure) && !has(self.cloudSettings.gcp))",message="Cloud settings can't be specified for BYOK" + Spec ClusterSpec `json:"spec,omitempty"` + + Status ClusterStatus `json:"status,omitempty"` } type ClusterSpec struct { @@ -79,10 +89,6 @@ type ClusterCloudSettings struct { // GCP cluster customizations. // +kubebuilder:validation:Optional GCP *ClusterGCPCloudSettings `json:"gcp,omitempty"` - - // BYOK cluster customizations. - // +kubebuilder:validation:Optional - BYOK *ClusterBYOKCloudSettings `json:"byok,omitempty"` } type ClusterAWSCloudSettings struct { @@ -109,10 +115,6 @@ type ClusterGCPCloudSettings struct { Project string `json:"project"` } -type ClusterBYOKCloudSettings struct { - // TODO: Decide how we handle BYOK settings and how we will deploy operator. -} - type ClusterNodePool struct { // Name of the node pool. Must be unique. Name string `json:"name"` diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 95eaec81c..31d85edde 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -142,21 +142,6 @@ func (in *ClusterAzureCloudSettings) DeepCopy() *ClusterAzureCloudSettings { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterBYOKCloudSettings) DeepCopyInto(out *ClusterBYOKCloudSettings) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterBYOKCloudSettings. -func (in *ClusterBYOKCloudSettings) DeepCopy() *ClusterBYOKCloudSettings { - if in == nil { - return nil - } - out := new(ClusterBYOKCloudSettings) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterCloudSettings) DeepCopyInto(out *ClusterCloudSettings) { *out = *in @@ -175,11 +160,6 @@ func (in *ClusterCloudSettings) DeepCopyInto(out *ClusterCloudSettings) { *out = new(ClusterGCPCloudSettings) **out = **in } - if in.BYOK != nil { - in, out := &in.BYOK, &out.BYOK - *out = new(ClusterBYOKCloudSettings) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCloudSettings. diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 503596473..4b7c0d123 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -125,9 +125,6 @@ spec: - resourceGroup - subscriptionId type: object - byok: - description: BYOK cluster customizations. - type: object gcp: description: GCP cluster customizations. properties: @@ -270,6 +267,22 @@ spec: - cloud - nodePools type: object + x-kubernetes-validations: + - message: AWS cloud settings are required + rule: self.cloud == 'aws' && (has(self.cloudSettings.aws) + - message: Azure cloud settings are required + rule: self.cloud == 'azure' && (has(self.cloudSettings.azure) + - message: GCP cloud settings are required + rule: self.cloud == 'gcp' && (has(self.cloudSettings.gcp) + - message: Only AWS cloud settings can be specified + rule: self.cloud == 'aws' && (!has(self.cloudSettings.azure) && !has(self.cloudSettings.gcp) + - message: Only Azure cloud settings can be specified + rule: self.cloud == 'azure' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.gcp) + - message: Only GCP cloud settings can be specified + rule: self.cloud == 'gcp' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure) + - message: Cloud settings can't be specified for BYOK + rule: self.cloud == 'byok' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure) + && !has(self.cloudSettings.gcp)) status: properties: currentVersion: From 2c58a792895833d0ab814dfe358a89c7b7074496 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Thu, 7 Dec 2023 17:12:03 +0100 Subject: [PATCH 008/198] add provider crd, reconciler and option to run only a subset of reconcilers --- .../apis/deployments/v1alpha1/provider.go | 90 ++++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 124 ++++++++++++++++ .../deployments.plural.sh_providers.yaml | 136 ++++++++++++++++++ .../config/crd/examples/provider_byok.yaml | 9 ++ controller/main.go | 62 ++++++-- .../pkg/provider_controller/controller.go | 34 +++++ controller/pkg/types/controller.go | 9 ++ controller/pkg/types/reconciler.go | 77 ++++++++++ 8 files changed, 528 insertions(+), 13 deletions(-) create mode 100644 controller/apis/deployments/v1alpha1/provider.go create mode 100644 controller/config/crd/bases/deployments.plural.sh_providers.yaml create mode 100644 controller/config/crd/examples/provider_byok.yaml create mode 100644 controller/pkg/provider_controller/controller.go create mode 100644 controller/pkg/types/controller.go create mode 100644 controller/pkg/types/reconciler.go diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go new file mode 100644 index 000000000..2eca2c9c3 --- /dev/null +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -0,0 +1,90 @@ +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + SchemeBuilder.Register(&Provider{}, &ProviderList{}) +} + +type CloudProvider string + +const ( + AWS CloudProvider = "aws" + Azure CloudProvider = "azure" + BYOK CloudProvider = "byok" + GCP CloudProvider = "gcp" +) + +// CloudProviderSettings ... +type CloudProviderSettings struct { + // +kubebuilder:validation:Optional + AWS *v1.SecretReference `json:"aws,omitempty"` + // +kubebuilder:validation:Optional + Azure *v1.SecretReference `json:"azure,omitempty"` + // +kubebuilder:validation:Optional + GCP *v1.SecretReference `json:"gcp,omitempty"` +} + +// Provider ... +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="ID of the provider in the Console API." +// +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".spec.name",description="Human-readable name of the Provider." +// +kubebuilder:printcolumn:name="Cloud",type="string",JSONPath=".spec.cloud",description="Name of the Provider cloud service." +type Provider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec ProviderSpec `json:"spec"` + // +kubebuilder:validation:Optional + Status ProviderStatus `json:"status,omitempty"` +} + +// ProviderList ... +// +kubebuilder:object:root=true +type ProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Provider `json:"items"` +} + +// ProviderSpec ... +// +kubebuilder:validation:Validation:rule="(self.cloud == 'byok' && !has(self.cloudSettings)) || (self.cloud != 'byok' && has(self.cloudSettings))",message="Cloud Settings must be provided only when Cloud is not set to BYOK." +type ProviderSpec struct { + // Cloud is the name of the cloud service for the Provider. + // One of (CloudProvider): [byok, gcp, aws, azure] + // +kubebuilder:example:=byok + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string + // +kubebuilder:validation:Enum:=byok;gcp;aws;azure + // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Cloud is immutable" + Cloud CloudProvider `json:"cloud"` + // CloudSettings reference cloud provider credentials secrets used for provisioning the Cluster. + // Not required when Cloud is set to CloudProvider(BYOK). + // +kubebuilder:validation:Optional + // +structType=atomic + CloudSettings *CloudProviderSettings `json:"cloudSettings,omitempty"` + // Name is a human-readable name of the Provider. + // +kubebuilder:example:=gcp-provider + // +kubebuilder:validation:Required + // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Name is immutable" + Name string `json:"name"` + // Namespace is the namespace ClusterAPI resources are deployed into. + // +kubebuilder:example:=capi-gcp + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Namespace is immutable" + Namespace string `json:"namespace,omitempty"` +} + +// ProviderStatus ... +type ProviderStatus struct { + // ID of the provider in the Console API. + // +kubebuilder:validation:Optional + ID string `json:"id,omitempty"` +} diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 95eaec81c..8ae25d857 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -85,6 +85,36 @@ func (in *Bindings) DeepCopy() *Bindings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProviderSettings) DeepCopyInto(out *CloudProviderSettings) { + *out = *in + if in.AWS != nil { + in, out := &in.AWS, &out.AWS + *out = new(v1.SecretReference) + **out = **in + } + if in.Azure != nil { + in, out := &in.Azure, &out.Azure + *out = new(v1.SecretReference) + **out = **in + } + if in.GCP != nil { + in, out := &in.GCP, &out.GCP + *out = new(v1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProviderSettings. +func (in *CloudProviderSettings) DeepCopy() *CloudProviderSettings { + if in == nil { + return nil + } + out := new(CloudProviderSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in @@ -505,6 +535,100 @@ func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Provider) DeepCopyInto(out *Provider) { + *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 Provider. +func (in *Provider) DeepCopy() *Provider { + if in == nil { + return nil + } + out := new(Provider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Provider) 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 *ProviderList) DeepCopyInto(out *ProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Provider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderList. +func (in *ProviderList) DeepCopy() *ProviderList { + if in == nil { + return nil + } + out := new(ProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderList) 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 *ProviderSpec) DeepCopyInto(out *ProviderSpec) { + *out = *in + if in.CloudSettings != nil { + in, out := &in.CloudSettings, &out.CloudSettings + *out = new(CloudProviderSettings) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec. +func (in *ProviderSpec) DeepCopy() *ProviderSpec { + if in == nil { + return nil + } + out := new(ProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. +func (in *ProviderStatus) DeepCopy() *ProviderStatus { + if in == nil { + return nil + } + out := new(ProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Taint) DeepCopyInto(out *Taint) { *out = *in diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml new file mode 100644 index 000000000..57184aa24 --- /dev/null +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: providers.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: Provider + listKind: ProviderList + plural: providers + singular: provider + scope: Cluster + versions: + - additionalPrinterColumns: + - description: ID of the provider in the Console API. + jsonPath: .status.id + name: ID + type: string + - description: Human-readable name of the Provider. + jsonPath: .spec.name + name: Name + type: string + - description: Name of the Provider cloud service. + jsonPath: .spec.cloud + name: Cloud + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Provider ... + 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: ProviderSpec ... + properties: + cloud: + description: 'Cloud is the name of the cloud service for the Provider. + One of (CloudProvider): [byok, gcp, aws, azure]' + enum: + - byok + - gcp + - aws + - azure + example: byok + type: string + cloudSettings: + description: CloudSettings reference cloud provider credentials secrets + used for provisioning the Cluster. Not required when Cloud is set + to CloudProvider(BYOK). + properties: + aws: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + azure: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + gcp: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-map-type: atomic + name: + description: Name is a human-readable name of the Provider. + example: gcp-provider + type: string + namespace: + description: Namespace is the namespace ClusterAPI resources are deployed + into. + example: capi-gcp + type: string + required: + - cloud + - name + type: object + status: + description: ProviderStatus ... + properties: + id: + description: ID of the provider in the Console API. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controller/config/crd/examples/provider_byok.yaml b/controller/config/crd/examples/provider_byok.yaml new file mode 100644 index 000000000..d69df86a7 --- /dev/null +++ b/controller/config/crd/examples/provider_byok.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: byok + namespace: byok +spec: + cloud: byok + name: byok diff --git a/controller/main.go b/controller/main.go index 64b20bdd2..5673c6053 100644 --- a/controller/main.go +++ b/controller/main.go @@ -2,11 +2,14 @@ package main import ( "flag" + "fmt" + "github.com/pluralsh/console/controller/pkg/types" + "go.uber.org/zap" "os" + "strings" deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/client" - gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" "github.com/pluralsh/console/controller/pkg/log" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -14,7 +17,7 @@ import ( "k8s.io/klog" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/log/zap" + ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" ) var ( @@ -33,13 +36,16 @@ type controllerRunOptions struct { probeAddr string consoleUrl string consoleToken string + reconcilers types.ReconcilerList } func main() { klog.InitFlags(nil) - opt := &controllerRunOptions{} - opts := zap.Options{ + opt := &controllerRunOptions{ + reconcilers: types.Reconcilers(), + } + opts := ctrlruntimezap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) @@ -50,10 +56,14 @@ func main() { "Enabling this will ensure there is only one active controller manager.") flag.StringVar(&opt.consoleUrl, "console-url", "", "the url of the console api to fetch services from") flag.StringVar(&opt.consoleToken, "console-token", "", "the deploy token to auth to console api with") + flag.Func("reconcilers", "Comma delimited list of reconciler names. Available reconcilers: gitrepository,cluster,provider,servicedeployment", func(reconcilersStr string) (err error) { + opt.reconcilers, err = parseReconcilers(reconcilersStr) + return err + }) flag.Parse() - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + ctrl.SetLogger(ctrlruntimezap.New(ctrlruntimezap.UseFlagOptions(&opts))) if opt.consoleToken == "" { opt.consoleToken = os.Getenv("CONSOLE_TOKEN") @@ -75,20 +85,46 @@ func main() { } consoleClient := client.New(opt.consoleUrl, opt.consoleToken) - - if err = (&gitrepositorycontroller.Reconciler{ - Client: mgr.GetClient(), - Log: setupLog.Named("gitrepository-operator"), - ConsoleClient: consoleClient, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "gitrepository") + controllers, err := opt.reconcilers.ToControllers(mgr, setupLog, consoleClient) + if err != nil { + setupLog.Error(err) os.Exit(1) } + runOrDie(controllers, mgr, setupLog) +} + +func parseReconcilers(reconcilersStr string) (types.ReconcilerList, error) { + split := strings.Split(reconcilersStr, ",") + if len(reconcilersStr) == 0 || len(split) == 0 { + return nil, fmt.Errorf("reconcilers arg cannot be empty") + } + + result := make(types.ReconcilerList, len(split)) + for i, r := range split { + reconciler, err := types.ToReconciler(r) + if err != nil { + return nil, err + } + + result[i] = reconciler + } + + return result, nil +} + +func runOrDie(controllers []types.Controller, mgr ctrl.Manager, logger *zap.SugaredLogger) { + for _, c := range controllers { + if err := c.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to create controller") + os.Exit(1) + } + } + ctx := ctrl.SetupSignalHandler() setupLog.Info("starting manager") if err := mgr.Start(ctx); err != nil { - setupLog.Error(err, "problem running manager") + setupLog.Error(err, "error running manager") os.Exit(1) } } diff --git a/controller/pkg/provider_controller/controller.go b/controller/pkg/provider_controller/controller.go new file mode 100644 index 000000000..a288ee191 --- /dev/null +++ b/controller/pkg/provider_controller/controller.go @@ -0,0 +1,34 @@ +package providercontroller + +import ( + "context" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// Reconciler reconciles a v1alpha1.Provider object. +// Implements reconcile.Reconciler and types.Controller +type Reconciler struct { + client.Client + + ConsoleClient consoleclient.ConsoleClient + Log *zap.SugaredLogger + Scheme *runtime.Scheme +} + +func (p *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + //TODO implement me + panic("implement me") +} + +func (p *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + p.Log.Infow("starting reconciler", "reconciler", "provider_reconciler") + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Provider{}). + Complete(p) +} diff --git a/controller/pkg/types/controller.go b/controller/pkg/types/controller.go new file mode 100644 index 000000000..32e3b0c7f --- /dev/null +++ b/controller/pkg/types/controller.go @@ -0,0 +1,9 @@ +package types + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +type Controller interface { + SetupWithManager(manager ctrl.Manager) error +} diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go new file mode 100644 index 000000000..13d76ca84 --- /dev/null +++ b/controller/pkg/types/reconciler.go @@ -0,0 +1,77 @@ +package types + +import ( + "fmt" + "github.com/pluralsh/console/controller/pkg/client" + gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" + providercontroller "github.com/pluralsh/console/controller/pkg/provider_controller" + "go.uber.org/zap" + ctrl "sigs.k8s.io/controller-runtime" +) + +type Reconciler string + +const ( + GitRepositoryReconciler Reconciler = "gitrepository" + ServiceDeploymentReconciler Reconciler = "servicedeployment" + ClusterReconciler Reconciler = "cluster" + ProviderReconciler Reconciler = "provider" +) + +func ToReconciler(reconciler string) (Reconciler, error) { + switch Reconciler(reconciler) { + case GitRepositoryReconciler: + case ServiceDeploymentReconciler: + case ClusterReconciler: + case ProviderReconciler: + return Reconciler(reconciler), nil + } + + return "", fmt.Errorf("reconciler %q is not supported", reconciler) +} + +func (sc Reconciler) ToController(mgr ctrl.Manager, logger *zap.SugaredLogger, consoleClient client.ConsoleClient) (Controller, error) { + unsupported := fmt.Errorf("reconciler %q is not supported", sc) + + switch sc { + case GitRepositoryReconciler: + return &gitrepositorycontroller.Reconciler{ + Client: mgr.GetClient(), + Log: logger, + ConsoleClient: consoleClient, + }, nil + case ServiceDeploymentReconciler: + return nil, unsupported + case ClusterReconciler: + return nil, unsupported + case ProviderReconciler: + return &providercontroller.Reconciler{ + Client: mgr.GetClient(), + ConsoleClient: consoleClient, + Log: logger, + Scheme: mgr.GetScheme(), + }, nil + default: + return nil, unsupported + } +} + +type ReconcilerList []Reconciler + +func Reconcilers() ReconcilerList { + return []Reconciler{GitRepositoryReconciler, ProviderReconciler} +} + +func (rl ReconcilerList) ToControllers(mgr ctrl.Manager, logger *zap.SugaredLogger, consoleClient client.ConsoleClient) ([]Controller, error) { + result := make([]Controller, len(rl)) + for i, r := range rl { + controller, err := r.ToController(mgr, logger, consoleClient) + if err != nil { + return nil, err + } + + result[i] = controller + } + + return result, nil +} From f0e20aad47dbac21d3f42f7d512b1f2b298a8f2e Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Thu, 7 Dec 2023 17:15:49 +0100 Subject: [PATCH 009/198] regenerate code --- .../v1alpha1/zz_generated.deepcopy.go | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 3e225c6bb..87d9d0d64 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -85,6 +85,36 @@ func (in *Bindings) DeepCopy() *Bindings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProviderSettings) DeepCopyInto(out *CloudProviderSettings) { + *out = *in + if in.AWS != nil { + in, out := &in.AWS, &out.AWS + *out = new(v1.SecretReference) + **out = **in + } + if in.Azure != nil { + in, out := &in.Azure, &out.Azure + *out = new(v1.SecretReference) + **out = **in + } + if in.GCP != nil { + in, out := &in.GCP, &out.GCP + *out = new(v1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProviderSettings. +func (in *CloudProviderSettings) DeepCopy() *CloudProviderSettings { + if in == nil { + return nil + } + out := new(CloudProviderSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in @@ -500,6 +530,100 @@ func (in *NamespacedName) DeepCopy() *NamespacedName { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Provider) DeepCopyInto(out *Provider) { + *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 Provider. +func (in *Provider) DeepCopy() *Provider { + if in == nil { + return nil + } + out := new(Provider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Provider) 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 *ProviderList) DeepCopyInto(out *ProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Provider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderList. +func (in *ProviderList) DeepCopy() *ProviderList { + if in == nil { + return nil + } + out := new(ProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderList) 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 *ProviderSpec) DeepCopyInto(out *ProviderSpec) { + *out = *in + if in.CloudSettings != nil { + in, out := &in.CloudSettings, &out.CloudSettings + *out = new(CloudProviderSettings) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec. +func (in *ProviderSpec) DeepCopy() *ProviderSpec { + if in == nil { + return nil + } + out := new(ProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. +func (in *ProviderStatus) DeepCopy() *ProviderStatus { + if in == nil { + return nil + } + out := new(ProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Service) DeepCopyInto(out *Service) { *out = *in From 3f1c79dc5183b7347933ff15835a2c2c26e90575 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Thu, 7 Dec 2023 17:28:39 +0100 Subject: [PATCH 010/198] add some docs --- controller/pkg/provider_controller/controller.go | 3 +++ controller/pkg/types/controller.go | 2 ++ controller/pkg/types/reconciler.go | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/controller/pkg/provider_controller/controller.go b/controller/pkg/provider_controller/controller.go index a288ee191..9ea8c6e06 100644 --- a/controller/pkg/provider_controller/controller.go +++ b/controller/pkg/provider_controller/controller.go @@ -21,11 +21,14 @@ type Reconciler struct { Scheme *runtime.Scheme } +// Reconcile ... +// TODO: Add kubebuilder rbac annotation func (p *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { //TODO implement me panic("implement me") } +// SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. func (p *Reconciler) SetupWithManager(mgr ctrl.Manager) error { p.Log.Infow("starting reconciler", "reconciler", "provider_reconciler") return ctrl.NewControllerManagedBy(mgr). diff --git a/controller/pkg/types/controller.go b/controller/pkg/types/controller.go index 32e3b0c7f..bb13bf99d 100644 --- a/controller/pkg/types/controller.go +++ b/controller/pkg/types/controller.go @@ -4,6 +4,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) +// Controller is a simple interface that unifies the way of +// initializing reconcilers with ctrl.Manager. type Controller interface { SetupWithManager(manager ctrl.Manager) error } diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 13d76ca84..2b066307d 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -9,6 +9,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) +// Reconciler is a name of reconciler supported by this controller. type Reconciler string const ( @@ -18,6 +19,7 @@ const ( ProviderReconciler Reconciler = "provider" ) +// ToReconciler maps reconciler string to a Reconciler type. func ToReconciler(reconciler string) (Reconciler, error) { switch Reconciler(reconciler) { case GitRepositoryReconciler: @@ -30,6 +32,7 @@ func ToReconciler(reconciler string) (Reconciler, error) { return "", fmt.Errorf("reconciler %q is not supported", reconciler) } +// ToController creates Controller instance based on this Reconciler. func (sc Reconciler) ToController(mgr ctrl.Manager, logger *zap.SugaredLogger, consoleClient client.ConsoleClient) (Controller, error) { unsupported := fmt.Errorf("reconciler %q is not supported", sc) @@ -56,12 +59,17 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, logger *zap.SugaredLogger, c } } +// ReconcilerList is a wrapper around Reconciler array type to allow +// defining custom functions. type ReconcilerList []Reconciler +// Reconcilers defines a list of reconcilers that will be started by default +// if '--reconcilers=...' flag is not provided. func Reconcilers() ReconcilerList { return []Reconciler{GitRepositoryReconciler, ProviderReconciler} } +// ToControllers returns a list of Controller instances based on this Reconciler array. func (rl ReconcilerList) ToControllers(mgr ctrl.Manager, logger *zap.SugaredLogger, consoleClient client.ConsoleClient) ([]Controller, error) { result := make([]Controller, len(rl)) for i, r := range rl { From aa8fee1577fa3cbb58ab7536653f8335c61911dc Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 09:07:33 +0100 Subject: [PATCH 011/198] init service controller --- .../deployments/v1alpha1/git_repository.go | 1 + .../apis/deployments/v1alpha1/service.go | 1 + ...deployments.plural.sh_gitrepositories.yaml | 3 + .../gitrepository_controller/controller.go | 25 +----- controller/pkg/kubernetes/helper.go | 26 ++++++ .../pkg/service_controller/controller.go | 82 +++++++++++++++++++ 6 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 controller/pkg/service_controller/controller.go diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index e8cef133c..7d66af5a8 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -10,6 +10,7 @@ func init() { } type GitRepositorySpec struct { + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Url is immutable" Url string `json:"url"` // CredentialsRef is a secret reference which should contain privateKey, passphrase, username and password. diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index b94f7d8b5..a061317e1 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -101,6 +101,7 @@ type Service struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + // +kubebuilder:validation:Required Spec ServiceSpec `json:"spec,omitempty"` Status ServiceStatus `json:"status,omitempty"` } diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index 313a4e214..ac0efe9b9 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -58,6 +58,9 @@ spec: x-kubernetes-map-type: atomic url: type: string + x-kubernetes-validations: + - message: Url is immutable + rule: self == oldSelf required: - url type: object diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index c8e76185a..38aded465 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -114,7 +114,7 @@ func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitReposit if secret.Name != "" { if controllerutil.ContainsFinalizer(secret, RepoFinalizer) { r.Log.Info("delete credential secret") - err := r.deleteSecret(ctx, repo.Spec.CredentialsRef.Namespace, repo.Spec.CredentialsRef.Name) + err := kubernetes.DeleteSecret(ctx, r.Client, repo.Spec.CredentialsRef.Namespace, repo.Spec.CredentialsRef.Name) if err != nil { return ctrl.Result{}, err } @@ -193,29 +193,6 @@ func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, return existingRepos.GitRepository, nil } -func (r *Reconciler) deleteSecret(ctx context.Context, secretNamespace, secretName string) error { - if secretName == "" { - return nil - } - - secret := &corev1.Secret{} - name := types.NamespacedName{Name: secretName, Namespace: secretNamespace} - err := r.Get(ctx, name, secret) - if apierrors.IsNotFound(err) { - return nil - } - if err != nil { - return fmt.Errorf("failed to get Secret %q: %w", name.String(), err) - } - - if err := r.Delete(ctx, secret); err != nil { - return fmt.Errorf("failed to delete Secret %q: %w", name.String(), err) - } - - // We successfully deleted the secret - return nil -} - type RepoPatchFunc func(repo *v1alpha1.GitRepository) func UpdateReposStatus(ctx context.Context, client ctrlruntimeclient.Client, bootstrap *v1alpha1.GitRepository, patch RepoPatchFunc) error { diff --git a/controller/pkg/kubernetes/helper.go b/controller/pkg/kubernetes/helper.go index c83327ec7..2cea3edbc 100644 --- a/controller/pkg/kubernetes/helper.go +++ b/controller/pkg/kubernetes/helper.go @@ -4,10 +4,13 @@ import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -100,3 +103,26 @@ func TryAddFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj c return nil } + +func DeleteSecret(ctx context.Context, client client.Client, secretNamespace, secretName string) error { + if secretName == "" { + return nil + } + + secret := &corev1.Secret{} + name := types.NamespacedName{Name: secretName, Namespace: secretNamespace} + err := client.Get(ctx, name, secret) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return fmt.Errorf("failed to get Secret %q: %w", name.String(), err) + } + + if err := client.Delete(ctx, secret); err != nil { + return fmt.Errorf("failed to delete Secret %q: %w", name.String(), err) + } + + // We successfully deleted the secret + return nil +} diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go new file mode 100644 index 000000000..cfc219ded --- /dev/null +++ b/controller/pkg/service_controller/controller.go @@ -0,0 +1,82 @@ +package servicecontroller + +import ( + "context" + "reflect" + "time" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + + "go.uber.org/zap" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// Reconciler reconciles a Service object +type Reconciler struct { + client.Client + ConsoleClient consoleclient.ConsoleClient + Log *zap.SugaredLogger +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + service := &v1alpha1.Service{} + if err := r.Get(ctx, req.NamespacedName, service); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + if !service.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, service) + } + + return ctrl.Result{ + // update status + RequeueAfter: 30 * time.Second, + }, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Service{}). + Complete(r) +} + +func (r *Reconciler) handleDelete(ctx context.Context, service *v1alpha1.Service) (ctrl.Result, error) { + if controllerutil.ContainsFinalizer(service, "") { + + } + return ctrl.Result{}, nil +} + +type RepoPatchFunc func(service *v1alpha1.Service) + +func UpdateServiceStatus(ctx context.Context, client ctrlruntimeclient.Client, service *v1alpha1.Service, patch RepoPatchFunc) error { + key := ctrlruntimeclient.ObjectKeyFromObject(service) + + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + // fetch the current state of the cluster + if err := client.Get(ctx, key, service); err != nil { + return err + } + + // modify it + original := service.DeepCopy() + patch(service) + + // save some work + if reflect.DeepEqual(original.Status, service.Status) { + return nil + } + + // update the status + return client.Status().Patch(ctx, service, ctrlruntimeclient.MergeFrom(original)) + }) +} From 8df85eb3e6fd1fc1e96ea47274b78c4d9dbdf2fa Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 09:35:59 +0100 Subject: [PATCH 012/198] add cluster examples --- .../apis/deployments/v1alpha1/cluster.go | 2 +- .../v1alpha1/zz_generated.deepcopy.go | 6 ++- .../config/crd/examples/cluster_aws.yaml | 41 +++++++++++++++++++ .../config/crd/examples/cluster_byok.yaml | 11 +++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 controller/config/crd/examples/cluster_aws.yaml create mode 100644 controller/config/crd/examples/cluster_byok.yaml diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index ee40b7e1b..e94d40830 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -50,7 +50,7 @@ type ClusterSpec struct { // ProviderRef references provider to use for this cluster. Can be skipped only for BYOK. // +kubebuilder:validation:Optional - ProviderRef corev1.ObjectReference `json:"providerRef,omitempty"` + ProviderRef *corev1.ObjectReference `json:"providerRef,omitempty"` // Cloud provider to use for this cluster. // +kubebuilder:validation:Enum=aws;azure;gcp;byok diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 87d9d0d64..245f5b6e7 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -334,7 +334,11 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = new(string) **out = **in } - out.ProviderRef = in.ProviderRef + if in.ProviderRef != nil { + in, out := &in.ProviderRef, &out.ProviderRef + *out = new(v1.ObjectReference) + **out = **in + } if in.Protect != nil { in, out := &in.Protect, &out.Protect *out = new(bool) diff --git a/controller/config/crd/examples/cluster_aws.yaml b/controller/config/crd/examples/cluster_aws.yaml new file mode 100644 index 000000000..150eeffa5 --- /dev/null +++ b/controller/config/crd/examples/cluster_aws.yaml @@ -0,0 +1,41 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: aws + namespace: default +spec: + handle: aws + cloud: aws + version: 1.24 + protect: false +# providerRef: +# ... + cloudSettings: + aws: + region: us-east-1 + nodePools: + - name: pool1 + instanceType: t5.large + minSize: 1 + maxSize: 5 + - name: pool2 + instanceType: t5.large + minSize: 1 + maxSize: 3 + labels: + key1: value1 + key2: value2 + key3: value3 + taints: + - key: key + value: value + effect: NoSchedule + - name: pool3 + instanceType: t5.large + minSize: 1 + maxSize: 3 + cloudSettings: + aws: + launchTemplateId: test + tags: + managed-by: plural-controller diff --git a/controller/config/crd/examples/cluster_byok.yaml b/controller/config/crd/examples/cluster_byok.yaml new file mode 100644 index 000000000..ded3fe292 --- /dev/null +++ b/controller/config/crd/examples/cluster_byok.yaml @@ -0,0 +1,11 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: byok + namespace: default +spec: + handle: byok + cloud: byok + protect: false + tags: + managed-by: plural-controller From 8c0747f1f93fa6c441d10501b146e563ce141498 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 09:47:06 +0100 Subject: [PATCH 013/198] fetch cluster --- .../apis/deployments/v1alpha1/service.go | 2 ++ controller/pkg/client/console.go | 2 +- controller/pkg/client/service.go | 4 ++-- .../pkg/service_controller/controller.go | 23 ++++++++++++++----- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index a061317e1..04f735bb5 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -49,8 +49,10 @@ type ServiceSpec struct { // +optional Helm *ServiceHelm `json:"helm,omitempty"` + // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Repository is immutable" RepositoryRef corev1.ObjectReference `json:"repositoryRef"` + // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Cluster is immutable" ClusterRef corev1.ObjectReference `json:"clusterRef"` // ConfigurationRef is a secret reference which should contain service configuration. diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index c2ddd5a13..dfd7d2c49 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -24,7 +24,7 @@ type client struct { type ConsoleClient interface { GetServices() ([]*console.ServiceDeploymentBaseFragment, error) - GetService(id string) (*console.ServiceDeploymentExtended, error) + GetService(clusterID, serviceName string) (*console.ServiceDeploymentExtended, error) UpdateComponents(id string, components []*console.ComponentAttributes, errs []*console.ServiceErrorAttributes) error CreateRepository(url string, privateKey, passphrase, username, password *string) (*console.CreateGitRepository, error) ListRepositories() (*console.ListGitRepositories, error) diff --git a/controller/pkg/client/service.go b/controller/pkg/client/service.go index a122d506c..ef7f82760 100644 --- a/controller/pkg/client/service.go +++ b/controller/pkg/client/service.go @@ -13,8 +13,8 @@ func (c *client) GetServices() ([]*console.ServiceDeploymentBaseFragment, error) return resp.ClusterServices, nil } -func (c *client) GetService(id string) (*console.ServiceDeploymentExtended, error) { - resp, err := c.consoleClient.GetServiceDeployment(c.ctx, id) +func (c *client) GetService(clusterID, serviceName string) (*console.ServiceDeploymentExtended, error) { + resp, err := c.consoleClient.GetServiceDeploymentByHandle(c.ctx, clusterID, serviceName) if err != nil { return nil, err } diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index cfc219ded..d8e8642c0 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -7,9 +7,8 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" - + "github.com/pluralsh/console/controller/pkg/errors" "go.uber.org/zap" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,15 +26,27 @@ type Reconciler struct { func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { service := &v1alpha1.Service{} if err := r.Get(ctx, req.NamespacedName, service); err != nil { - if apierrors.IsNotFound(err) { - return ctrl.Result{}, nil - } - return ctrl.Result{}, err + return ctrl.Result{}, client.IgnoreNotFound(err) } if !service.GetDeletionTimestamp().IsZero() { return r.handleDelete(ctx, service) } + cluster := &v1alpha1.Cluster{} + if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { + return ctrl.Result{}, err + } + + existingService, err := r.ConsoleClient.GetService(*cluster.Status.Id, service.Name) + if err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + if existingService == nil { + + } + return ctrl.Result{ // update status RequeueAfter: 30 * time.Second, From 1746d09216ead4a691cce0f0df17910126192465 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 09:49:44 +0100 Subject: [PATCH 014/198] set namespace scope for Cluster --- controller/apis/deployments/v1alpha1/cluster.go | 2 +- controller/config/crd/bases/deployments.plural.sh_clusters.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index e94d40830..74ec6ca43 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -17,7 +17,7 @@ type ClusterList struct { } // +kubebuilder:object:root=true -// +kubebuilder:resource:scope=Cluster +// +kubebuilder:resource:scope=Namespaced // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Cluster health status" // +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console cluster ID" diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 4b7c0d123..fbad20419 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -13,7 +13,7 @@ spec: listKind: ClusterList plural: clusters singular: cluster - scope: Cluster + scope: Namespaced versions: - additionalPrinterColumns: - description: Cluster health status From f31670df4b6d5440838657d3ef0376b7f2028c51 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 12:11:44 +0100 Subject: [PATCH 015/198] update service model and controller --- .../deployments/v1alpha1/git_repository.go | 10 ++++++- .../apis/deployments/v1alpha1/service.go | 4 +++ .../v1alpha1/zz_generated.deepcopy.go | 5 ---- ...deployments.plural.sh_gitrepositories.yaml | 3 ++ .../gitrepository_controller/controller.go | 28 +++++++++---------- .../pkg/service_controller/controller.go | 26 +++++++++++++++++ controller/pkg/types/reconciler.go | 10 +++++-- 7 files changed, 64 insertions(+), 22 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 7d66af5a8..5e4ad2e8b 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -18,10 +18,18 @@ type GitRepositorySpec struct { CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"` } +type GitHealth string + +const ( + GitHealthPullable GitHealth = "PULLABLE" + GitHealthFailed GitHealth = "FAILED" +) + type GitRepositoryStatus struct { // Health status. // +optional - Health *string `json:"health,omitempty"` + // +kubebuilder:validation:Enum:=PULLABLE;FAILED + Health GitHealth `json:"health,omitempty"` // Message indicating details about last transition. // +optional Message *string `json:"message,omitempty"` diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index 04f735bb5..be50487cf 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -5,6 +5,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func init() { + SchemeBuilder.Register(&Service{}, &ServiceList{}) +} + type ComponentState string const ( diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 245f5b6e7..91b6aa670 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -492,11 +492,6 @@ func (in *GitRepositorySpec) DeepCopy() *GitRepositorySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitRepositoryStatus) DeepCopyInto(out *GitRepositoryStatus) { *out = *in - if in.Health != nil { - in, out := &in.Health, &out.Health - *out = new(string) - **out = **in - } if in.Message != nil { in, out := &in.Message, &out.Message *out = new(string) diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index ac0efe9b9..df8ac63ac 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -68,6 +68,9 @@ spec: properties: health: description: Health status. + enum: + - PULLABLE + - FAILED type: string id: description: Id of repo in console. diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 38aded465..19e9f9257 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -56,13 +56,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return r.handleDelete(ctx, repo) } - existingRepos, err := r.getRepository(repo.Spec.Url) + existingRepo, err := r.getRepository(repo.Spec.Url) if err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, err } } - if existingRepos == nil { + if existingRepo == nil { cred, err := r.getRepositoryCredentials(ctx, repo) if err != nil { return ctrl.Result{}, err @@ -71,17 +71,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - existingRepos = resp.CreateGitRepository + existingRepo = resp.CreateGitRepository } if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { return ctrl.Result{}, err } if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { - r.Status.Message = existingRepos.Error - r.Status.Id = &existingRepos.ID - if existingRepos.Health != nil { - r.Status.Health = (*string)(existingRepos.Health) + r.Status.Message = existingRepo.Error + r.Status.Id = &existingRepo.ID + if existingRepo.Health != nil { + r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } }); err != nil { @@ -195,25 +195,25 @@ func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, type RepoPatchFunc func(repo *v1alpha1.GitRepository) -func UpdateReposStatus(ctx context.Context, client ctrlruntimeclient.Client, bootstrap *v1alpha1.GitRepository, patch RepoPatchFunc) error { - key := ctrlruntimeclient.ObjectKeyFromObject(bootstrap) +func UpdateReposStatus(ctx context.Context, client ctrlruntimeclient.Client, repository *v1alpha1.GitRepository, patch RepoPatchFunc) error { + key := ctrlruntimeclient.ObjectKeyFromObject(repository) return retry.RetryOnConflict(retry.DefaultRetry, func() error { // fetch the current state of the cluster - if err := client.Get(ctx, key, bootstrap); err != nil { + if err := client.Get(ctx, key, repository); err != nil { return err } // modify it - original := bootstrap.DeepCopy() - patch(bootstrap) + original := repository.DeepCopy() + patch(repository) // save some work - if reflect.DeepEqual(original.Status, bootstrap.Status) { + if reflect.DeepEqual(original.Status, repository.Status) { return nil } // update the status - return client.Status().Patch(ctx, bootstrap, ctrlruntimeclient.MergeFrom(original)) + return client.Status().Patch(ctx, repository, ctrlruntimeclient.MergeFrom(original)) }) } diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index d8e8642c0..b1f684b20 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -36,6 +36,32 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { return ctrl.Result{}, err } + if cluster.Status.Id == nil { + r.Log.Info("Cluster is not ready", service.Spec.ClusterRef.Name) + return ctrl.Result{ + // update status + RequeueAfter: 30 * time.Second, + }, nil + } + + repository := &v1alpha1.GitRepository{} + if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.RepositoryRef.Name, Namespace: service.Spec.RepositoryRef.Namespace}, repository); err != nil { + return ctrl.Result{}, err + } + if repository.Status.Id == nil { + r.Log.Info("Repository is not ready", service.Spec.RepositoryRef.Name) + return ctrl.Result{ + // update status + RequeueAfter: 30 * time.Second, + }, nil + } + if repository.Status.Health == v1alpha1.GitHealthFailed { + r.Log.Info("Repository is not healthy", service.Spec.RepositoryRef.Name) + return ctrl.Result{ + // update status + RequeueAfter: 30 * time.Second, + }, nil + } existingService, err := r.ConsoleClient.GetService(*cluster.Status.Id, service.Name) if err != nil { diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 2b066307d..436711c8d 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -2,9 +2,11 @@ package types import ( "fmt" + "github.com/pluralsh/console/controller/pkg/client" gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" providercontroller "github.com/pluralsh/console/controller/pkg/provider_controller" + servicecontroller "github.com/pluralsh/console/controller/pkg/service_controller" "go.uber.org/zap" ctrl "sigs.k8s.io/controller-runtime" ) @@ -44,7 +46,11 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, logger *zap.SugaredLogger, c ConsoleClient: consoleClient, }, nil case ServiceDeploymentReconciler: - return nil, unsupported + return &servicecontroller.Reconciler{ + Client: mgr.GetClient(), + Log: logger, + ConsoleClient: consoleClient, + }, nil case ClusterReconciler: return nil, unsupported case ProviderReconciler: @@ -66,7 +72,7 @@ type ReconcilerList []Reconciler // Reconcilers defines a list of reconcilers that will be started by default // if '--reconcilers=...' flag is not provided. func Reconcilers() ReconcilerList { - return []Reconciler{GitRepositoryReconciler, ProviderReconciler} + return []Reconciler{GitRepositoryReconciler, ProviderReconciler, ServiceDeploymentReconciler} } // ToControllers returns a list of Controller instances based on this Reconciler array. From 5fe428b16b63d114ac33583922796fa4c15789a0 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 12:29:14 +0100 Subject: [PATCH 016/198] add create service client method --- controller/pkg/client/console.go | 1 + controller/pkg/client/service.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index dfd7d2c49..b6237eb46 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -31,6 +31,7 @@ type ConsoleClient interface { UpdateRepository(id string, attrs console.GitAttributes) (*console.UpdateGitRepository, error) DeleteRepository(id string) error GetRepository(url *string) (*console.GetGitRepository, error) + CreateService(clusterId *string, attributes console.ServiceDeploymentAttributes) (*console.ServiceDeploymentFragment, error) } func New(url, token string) ConsoleClient { diff --git a/controller/pkg/client/service.go b/controller/pkg/client/service.go index ef7f82760..0dbf42ae3 100644 --- a/controller/pkg/client/service.go +++ b/controller/pkg/client/service.go @@ -1,6 +1,8 @@ package client import ( + "fmt" + console "github.com/pluralsh/console-client-go" ) @@ -22,6 +24,22 @@ func (c *client) GetService(clusterID, serviceName string) (*console.ServiceDepl return resp.ServiceDeployment, nil } +func (c *client) CreateService(clusterId *string, attributes console.ServiceDeploymentAttributes) (*console.ServiceDeploymentFragment, error) { + if clusterId == nil { + return nil, fmt.Errorf("clusterId and clusterName can not be null") + } + + result, err := c.consoleClient.CreateServiceDeployment(c.ctx, *clusterId, attributes) + if err != nil { + return nil, err + } + if result == nil { + return nil, fmt.Errorf("new created service %s is nil", attributes.Name) + } + return result.CreateServiceDeployment, nil + +} + func (c *client) UpdateComponents(id string, components []*console.ComponentAttributes, errs []*console.ServiceErrorAttributes) error { _, err := c.consoleClient.UpdateServiceComponents(c.ctx, id, components, errs) return err From d73f417c56b3501bde1d759aea3437ce3b80fcf9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 12:41:57 +0100 Subject: [PATCH 017/198] initialize cluster controller --- .../apis/deployments/v1alpha1/cluster.go | 10 +- .../apis/deployments/v1alpha1/provider.go | 2 +- .../v1alpha1/zz_generated.deepcopy.go | 20 +- .../bases/deployments.plural.sh_clusters.yaml | 10 +- controller/pkg/client/cluster.go | 31 +++ controller/pkg/client/console.go | 4 + .../pkg/cluster_controller/reconciler.go | 176 ++++++++++++++++++ controller/pkg/types/reconciler.go | 11 +- 8 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 controller/pkg/client/cluster.go create mode 100644 controller/pkg/cluster_controller/reconciler.go diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index e94d40830..accc51dba 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -158,13 +158,17 @@ type ClusterNodePoolAWSCloudSettings struct { type ClusterStatus struct { // Id from Console. // +kubebuilder:validation:Optional - Id *string `json:"id,omitempty"` + ID *string `json:"id,omitempty"` // CurrentVersion contains current Kubernetes version this cluster is using. // +kubebuilder:validation:Optional CurrentVersion *string `json:"currentVersion,omitempty"` - // Health status. + // KasURL contains KAS URL. + // +kubebuilder:validation:Optional + KasURL *string `json:"kasURL,omitempty"` + + // PingedAt contains timestamp of last successful cluster ping. // +optional - Health *string `json:"health,omitempty"` + PingedAt *string `json:"pingedAt,omitempty"` } diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index 2eca2c9c3..bb9d6e35c 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -86,5 +86,5 @@ type ProviderSpec struct { type ProviderStatus struct { // ID of the provider in the Console API. // +kubebuilder:validation:Optional - ID string `json:"id,omitempty"` + ID *string `json:"id,omitempty"` } diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 245f5b6e7..d50c634fc 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -383,8 +383,8 @@ func (in *ClusterSpec) DeepCopy() *ClusterSpec { // 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 - if in.Id != nil { - in, out := &in.Id, &out.Id + if in.ID != nil { + in, out := &in.ID, &out.ID *out = new(string) **out = **in } @@ -393,8 +393,13 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = new(string) **out = **in } - if in.Health != nil { - in, out := &in.Health, &out.Health + if in.KasURL != nil { + in, out := &in.KasURL, &out.KasURL + *out = new(string) + **out = **in + } + if in.PingedAt != nil { + in, out := &in.PingedAt, &out.PingedAt *out = new(string) **out = **in } @@ -540,7 +545,7 @@ func (in *Provider) DeepCopyInto(out *Provider) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Provider. @@ -616,6 +621,11 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 4b7c0d123..d2be3f3bc 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -289,12 +289,16 @@ spec: description: CurrentVersion contains current Kubernetes version this cluster is using. type: string - health: - description: Health status. - type: string id: description: Id from Console. type: string + kasURL: + description: KasURL contains KAS URL. + type: string + pingedAt: + description: PingedAt contains timestamp of last successful cluster + ping. + type: string type: object type: object served: true diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go new file mode 100644 index 000000000..d71e3f20b --- /dev/null +++ b/controller/pkg/client/cluster.go @@ -0,0 +1,31 @@ +package client + +import ( + console "github.com/pluralsh/console-client-go" +) + +func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.CreateCluster, error) { + return c.consoleClient.CreateCluster(c.ctx, attrs) +} + +func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { + response, err := c.consoleClient.GetCluster(c.ctx, id) + if err != nil { + return nil, err + } + + return response.Cluster, nil +} + +func (c *client) ListClusters() (*console.ListClusters, error) { + return c.consoleClient.ListClusters(c.ctx, nil, nil, nil) +} + +func (c *client) DeleteCluster(id string) (*console.ClusterFragment, error) { + response, err := c.consoleClient.DeleteCluster(c.ctx, id) + if err != nil { + return nil, err + } + + return response.DeleteCluster, nil +} diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index c2ddd5a13..d50754404 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -31,6 +31,10 @@ type ConsoleClient interface { UpdateRepository(id string, attrs console.GitAttributes) (*console.UpdateGitRepository, error) DeleteRepository(id string) error GetRepository(url *string) (*console.GetGitRepository, error) + GetCluster(id *string) (*console.ClusterFragment, error) + CreateCluster(attrs console.ClusterAttributes) (*console.CreateCluster, error) + ListClusters() (*console.ListClusters, error) + DeleteCluster(id string) (*console.ClusterFragment, error) } func New(url, token string) ConsoleClient { diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go new file mode 100644 index 000000000..efc74899e --- /dev/null +++ b/controller/pkg/cluster_controller/reconciler.go @@ -0,0 +1,176 @@ +package cluster_controller + +import ( + "context" + "fmt" + "reflect" + "time" + + console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/errors" + "github.com/pluralsh/console/controller/pkg/kubernetes" + "go.uber.org/zap" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + ClusterFinalizer = "deployments.plural.sh/cluster-protection" +) + +// Reconciler reconciles a Cluster object. +type Reconciler struct { + client.Client + ConsoleClient consoleclient.ConsoleClient + Log *zap.SugaredLogger + Scheme *runtime.Scheme +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Cluster{}). + Complete(r) +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + cluster := &v1alpha1.Cluster{} + if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if !cluster.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, cluster) + } + + apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) + if err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + + // TODO: Move? + var providerId *string + if cluster.Spec.Cloud != "byok" { + provider, err := r.getProvider(ctx, cluster) + if err != nil { + return ctrl.Result{}, err + } + + if provider.Status.ID == nil { + r.Log.Info(fmt.Errorf("provider does not have ID set yet")) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + providerId = provider.Status.ID + } + + if apiCluster == nil { + // TODO: Set owner ref. + _, err := r.ConsoleClient.CreateCluster(console.ClusterAttributes{ + Name: "", + Handle: nil, + ProviderID: providerId, + CredentialID: nil, + Version: nil, + Protect: nil, + Kubeconfig: nil, + CloudSettings: nil, + NodePools: nil, + ReadBindings: nil, + WriteBindings: nil, + Tags: nil, + }) + if err != nil { + return ctrl.Result{}, err + } + // TODO: apiCluster = resp.CreateCluster + } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, cluster, ClusterFinalizer); err != nil { + return ctrl.Result{}, err + } + + if err := r.updateStatus(ctx, cluster, func(r *v1alpha1.Cluster) { + r.Status.ID = &apiCluster.ID + r.Status.KasURL = apiCluster.KasURL + r.Status.CurrentVersion = apiCluster.CurrentVersion + r.Status.PingedAt = apiCluster.PingedAt + }); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil +} + +func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { + if controllerutil.ContainsFinalizer(cluster, ClusterFinalizer) { + r.Log.Info("delete cluster") + if cluster.Status.ID == nil { + return ctrl.Result{}, fmt.Errorf("cluster ID can not be nil") + } + + apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + + if apiCluster != nil { + if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + + if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, cluster, ClusterFinalizer); err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} + +func (r *Reconciler) updateStatus(ctx context.Context, cluster *v1alpha1.Cluster, patch func(cluster *v1alpha1.Cluster)) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := r.Client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(cluster), cluster); err != nil { + return fmt.Errorf("could not fetch current cluster state, got error: %+v", err) + } + + original := cluster.DeepCopy() + + patch(cluster) + + if reflect.DeepEqual(original.Status, cluster.Status) { + return nil + } + + return r.Client.Status().Patch(ctx, cluster, ctrlruntimeclient.MergeFrom(original)) + }) +} + +func (r *Reconciler) getProvider(ctx context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Provider, error) { + if cluster.Spec.ProviderRef == nil { + return nil, fmt.Errorf("could not get provider, reference is not set") + } + + provider := &v1alpha1.Provider{} + err := r.Get(ctx, types.NamespacedName{ + Name: cluster.Spec.ProviderRef.Name, + Namespace: cluster.Spec.ProviderRef.Namespace, + }, provider) + if err != nil { + return nil, fmt.Errorf("could not get provider, got error: %+v", err) + } + + return provider, nil +} diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 2b066307d..04ce89269 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -2,7 +2,9 @@ package types import ( "fmt" + "github.com/pluralsh/console/controller/pkg/client" + clustercontroller "github.com/pluralsh/console/controller/pkg/cluster_controller" gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" providercontroller "github.com/pluralsh/console/controller/pkg/provider_controller" "go.uber.org/zap" @@ -46,7 +48,12 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, logger *zap.SugaredLogger, c case ServiceDeploymentReconciler: return nil, unsupported case ClusterReconciler: - return nil, unsupported + return &clustercontroller.Reconciler{ + Client: mgr.GetClient(), + ConsoleClient: consoleClient, + Log: logger, + Scheme: mgr.GetScheme(), + }, nil case ProviderReconciler: return &providercontroller.Reconciler{ Client: mgr.GetClient(), @@ -66,7 +73,7 @@ type ReconcilerList []Reconciler // Reconcilers defines a list of reconcilers that will be started by default // if '--reconcilers=...' flag is not provided. func Reconcilers() ReconcilerList { - return []Reconciler{GitRepositoryReconciler, ProviderReconciler} + return []Reconciler{GitRepositoryReconciler, ProviderReconciler, ClusterReconciler} } // ToControllers returns a list of Controller instances based on this Reconciler array. From 44f61d72daaf40420de3f6d0d176f0192a38e0b9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 12:59:22 +0100 Subject: [PATCH 018/198] update cluster model --- .../apis/deployments/v1alpha1/cluster.go | 46 +++++++++++++++++-- .../bases/deployments.plural.sh_clusters.yaml | 24 +++++++--- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index e5839921b..47f636788 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -19,8 +19,8 @@ type ClusterList struct { // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Namespaced // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Health",type="string",JSONPath=".status.health",description="Cluster health status" -// +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console cluster ID" +// +kubebuilder:printcolumn:name="CurrentVersion",type="string",JSONPath=".status.currentVersion",description="Current Kubernetes version" +// +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console ID" type Cluster struct { metav1.TypeMeta `json:",inline"` @@ -42,10 +42,14 @@ type ClusterSpec struct { // Handle is a short, unique human-readable name used to identify this cluster. // Does not necessarily map to the cloud resource name. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + // +kubebuilder:example:=myclusterhandle Handle *string `json:"handle,omitempty"` // Version of Kubernetes to use for this cluster. Can be skipped only for BYOK. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + // +kubebuilder:example:="1.25.11" Version *string `json:"version,omitempty"` // ProviderRef references provider to use for this cluster. Can be skipped only for BYOK. @@ -53,12 +57,16 @@ type ClusterSpec struct { ProviderRef *corev1.ObjectReference `json:"providerRef,omitempty"` // Cloud provider to use for this cluster. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string // +kubebuilder:validation:Enum=aws;azure;gcp;byok // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Cloud is immutable" + // +kubebuilder:example:=azure Cloud string `json:"cloud"` // Protect cluster from being deleted. // +kubebuilder:validation:Optional + // +kubebuilder:example:=false Protect *bool `json:"protect,omitempty"` // Tags used to filter clusters. @@ -67,13 +75,16 @@ type ClusterSpec struct { // Bindings contain read and write policies of this cluster // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Bindings are immutable" Bindings *Bindings `json:"bindings,omitempty"` // CloudSettings contains cloud-specific settings for this cluster. // +kubebuilder:validation:Optional + // +structType=atomic CloudSettings *ClusterCloudSettings `json:"cloudSettings,omitempty"` // NodePools contains specs of node pools managed by this cluster. + // +kubebuilder:validation:Optional NodePools []ClusterNodePool `json:"nodePools"` } @@ -93,40 +104,61 @@ type ClusterCloudSettings struct { type ClusterAWSCloudSettings struct { // Region in AWS to deploy this cluster to. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string Region string `json:"region"` } type ClusterAzureCloudSettings struct { // ResourceGroup is a name for the Azure resource group for this cluster. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string + // +kubebuilder:example:=myresourcegroup ResourceGroup string `json:"resourceGroup"` // Network is a name for the Azure virtual network for this cluster. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string + // +kubebuilder:example:=mynetwork Network string `json:"network"` // SubscriptionId is GUID of the Azure subscription to hold this cluster. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string SubscriptionId string `json:"subscriptionId"` - // Location in Azure to deploy this cluster to, i.e. eastus. + // Location in Azure to deploy this cluster to. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string + // +kubebuilder:example:=eastus Location string `json:"location"` } type ClusterGCPCloudSettings struct { // Project in GCP to deploy cluster to. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string Project string `json:"project"` } type ClusterNodePool struct { // Name of the node pool. Must be unique. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string Name string `json:"name"` // InstanceType contains the type of node to use. Usually cloud-specific. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string InstanceType string `json:"instanceType"` // MinSize is minimum number of instances in this node pool. + // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 MinSize int `json:"minSize"` // MaxSize is maximum number of instances in this node pool. + // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 MaxSize int `json:"maxSize"` @@ -140,6 +172,7 @@ type ClusterNodePool struct { // CloudSettings contains cloud-specific settings for this node pool. // +kubebuilder:validation:Optional + // +structType=atomic CloudSettings *ClusterNodePoolCloudSettings `json:"cloudSettings,omitempty"` } @@ -152,23 +185,28 @@ type ClusterNodePoolCloudSettings struct { type ClusterNodePoolAWSCloudSettings struct { // LaunchTemplateId is an ID of custom launch template for your nodes. Useful for Golden AMI setups. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string LaunchTemplateId *string `json:"launchTemplateId,omitempty"` } type ClusterStatus struct { // Id from Console. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string ID *string `json:"id,omitempty"` // CurrentVersion contains current Kubernetes version this cluster is using. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string CurrentVersion *string `json:"currentVersion,omitempty"` // KasURL contains KAS URL. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string KasURL *string `json:"kasURL,omitempty"` // PingedAt contains timestamp of last successful cluster ping. - // +optional + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string PingedAt *string `json:"pingedAt,omitempty"` } diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 4d4e2eaab..a2e70742b 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -16,11 +16,11 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - description: Cluster health status - jsonPath: .status.health - name: Health + - description: Current Kubernetes version + jsonPath: .status.currentVersion + name: CurrentVersion type: string - - description: Console cluster ID + - description: Console ID jsonPath: .status.id name: Id type: string @@ -76,6 +76,9 @@ spec: type: object type: array type: object + x-kubernetes-validations: + - message: Bindings are immutable + rule: self == oldSelf cloud: description: Cloud provider to use for this cluster. enum: @@ -83,6 +86,7 @@ spec: - azure - gcp - byok + example: azure type: string x-kubernetes-validations: - message: Cloud is immutable @@ -104,16 +108,18 @@ spec: description: Azure cluster customizations. properties: location: - description: Location in Azure to deploy this cluster to, - i.e. eastus. + description: Location in Azure to deploy this cluster to. + example: eastus type: string network: description: Network is a name for the Azure virtual network for this cluster. + example: mynetwork type: string resourceGroup: description: ResourceGroup is a name for the Azure resource group for this cluster. + example: myresourcegroup type: string subscriptionId: description: SubscriptionId is GUID of the Azure subscription @@ -135,10 +141,12 @@ spec: - project type: object type: object + x-kubernetes-map-type: atomic handle: description: Handle is a short, unique human-readable name used to identify this cluster. Does not necessarily map to the cloud resource name. + example: myclusterhandle type: string nodePools: description: NodePools contains specs of node pools managed by this @@ -158,6 +166,7 @@ spec: type: string type: object type: object + x-kubernetes-map-type: atomic instanceType: description: InstanceType contains the type of node to use. Usually cloud-specific. @@ -215,6 +224,7 @@ spec: type: array protect: description: Protect cluster from being deleted. + example: false type: boolean providerRef: description: ProviderRef references provider to use for this cluster. @@ -262,10 +272,10 @@ spec: version: description: Version of Kubernetes to use for this cluster. Can be skipped only for BYOK. + example: 1.25.11 type: string required: - cloud - - nodePools type: object x-kubernetes-validations: - message: AWS cloud settings are required From a2f5ed4c2d474796d1cf0538dc3d230a585cf8b9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 13:14:28 +0100 Subject: [PATCH 019/198] update cluster controller --- controller/pkg/client/cluster.go | 26 +++++++++++++++++-- controller/pkg/client/console.go | 2 +- .../pkg/cluster_controller/reconciler.go | 8 +++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go index d71e3f20b..774bc31f0 100644 --- a/controller/pkg/client/cluster.go +++ b/controller/pkg/client/cluster.go @@ -4,8 +4,30 @@ import ( console "github.com/pluralsh/console-client-go" ) -func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.CreateCluster, error) { - return c.consoleClient.CreateCluster(c.ctx, attrs) +func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) { + response, err := c.consoleClient.CreateCluster(c.ctx, attrs) + if err != nil { + return nil, err + } + + // Create cluster returns cluster fragment extended with deploy token which makes types incompatible. + // Doing mapping here to use the same types anywhere. Deploy token is not needed at the moment anyway. + return &console.ClusterFragment{ + ID: response.CreateCluster.ID, + Name: response.CreateCluster.Name, + Handle: response.CreateCluster.Handle, + Self: response.CreateCluster.Self, + Version: response.CreateCluster.Version, + InsertedAt: response.CreateCluster.InsertedAt, + PingedAt: response.CreateCluster.PingedAt, + Protect: response.CreateCluster.Protect, + CurrentVersion: response.CreateCluster.CurrentVersion, + KasURL: response.CreateCluster.KasURL, + Tags: response.CreateCluster.Tags, + Credential: response.CreateCluster.Credential, + Provider: response.CreateCluster.Provider, + NodePools: response.CreateCluster.NodePools, + }, nil } func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index b0c715ed1..b0be62d58 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -33,7 +33,7 @@ type ConsoleClient interface { GetRepository(url *string) (*console.GetGitRepository, error) CreateService(clusterId *string, attributes console.ServiceDeploymentAttributes) (*console.ServiceDeploymentFragment, error) GetCluster(id *string) (*console.ClusterFragment, error) - CreateCluster(attrs console.ClusterAttributes) (*console.CreateCluster, error) + CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) ListClusters() (*console.ListClusters, error) DeleteCluster(id string) (*console.ClusterFragment, error) } diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index efc74899e..80dcb6392 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -51,7 +51,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } if !cluster.GetDeletionTimestamp().IsZero() { - return r.handleDelete(ctx, cluster) + return r.delete(ctx, cluster) } apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) @@ -79,7 +79,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if apiCluster == nil { // TODO: Set owner ref. - _, err := r.ConsoleClient.CreateCluster(console.ClusterAttributes{ + response, err := r.ConsoleClient.CreateCluster(console.ClusterAttributes{ Name: "", Handle: nil, ProviderID: providerId, @@ -96,7 +96,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - // TODO: apiCluster = resp.CreateCluster + apiCluster = response } if err := kubernetes.TryAddFinalizer(ctx, r.Client, cluster, ClusterFinalizer); err != nil { return ctrl.Result{}, err @@ -114,7 +114,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } -func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { +func (r *Reconciler) delete(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { if controllerutil.ContainsFinalizer(cluster, ClusterFinalizer) { r.Log.Info("delete cluster") if cluster.Status.ID == nil { From be76bc6249028f302f8eebb1dc799ae43003c501 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 8 Dec 2023 13:46:50 +0100 Subject: [PATCH 020/198] remove ns from provider crd example --- controller/config/crd/examples/provider_byok.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/controller/config/crd/examples/provider_byok.yaml b/controller/config/crd/examples/provider_byok.yaml index d69df86a7..acf860069 100644 --- a/controller/config/crd/examples/provider_byok.yaml +++ b/controller/config/crd/examples/provider_byok.yaml @@ -3,7 +3,6 @@ apiVersion: deployments.plural.sh/v1alpha1 kind: Provider metadata: name: byok - namespace: byok spec: cloud: byok name: byok From 18e09559fbf29ef7ec15d0ce31520bdda180e9ce Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 14:12:42 +0100 Subject: [PATCH 021/198] add more cluster attributes --- controller/go.mod | 4 ++ controller/go.sum | 10 ++++ .../pkg/cluster_controller/attributes.go | 52 +++++++++++++++++++ .../pkg/cluster_controller/reconciler.go | 22 ++------ 4 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 controller/pkg/cluster_controller/attributes.go diff --git a/controller/go.mod b/controller/go.mod index 165978b1a..5613e0c07 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -7,6 +7,7 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 github.com/pluralsh/console-client-go v0.0.53 + github.com/pluralsh/polly v0.1.4 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 @@ -20,6 +21,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // 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.9.0 // indirect @@ -57,12 +59,14 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/samber/lo v1.33.0 // indirect github.com/schollz/progressbar/v3 v3.8.6 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/vektah/gqlparser/v2 v2.5.10 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.5.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/sys v0.12.0 // indirect diff --git a/controller/go.sum b/controller/go.sum index 70e9f6710..249310031 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -48,6 +48,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 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/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -230,6 +232,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pluralsh/console-client-go v0.0.53 h1:Vawo9pP/nrXC8kSP7mUazMIo5YEigRNchDi/RZWnpVc= github.com/pluralsh/console-client-go v0.0.53/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= +github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= +github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= 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= @@ -261,6 +265,8 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua 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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk= +github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= @@ -285,6 +291,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -325,6 +333,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 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= diff --git a/controller/pkg/cluster_controller/attributes.go b/controller/pkg/cluster_controller/attributes.go new file mode 100644 index 000000000..629772479 --- /dev/null +++ b/controller/pkg/cluster_controller/attributes.go @@ -0,0 +1,52 @@ +package cluster_controller + +import ( + console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/polly/algorithms" +) + +func clusterAttributes(cluster *v1alpha1.Cluster, providerId *string) console.ClusterAttributes { + attrs := console.ClusterAttributes{ + Name: cluster.Name, + Handle: cluster.Spec.Handle, + ProviderID: providerId, + Version: cluster.Spec.Version, + Protect: cluster.Spec.Protect, + CloudSettings: nil, + NodePools: nil, + Tags: tagsAttribute(cluster.Spec.Tags), + } + + if cluster.Spec.Bindings != nil { + attrs.ReadBindings = bindingsAttribute(cluster.Spec.Bindings.Read) + attrs.WriteBindings = bindingsAttribute(cluster.Spec.Bindings.Write) + } + + return attrs +} + +func bindingsAttribute(input []v1alpha1.Binding) []*console.PolicyBindingAttributes { + return algorithms.Map(input, func(v v1alpha1.Binding) *console.PolicyBindingAttributes { + return &console.PolicyBindingAttributes{ + ID: v.Id, + UserID: v.UserId, + GroupID: v.GroupId, + } + }) +} + +func tagsAttribute(input map[string]string) []*console.TagAttributes { + if len(input) == 0 { + return nil + } + + output := make([]*console.TagAttributes, len(input)) + for name, value := range input { + output = append(output, &console.TagAttributes{ + Name: name, + Value: value, + }) + } + return output +} diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 80dcb6392..49bac7724 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -6,7 +6,6 @@ import ( "reflect" "time" - console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" @@ -55,10 +54,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) - if err != nil { - if !errors.IsNotFound(err) { - return ctrl.Result{}, err - } + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err } // TODO: Move? @@ -79,20 +76,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if apiCluster == nil { // TODO: Set owner ref. - response, err := r.ConsoleClient.CreateCluster(console.ClusterAttributes{ - Name: "", - Handle: nil, - ProviderID: providerId, - CredentialID: nil, - Version: nil, - Protect: nil, - Kubeconfig: nil, - CloudSettings: nil, - NodePools: nil, - ReadBindings: nil, - WriteBindings: nil, - Tags: nil, - }) + response, err := r.ConsoleClient.CreateCluster(clusterAttributes(cluster, providerId)) if err != nil { return ctrl.Result{}, err } From 828c68458aa2dcfc1b44a06f37ffccf006785872 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 14:25:32 +0100 Subject: [PATCH 022/198] add more cluster attributes --- .../apis/deployments/v1alpha1/cluster.go | 4 +- .../apis/deployments/v1alpha1/common.go | 6 +-- .../v1alpha1/zz_generated.deepcopy.go | 12 ++--- .../bases/deployments.plural.sh_clusters.yaml | 14 +++--- .../bases/deployments.plural.sh_services.yaml | 12 ++--- .../pkg/cluster_controller/attributes.go | 44 +++++++++++++------ 6 files changed, 56 insertions(+), 36 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 47f636788..a2ba8de26 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -155,12 +155,12 @@ type ClusterNodePool struct { // MinSize is minimum number of instances in this node pool. // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 - MinSize int `json:"minSize"` + MinSize int64 `json:"minSize"` // MaxSize is maximum number of instances in this node pool. // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 - MaxSize int `json:"maxSize"` + MaxSize int64 `json:"maxSize"` // Labels to apply to the nodes in this pool. Useful for node selectors. // +kubebuilder:validation:Optional diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index 8b8cef349..ccc36478e 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -18,15 +18,15 @@ type Bindings struct { type Binding struct { // TODO: Add docs. // +kubebuilder:validation:Optional - Id *string `json:"id,omitempty"` + ID *string `json:"id,omitempty"` // TODO: Add docs. // +kubebuilder:validation:Optional - UserId *string `json:"userId,omitempty"` + UserID *string `json:"UserID,omitempty"` // TODO: Add docs. // +kubebuilder:validation:Optional - GroupId *string `json:"groupId,omitempty"` + GroupID *string `json:"groupID,omitempty"` } // Taint represents a Kubernetes taint. diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index f08adf9f4..866101217 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -29,18 +29,18 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Binding) DeepCopyInto(out *Binding) { *out = *in - if in.Id != nil { - in, out := &in.Id, &out.Id + if in.ID != nil { + in, out := &in.ID, &out.ID *out = new(string) **out = **in } - if in.UserId != nil { - in, out := &in.UserId, &out.UserId + if in.UserID != nil { + in, out := &in.UserID, &out.UserID *out = new(string) **out = **in } - if in.GroupId != nil { - in, out := &in.GroupId, &out.GroupId + if in.GroupID != nil { + in, out := &in.GroupID, &out.GroupID *out = new(string) **out = **in } diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index a2e70742b..44f4d548c 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -49,13 +49,13 @@ spec: description: Read bindings. items: properties: - groupId: + UserID: description: 'TODO: Add docs.' type: string - id: + groupID: description: 'TODO: Add docs.' type: string - userId: + id: description: 'TODO: Add docs.' type: string type: object @@ -64,13 +64,13 @@ spec: description: Write bindings. items: properties: - groupId: + UserID: description: 'TODO: Add docs.' type: string - id: + groupID: description: 'TODO: Add docs.' type: string - userId: + id: description: 'TODO: Add docs.' type: string type: object @@ -180,11 +180,13 @@ spec: maxSize: description: MaxSize is maximum number of instances in this node pool. + format: int64 minimum: 1 type: integer minSize: description: MinSize is minimum number of instances in this node pool. + format: int64 minimum: 1 type: integer name: diff --git a/controller/config/crd/bases/deployments.plural.sh_services.yaml b/controller/config/crd/bases/deployments.plural.sh_services.yaml index 210b00782..014cbbde6 100644 --- a/controller/config/crd/bases/deployments.plural.sh_services.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_services.yaml @@ -45,13 +45,13 @@ spec: description: Read bindings. items: properties: - groupId: + UserID: description: 'TODO: Add docs.' type: string - id: + groupID: description: 'TODO: Add docs.' type: string - userId: + id: description: 'TODO: Add docs.' type: string type: object @@ -60,13 +60,13 @@ spec: description: Write bindings. items: properties: - groupId: + UserID: description: 'TODO: Add docs.' type: string - id: + groupID: description: 'TODO: Add docs.' type: string - userId: + id: description: 'TODO: Add docs.' type: string type: object diff --git a/controller/pkg/cluster_controller/attributes.go b/controller/pkg/cluster_controller/attributes.go index 629772479..4447a072a 100644 --- a/controller/pkg/cluster_controller/attributes.go +++ b/controller/pkg/cluster_controller/attributes.go @@ -14,7 +14,7 @@ func clusterAttributes(cluster *v1alpha1.Cluster, providerId *string) console.Cl Version: cluster.Spec.Version, Protect: cluster.Spec.Protect, CloudSettings: nil, - NodePools: nil, + NodePools: nodePoolsAttribute(cluster.Spec.NodePools), Tags: tagsAttribute(cluster.Spec.Tags), } @@ -26,27 +26,45 @@ func clusterAttributes(cluster *v1alpha1.Cluster, providerId *string) console.Cl return attrs } -func bindingsAttribute(input []v1alpha1.Binding) []*console.PolicyBindingAttributes { - return algorithms.Map(input, func(v v1alpha1.Binding) *console.PolicyBindingAttributes { - return &console.PolicyBindingAttributes{ - ID: v.Id, - UserID: v.UserId, - GroupID: v.GroupId, +func nodePoolsAttribute(nodePools []v1alpha1.ClusterNodePool) []*console.NodePoolAttributes { + if nodePools == nil { + return nil + } + + return algorithms.Map(nodePools, func(nodePool v1alpha1.ClusterNodePool) *console.NodePoolAttributes { + return &console.NodePoolAttributes{ + Name: nodePool.Name, + MinSize: nodePool.MinSize, + MaxSize: nodePool.MaxSize, + InstanceType: nodePool.InstanceType, + Labels: nil, + Taints: nil, + CloudSettings: nil, } }) } -func tagsAttribute(input map[string]string) []*console.TagAttributes { - if len(input) == 0 { +func tagsAttribute(tags map[string]string) []*console.TagAttributes { + if tags == nil { return nil } - output := make([]*console.TagAttributes, len(input)) - for name, value := range input { - output = append(output, &console.TagAttributes{ + attr := make([]*console.TagAttributes, len(tags)) + for name, value := range tags { + attr = append(attr, &console.TagAttributes{ Name: name, Value: value, }) } - return output + return attr +} + +func bindingsAttribute(bindings []v1alpha1.Binding) []*console.PolicyBindingAttributes { + return algorithms.Map(bindings, func(binding v1alpha1.Binding) *console.PolicyBindingAttributes { + return &console.PolicyBindingAttributes{ + ID: binding.ID, + UserID: binding.UserID, + GroupID: binding.GroupID, + } + }) } From ee4c71e2aac0d3508bd7b90bf4fec43526395403 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 14:26:29 +0100 Subject: [PATCH 023/198] fix slice initialization --- controller/pkg/cluster_controller/attributes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/pkg/cluster_controller/attributes.go b/controller/pkg/cluster_controller/attributes.go index 4447a072a..60cd9df08 100644 --- a/controller/pkg/cluster_controller/attributes.go +++ b/controller/pkg/cluster_controller/attributes.go @@ -49,7 +49,7 @@ func tagsAttribute(tags map[string]string) []*console.TagAttributes { return nil } - attr := make([]*console.TagAttributes, len(tags)) + attr := make([]*console.TagAttributes, 0) for name, value := range tags { attr = append(attr, &console.TagAttributes{ Name: name, From caa2dda7fa485d7f2e7130c88bf2e2effa70c8fc Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 14:52:52 +0100 Subject: [PATCH 024/198] create service --- .../apis/deployments/v1alpha1/service.go | 6 +- .../v1alpha1/zz_generated.deepcopy.go | 22 ++- controller/pkg/kubernetes/helper.go | 24 ++++ .../pkg/service_controller/controller.go | 126 +++++++++++++++++- 4 files changed, 170 insertions(+), 8 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index be50487cf..a001efec0 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -28,11 +28,11 @@ type ServiceGit struct { type ServiceHelm struct { // +optional - ValuesRef corev1.ConfigMapKeySelector `json:"values,omitempty"` + ValuesRef *corev1.ConfigMapKeySelector `json:"values,omitempty"` // +optional - ValuesFiles []string `json:"valuesFiles,omitempty"` + ValuesFiles []*string `json:"valuesFiles,omitempty"` // +optional - ChartRef corev1.ConfigMapKeySelector `json:"chart,omitempty"` + ChartRef *corev1.ConfigMapKeySelector `json:"chart,omitempty"` // +optional Version *string `json:"version,omitempty"` // +optional diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 866101217..94a0f0b3b 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -728,13 +728,27 @@ func (in *ServiceGit) DeepCopy() *ServiceGit { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceHelm) DeepCopyInto(out *ServiceHelm) { *out = *in - in.ValuesRef.DeepCopyInto(&out.ValuesRef) + if in.ValuesRef != nil { + in, out := &in.ValuesRef, &out.ValuesRef + *out = new(v1.ConfigMapKeySelector) + (*in).DeepCopyInto(*out) + } if in.ValuesFiles != nil { in, out := &in.ValuesFiles, &out.ValuesFiles - *out = make([]string, len(*in)) - copy(*out, *in) + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } + if in.ChartRef != nil { + in, out := &in.ChartRef, &out.ChartRef + *out = new(v1.ConfigMapKeySelector) + (*in).DeepCopyInto(*out) } - in.ChartRef.DeepCopyInto(&out.ChartRef) if in.Version != nil { in, out := &in.Version, &out.Version *out = new(string) diff --git a/controller/pkg/kubernetes/helper.go b/controller/pkg/kubernetes/helper.go index 2cea3edbc..78f6c9c09 100644 --- a/controller/pkg/kubernetes/helper.go +++ b/controller/pkg/kubernetes/helper.go @@ -126,3 +126,27 @@ func DeleteSecret(ctx context.Context, client client.Client, secretNamespace, se // We successfully deleted the secret return nil } + +func GetSecret(ctx context.Context, client client.Client, ref *corev1.SecretReference) (*corev1.Secret, error) { + secret := &corev1.Secret{} + name := types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace} + err := client.Get(ctx, name, secret) + if err != nil { + return nil, err + } + return secret, err +} + +func GetConfigMapData(ctx context.Context, client client.Client, namespace string, ref *corev1.ConfigMapKeySelector) (string, error) { + configMap := &corev1.ConfigMap{} + name := types.NamespacedName{Name: ref.Name, Namespace: namespace} + err := client.Get(ctx, name, configMap) + if err != nil { + return "", err + } + if configMap.Data != nil { + return configMap.Data[ref.Key], nil + } + + return "", nil +} diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index b1f684b20..034f8e8c8 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,13 +2,17 @@ package servicecontroller import ( "context" + "github.com/pluralsh/console/controller/pkg/kubernetes" "reflect" "time" + console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -16,6 +20,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +const ( + ServiceFinalizer = "deployments.plural.sh/service-protection" +) + // Reconciler reconciles a Service object type Reconciler struct { client.Client @@ -70,7 +78,22 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } } if existingService == nil { - + attr, err := r.genCreateAttr(ctx, service, repository.Status.Id) + if err != nil { + return ctrl.Result{}, err + } + _, err = r.ConsoleClient.CreateService(cluster.Status.Id, *attr) + if err != nil { + return ctrl.Result{}, err + } + existingService, err = r.ConsoleClient.GetService(*cluster.Status.Id, service.Name) + if err != nil { + return ctrl.Result{}, err + } + } + err = r.updateReferences(ctx, service) + if err != nil { + return ctrl.Result{}, err } return ctrl.Result{ @@ -86,6 +109,107 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +func (r *Reconciler) genCreateAttr(ctx context.Context, service *v1alpha1.Service, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { + attr := &console.ServiceDeploymentAttributes{ + Name: service.Name, + Namespace: service.Namespace, + Version: &service.Spec.Version, + DocsPath: service.Spec.DocsPath, + Protect: &service.Spec.Protect, + RepositoryID: repositoryId, + } + if service.Spec.Bindings != nil { + attr.ReadBindings = make([]*console.PolicyBindingAttributes, 0) + attr.WriteBindings = make([]*console.PolicyBindingAttributes, 0) + + for _, r := range service.Spec.Bindings.Read { + attr.ReadBindings = append(attr.ReadBindings, &console.PolicyBindingAttributes{ + ID: r.Id, + UserID: r.UserId, + GroupID: r.GroupId, + }) + } + for _, w := range service.Spec.Bindings.Write { + attr.WriteBindings = append(attr.WriteBindings, &console.PolicyBindingAttributes{ + ID: w.Id, + UserID: w.UserId, + GroupID: w.GroupId, + }) + } + } + + if service.Spec.Kustomize != nil { + attr.Kustomize = &console.KustomizeAttributes{ + Path: service.Spec.Kustomize.Path, + } + } + if service.Spec.Git != nil { + attr.Git = &console.GitRefAttributes{ + Ref: service.Spec.Git.Ref, + Folder: service.Spec.Git.Folder, + } + } + if service.Spec.ConfigurationRef != nil { + attr.Configuration = make([]*console.ConfigAttributes, 0) + secret := &corev1.Secret{} + name := types.NamespacedName{Name: service.Spec.ConfigurationRef.Name, Namespace: service.Spec.ConfigurationRef.Namespace} + err := r.Get(ctx, name, secret) + if err != nil { + return nil, err + } + for k, v := range secret.Data { + value := string(v) + attr.Configuration = append(attr.Configuration, &console.ConfigAttributes{ + Name: k, + Value: &value, + }) + } + + } + if service.Spec.Helm != nil { + attr.Helm = &console.HelmConfigAttributes{ + ValuesFiles: service.Spec.Helm.ValuesFiles, + Version: service.Spec.Helm.Version, + } + if service.Spec.Helm.Repository != nil { + attr.Helm.Repository = &console.NamespacedName{ + Name: service.Spec.Helm.Repository.Name, + Namespace: service.Spec.Helm.Repository.Namespace, + } + } + if service.Spec.Helm.ValuesRef != nil { + val, err := kubernetes.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ValuesRef) + if err != nil { + return nil, err + } + attr.Helm.Values = &val + } + if service.Spec.Helm.ChartRef != nil { + val, err := kubernetes.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ChartRef) + if err != nil { + return nil, err + } + attr.Helm.Chart = &val + } + + } + return attr, nil +} + +func (r *Reconciler) updateReferences(ctx context.Context, service *v1alpha1.Service) error { + if service.Spec.ConfigurationRef != nil { + configurationSecret, err := kubernetes.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) + if err != nil { + return err + } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, configurationSecret, ServiceFinalizer); err != nil { + return err + } + } + + return nil +} + func (r *Reconciler) handleDelete(ctx context.Context, service *v1alpha1.Service) (ctrl.Result, error) { if controllerutil.ContainsFinalizer(service, "") { From d0f8b709ee7462f40836565ddd4574cc55c6e815 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 8 Dec 2023 14:55:19 +0100 Subject: [PATCH 025/198] fix type changes --- .../pkg/service_controller/controller.go | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 034f8e8c8..b92b06678 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,7 +2,6 @@ package servicecontroller import ( "context" - "github.com/pluralsh/console/controller/pkg/kubernetes" "reflect" "time" @@ -10,6 +9,7 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" + "github.com/pluralsh/console/controller/pkg/kubernetes" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -44,7 +44,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { return ctrl.Result{}, err } - if cluster.Status.Id == nil { + if cluster.Status.ID == nil { r.Log.Info("Cluster is not ready", service.Spec.ClusterRef.Name) return ctrl.Result{ // update status @@ -71,7 +71,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, nil } - existingService, err := r.ConsoleClient.GetService(*cluster.Status.Id, service.Name) + existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, err @@ -82,11 +82,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - _, err = r.ConsoleClient.CreateService(cluster.Status.Id, *attr) + _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) if err != nil { return ctrl.Result{}, err } - existingService, err = r.ConsoleClient.GetService(*cluster.Status.Id, service.Name) + existingService, err = r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil { return ctrl.Result{}, err } @@ -124,16 +124,16 @@ func (r *Reconciler) genCreateAttr(ctx context.Context, service *v1alpha1.Servic for _, r := range service.Spec.Bindings.Read { attr.ReadBindings = append(attr.ReadBindings, &console.PolicyBindingAttributes{ - ID: r.Id, - UserID: r.UserId, - GroupID: r.GroupId, + ID: r.ID, + UserID: r.UserID, + GroupID: r.GroupID, }) } for _, w := range service.Spec.Bindings.Write { attr.WriteBindings = append(attr.WriteBindings, &console.PolicyBindingAttributes{ - ID: w.Id, - UserID: w.UserId, - GroupID: w.GroupId, + ID: w.ID, + UserID: w.UserID, + GroupID: w.GroupID, }) } } From 80616d9df0b00e3a6b0111d9b209902a7d795784 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 15:16:09 +0100 Subject: [PATCH 026/198] refactor cluster attributes --- .../apis/deployments/v1alpha1/cluster.go | 105 ++++++++++++++++++ .../apis/deployments/v1alpha1/common.go | 16 +++ .../pkg/cluster_controller/attributes.go | 70 ------------ 3 files changed, 121 insertions(+), 70 deletions(-) delete mode 100644 controller/pkg/cluster_controller/attributes.go diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index a2ba8de26..ad2c1e93e 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -1,6 +1,8 @@ package v1alpha1 import ( + console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -38,6 +40,36 @@ type Cluster struct { Status ClusterStatus `json:"status,omitempty"` } +func (c *Cluster) Attributes(providerId *string) console.ClusterAttributes { + attrs := console.ClusterAttributes{ + Name: c.Name, + Handle: c.Spec.Handle, + ProviderID: providerId, + Version: c.Spec.Version, + Protect: c.Spec.Protect, + CloudSettings: c.Spec.CloudSettings.Attributes(), + NodePools: algorithms.Map(c.Spec.NodePools, + func(np ClusterNodePool) *console.NodePoolAttributes { return np.Attributes() }), + } + + if c.Spec.Tags != nil { + tags := make([]*console.TagAttributes, 0) + for name, value := range c.Spec.Tags { + tags = append(tags, &console.TagAttributes{Name: name, Value: value}) + } + attrs.Tags = tags + } + + if c.Spec.Bindings != nil { + attrs.ReadBindings = algorithms.Map(c.Spec.Bindings.Read, + func(b Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + attrs.WriteBindings = algorithms.Map(c.Spec.Bindings.Write, + func(b Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + } + + return attrs +} + type ClusterSpec struct { // Handle is a short, unique human-readable name used to identify this cluster. // Does not necessarily map to the cloud resource name. @@ -102,6 +134,18 @@ type ClusterCloudSettings struct { GCP *ClusterGCPCloudSettings `json:"gcp,omitempty"` } +func (cs *ClusterCloudSettings) Attributes() *console.CloudSettingsAttributes { + if cs == nil { + return nil + } + + return &console.CloudSettingsAttributes{ + Aws: cs.AWS.Attributes(), + Azure: cs.Azure.Attributes(), + Gcp: cs.GCP.Attributes(), + } +} + type ClusterAWSCloudSettings struct { // Region in AWS to deploy this cluster to. // +kubebuilder:validation:Required @@ -109,6 +153,16 @@ type ClusterAWSCloudSettings struct { Region string `json:"region"` } +func (cs *ClusterAWSCloudSettings) Attributes() *console.AwsCloudAttributes { + if cs == nil { + return nil + } + + return &console.AwsCloudAttributes{ + Region: &cs.Region, + } +} + type ClusterAzureCloudSettings struct { // ResourceGroup is a name for the Azure resource group for this cluster. // +kubebuilder:validation:Required @@ -134,11 +188,46 @@ type ClusterAzureCloudSettings struct { Location string `json:"location"` } +func (cs *ClusterAzureCloudSettings) Attributes() *console.AzureCloudAttributes { + if cs == nil { + return nil + } + + return &console.AzureCloudAttributes{ + Location: &cs.Location, + SubscriptionID: &cs.SubscriptionId, + ResourceGroup: &cs.ResourceGroup, + Network: &cs.Network, + } +} + type ClusterGCPCloudSettings struct { // Project in GCP to deploy cluster to. // +kubebuilder:validation:Required // +kubebuilder:validation:Type:=string Project string `json:"project"` + + // TODO: Add docs. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string + Network string `json:"network"` + + // TODO: Add docs. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type:=string + Region string `json:"region"` +} + +func (cs *ClusterGCPCloudSettings) Attributes() *console.GcpCloudAttributes { + if cs == nil { + return nil + } + + return &console.GcpCloudAttributes{ + Project: &cs.Project, + Network: &cs.Network, + Region: &cs.Region, + } } type ClusterNodePool struct { @@ -176,6 +265,22 @@ type ClusterNodePool struct { CloudSettings *ClusterNodePoolCloudSettings `json:"cloudSettings,omitempty"` } +func (np *ClusterNodePool) Attributes() *console.NodePoolAttributes { + if np == nil { + return nil + } + + return &console.NodePoolAttributes{ + Name: np.Name, + MinSize: np.MinSize, + MaxSize: np.MaxSize, + InstanceType: np.InstanceType, + Labels: nil, // TODO + Taints: nil, // TODO + CloudSettings: nil, // TODO + } +} + type ClusterNodePoolCloudSettings struct { // AWS node pool customizations. // +kubebuilder:validation:Optional diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index ccc36478e..e4e4c4cc4 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -1,5 +1,9 @@ package v1alpha1 +import ( + console "github.com/pluralsh/console-client-go" +) + type NamespacedName struct { Name string `json:"name"` Namespace string `json:"namespace"` @@ -29,6 +33,18 @@ type Binding struct { GroupID *string `json:"groupID,omitempty"` } +func (b *Binding) Attributes() *console.PolicyBindingAttributes { + if b == nil { + return nil + } + + return &console.PolicyBindingAttributes{ + ID: b.ID, + UserID: b.UserID, + GroupID: b.GroupID, + } +} + // Taint represents a Kubernetes taint. type Taint struct { // Effect specifies the effect for the taint. diff --git a/controller/pkg/cluster_controller/attributes.go b/controller/pkg/cluster_controller/attributes.go deleted file mode 100644 index 60cd9df08..000000000 --- a/controller/pkg/cluster_controller/attributes.go +++ /dev/null @@ -1,70 +0,0 @@ -package cluster_controller - -import ( - console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - "github.com/pluralsh/polly/algorithms" -) - -func clusterAttributes(cluster *v1alpha1.Cluster, providerId *string) console.ClusterAttributes { - attrs := console.ClusterAttributes{ - Name: cluster.Name, - Handle: cluster.Spec.Handle, - ProviderID: providerId, - Version: cluster.Spec.Version, - Protect: cluster.Spec.Protect, - CloudSettings: nil, - NodePools: nodePoolsAttribute(cluster.Spec.NodePools), - Tags: tagsAttribute(cluster.Spec.Tags), - } - - if cluster.Spec.Bindings != nil { - attrs.ReadBindings = bindingsAttribute(cluster.Spec.Bindings.Read) - attrs.WriteBindings = bindingsAttribute(cluster.Spec.Bindings.Write) - } - - return attrs -} - -func nodePoolsAttribute(nodePools []v1alpha1.ClusterNodePool) []*console.NodePoolAttributes { - if nodePools == nil { - return nil - } - - return algorithms.Map(nodePools, func(nodePool v1alpha1.ClusterNodePool) *console.NodePoolAttributes { - return &console.NodePoolAttributes{ - Name: nodePool.Name, - MinSize: nodePool.MinSize, - MaxSize: nodePool.MaxSize, - InstanceType: nodePool.InstanceType, - Labels: nil, - Taints: nil, - CloudSettings: nil, - } - }) -} - -func tagsAttribute(tags map[string]string) []*console.TagAttributes { - if tags == nil { - return nil - } - - attr := make([]*console.TagAttributes, 0) - for name, value := range tags { - attr = append(attr, &console.TagAttributes{ - Name: name, - Value: value, - }) - } - return attr -} - -func bindingsAttribute(bindings []v1alpha1.Binding) []*console.PolicyBindingAttributes { - return algorithms.Map(bindings, func(binding v1alpha1.Binding) *console.PolicyBindingAttributes { - return &console.PolicyBindingAttributes{ - ID: binding.ID, - UserID: binding.UserID, - GroupID: binding.GroupID, - } - }) -} From cea7e2d6f7b8fd5288aca5b9edfd1aedf0103523 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 15:17:07 +0100 Subject: [PATCH 027/198] update cluster controller --- controller/pkg/cluster_controller/reconciler.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 49bac7724..f4fff47bd 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -11,7 +11,6 @@ import ( "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" "go.uber.org/zap" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" @@ -43,10 +42,8 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { - if apierrors.IsNotFound(err) { - return ctrl.Result{}, nil - } - return ctrl.Result{}, err + r.Log.Error(err, "unable to fetch cluster") + return ctrl.Result{}, client.IgnoreNotFound(err) } if !cluster.GetDeletionTimestamp().IsZero() { @@ -55,7 +52,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) if err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, err + return ctrl.Result{}, err // TODO: Error handling? } // TODO: Move? @@ -76,7 +73,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if apiCluster == nil { // TODO: Set owner ref. - response, err := r.ConsoleClient.CreateCluster(clusterAttributes(cluster, providerId)) + response, err := r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) if err != nil { return ctrl.Result{}, err } From 076254827667ca75c7cddec34acbf35a22f2ea98 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 16:18:33 +0100 Subject: [PATCH 028/198] update cluster node pool taints and labels --- .../apis/deployments/v1alpha1/cluster.go | 32 +++++++++++++------ .../apis/deployments/v1alpha1/common.go | 11 +++++-- .../bases/deployments.plural.sh_clusters.yaml | 16 ++++++---- .../bases/deployments.plural.sh_services.yaml | 6 ---- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index ad2c1e93e..c569fa46d 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -1,10 +1,13 @@ package v1alpha1 import ( + "bytes" + console "github.com/pluralsh/console-client-go" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/json" ) func init() { @@ -207,12 +210,12 @@ type ClusterGCPCloudSettings struct { // +kubebuilder:validation:Type:=string Project string `json:"project"` - // TODO: Add docs. + // Network in GCP to use when creating the cluster. // +kubebuilder:validation:Required // +kubebuilder:validation:Type:=string Network string `json:"network"` - // TODO: Add docs. + // Region in GCP to deploy cluster to. // +kubebuilder:validation:Required // +kubebuilder:validation:Type:=string Region string `json:"region"` @@ -270,15 +273,24 @@ func (np *ClusterNodePool) Attributes() *console.NodePoolAttributes { return nil } - return &console.NodePoolAttributes{ - Name: np.Name, - MinSize: np.MinSize, - MaxSize: np.MaxSize, - InstanceType: np.InstanceType, - Labels: nil, // TODO - Taints: nil, // TODO + attrs := &console.NodePoolAttributes{ + Name: np.Name, + MinSize: np.MinSize, + MaxSize: np.MaxSize, + InstanceType: np.InstanceType, + Taints: algorithms.Map(np.Taints, + func(t Taint) *console.TaintAttributes { return t.Attributes() }), CloudSettings: nil, // TODO } + + if np.Labels != nil { + if marshalledLabels, err := json.Marshal(np.Labels); err == nil { // Ignoring errors. + labels := bytes.NewBuffer(marshalledLabels).String() + attrs.Labels = &labels + } + } + + return attrs } type ClusterNodePoolCloudSettings struct { @@ -295,7 +307,7 @@ type ClusterNodePoolAWSCloudSettings struct { } type ClusterStatus struct { - // Id from Console. + // ID from Console. // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string ID *string `json:"id,omitempty"` diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index e4e4c4cc4..d9fb509fb 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -20,15 +20,12 @@ type Bindings struct { } type Binding struct { - // TODO: Add docs. // +kubebuilder:validation:Optional ID *string `json:"id,omitempty"` - // TODO: Add docs. // +kubebuilder:validation:Optional UserID *string `json:"UserID,omitempty"` - // TODO: Add docs. // +kubebuilder:validation:Optional GroupID *string `json:"groupID,omitempty"` } @@ -58,5 +55,13 @@ type Taint struct { Value string `json:"value"` } +func (t *Taint) Attributes() *console.TaintAttributes { + return &console.TaintAttributes{ + Key: t.Key, + Value: t.Value, + Effect: string(t.Effect), + } +} + // TaintEffect is the effect for a Kubernetes taint. type TaintEffect string diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 44f4d548c..80b1be1cb 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -50,13 +50,10 @@ spec: items: properties: UserID: - description: 'TODO: Add docs.' type: string groupID: - description: 'TODO: Add docs.' type: string id: - description: 'TODO: Add docs.' type: string type: object type: array @@ -65,13 +62,10 @@ spec: items: properties: UserID: - description: 'TODO: Add docs.' type: string groupID: - description: 'TODO: Add docs.' type: string id: - description: 'TODO: Add docs.' type: string type: object type: array @@ -134,11 +128,19 @@ spec: gcp: description: GCP cluster customizations. properties: + network: + description: Network in GCP to use when creating the cluster. + type: string project: description: Project in GCP to deploy cluster to. type: string + region: + description: Region in GCP to deploy cluster to. + type: string required: + - network - project + - region type: object type: object x-kubernetes-map-type: atomic @@ -302,7 +304,7 @@ spec: cluster is using. type: string id: - description: Id from Console. + description: ID from Console. type: string kasURL: description: KasURL contains KAS URL. diff --git a/controller/config/crd/bases/deployments.plural.sh_services.yaml b/controller/config/crd/bases/deployments.plural.sh_services.yaml index 014cbbde6..2e7411d7e 100644 --- a/controller/config/crd/bases/deployments.plural.sh_services.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_services.yaml @@ -46,13 +46,10 @@ spec: items: properties: UserID: - description: 'TODO: Add docs.' type: string groupID: - description: 'TODO: Add docs.' type: string id: - description: 'TODO: Add docs.' type: string type: object type: array @@ -61,13 +58,10 @@ spec: items: properties: UserID: - description: 'TODO: Add docs.' type: string groupID: - description: 'TODO: Add docs.' type: string id: - description: 'TODO: Add docs.' type: string type: object type: array From 4ec7032e95592515a35919728da240983d140aa5 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 8 Dec 2023 16:23:10 +0100 Subject: [PATCH 029/198] update provider reconciler and logger management --- .../apis/deployments/v1alpha1/provider.go | 12 +++ controller/main.go | 12 +-- controller/pkg/client/console.go | 6 ++ controller/pkg/client/provider.go | 47 +++++++++ .../pkg/cluster_controller/reconciler.go | 6 +- controller/pkg/errors/base.go | 7 +- .../gitrepository_controller/controller.go | 4 +- .../pkg/provider_controller/controller.go | 95 +++++++++++++++++-- .../pkg/service_controller/controller.go | 8 +- controller/pkg/types/reconciler.go | 13 ++- 10 files changed, 179 insertions(+), 31 deletions(-) create mode 100644 controller/pkg/client/provider.go diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index bb9d6e35c..33a8dc3d1 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -88,3 +88,15 @@ type ProviderStatus struct { // +kubebuilder:validation:Optional ID *string `json:"id,omitempty"` } + +func (p *ProviderStatus) GetID() string { + if !p.HasID() { + return "" + } + + return *p.ID +} + +func (p *ProviderStatus) HasID() bool { + return p.ID != nil && len(*p.ID) > 0 +} diff --git a/controller/main.go b/controller/main.go index 5673c6053..7f9e16ded 100644 --- a/controller/main.go +++ b/controller/main.go @@ -4,13 +4,11 @@ import ( "flag" "fmt" "github.com/pluralsh/console/controller/pkg/types" - "go.uber.org/zap" "os" "strings" deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/log" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -22,7 +20,7 @@ import ( var ( scheme = runtime.NewScheme() - setupLog = log.Logger + setupLog = ctrl.Log.WithName("Setup") ) func init() { @@ -87,11 +85,11 @@ func main() { consoleClient := client.New(opt.consoleUrl, opt.consoleToken) controllers, err := opt.reconcilers.ToControllers(mgr, setupLog, consoleClient) if err != nil { - setupLog.Error(err) + setupLog.Error(err, "error when creating controllers") os.Exit(1) } - runOrDie(controllers, mgr, setupLog) + runOrDie(controllers, mgr) } func parseReconcilers(reconcilersStr string) (types.ReconcilerList, error) { @@ -113,10 +111,10 @@ func parseReconcilers(reconcilersStr string) (types.ReconcilerList, error) { return result, nil } -func runOrDie(controllers []types.Controller, mgr ctrl.Manager, logger *zap.SugaredLogger) { +func runOrDie(controllers []types.Controller, mgr ctrl.Manager) { for _, c := range controllers { if err := c.SetupWithManager(mgr); err != nil { - logger.Error(err, "unable to create controller") + setupLog.Error(err, "unable to setup controller") os.Exit(1) } } diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index b0be62d58..655b6ce7d 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -2,6 +2,7 @@ package client import ( "context" + gqlgenclient "github.com/Yamashou/gqlgenc/client" "net/http" console "github.com/pluralsh/console-client-go" @@ -36,6 +37,11 @@ type ConsoleClient interface { CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) ListClusters() (*console.ListClusters, error) DeleteCluster(id string) (*console.ClusterFragment, error) + CreateProvider(ctx context.Context, attributes console.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) + GetProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) + UpdateProvider(ctx context.Context, id string, attributes console.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) + DeleteProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) error + IsProviderExists(ctx context.Context, id string) bool } func New(url, token string) ConsoleClient { diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go new file mode 100644 index 000000000..bb94cf3cc --- /dev/null +++ b/controller/pkg/client/provider.go @@ -0,0 +1,47 @@ +package client + +import ( + "context" + gqlgenclient "github.com/Yamashou/gqlgenc/client" + gqlclient "github.com/pluralsh/console-client-go" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func (c *client) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + response, err := c.consoleClient.CreateClusterProvider(ctx, attributes, options...) + return response.CreateClusterProvider, err +} + +func (c *client) GetProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + response, err := c.consoleClient.GetClusterProvider(ctx, id, options...) + if err == nil && (response == nil || response.ClusterProvider == nil) { + return nil, errors.NewNotFound(schema.GroupResource{}, id) + } + + return response.ClusterProvider, err +} + +func (c *client) UpdateProvider(ctx context.Context, id string, attributes gqlclient.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + response, err := c.consoleClient.UpdateClusterProvider(ctx, id, attributes, options...) + if err == nil && (response == nil || response.UpdateClusterProvider == nil) { + return nil, errors.NewNotFound(schema.GroupResource{}, id) + } + + return response.UpdateClusterProvider, err +} + +func (c *client) DeleteProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) error { + _, err := c.consoleClient.DeleteClusterProvider(ctx, id, options...) + return err +} + +func (c *client) IsProviderExists(ctx context.Context, id string) bool { + _, err := c.GetProvider(ctx, id) + if errors.IsNotFound(err) { + return false + } + + // We are assuming that if there is an error, and it is not ErrorNotFound then provider does not exist. + return err == nil +} diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 80dcb6392..cba252486 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -3,6 +3,7 @@ package cluster_controller import ( "context" "fmt" + "github.com/go-logr/logr" "reflect" "time" @@ -11,7 +12,6 @@ import ( consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" - "go.uber.org/zap" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -30,7 +30,7 @@ const ( type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log *zap.SugaredLogger + Log logr.Logger Scheme *runtime.Scheme } @@ -70,7 +70,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } if provider.Status.ID == nil { - r.Log.Info(fmt.Errorf("provider does not have ID set yet")) + r.Log.Info("provider does not have ID set yet") return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } diff --git a/controller/pkg/errors/base.go b/controller/pkg/errors/base.go index ad5714a70..c96ead6fd 100644 --- a/controller/pkg/errors/base.go +++ b/controller/pkg/errors/base.go @@ -5,12 +5,15 @@ import ( "github.com/Yamashou/gqlgenc/client" ) -var ErrExpected = errors.New("this is a transient, expected error") - type KnownError string +func (k KnownError) String() string { + return string(k) +} + const ( ErrorNotFound KnownError = "could not find resource" + ErrExpected KnownError = "this is a transient, expected error" ) type wrappedErrorResponse struct { diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 19e9f9257..e48b9a157 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -3,6 +3,7 @@ package gitrepositorycontroller import ( "context" "fmt" + "github.com/go-logr/logr" "reflect" "time" @@ -11,7 +12,6 @@ import ( consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" - "go.uber.org/zap" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -41,7 +41,7 @@ type GitRepoCred struct { type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log *zap.SugaredLogger + Log logr.Logger } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { diff --git a/controller/pkg/provider_controller/controller.go b/controller/pkg/provider_controller/controller.go index 9ea8c6e06..ab6b7c784 100644 --- a/controller/pkg/provider_controller/controller.go +++ b/controller/pkg/provider_controller/controller.go @@ -2,13 +2,16 @@ package providercontroller import ( "context" + console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" - "go.uber.org/zap" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "time" ) // Reconciler reconciles a v1alpha1.Provider object. @@ -17,20 +20,100 @@ type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log *zap.SugaredLogger Scheme *runtime.Scheme } +const ( + RequeueAfter = 30 * time.Second + FinalizerName = "providers.deployments.plural.sh/finalizer" +) + +var ( + requeue = ctrl.Result{RequeueAfter: RequeueAfter} +) + // Reconcile ... // TODO: Add kubebuilder rbac annotation -func (p *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { - //TODO implement me - panic("implement me") +func (p *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := log.FromContext(ctx) + + // Read Provider CRD from the K8S API + var provider v1alpha1.Provider + if err := p.Get(ctx, req.NamespacedName, &provider); err != nil { + log.Error(err, "unable to fetch provider") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Handle resource deletion + ret, err := p.addOrRemoveFinalizer(ctx, provider) + if ret || err != nil { + return ctrl.Result{}, err + } + + // Sync Provider CRD with the Console API + apiProvider, err := p.createOrUpdateProvider(ctx, provider) + if err != nil { + log.Error(err, "unable to create or update provider") + return ctrl.Result{}, err + } + + // Sync back Provider ID to crd + provider.Status.ID = &apiProvider.ID + // TODO: update CRD + + return requeue, nil +} + +func (p *Reconciler) createOrUpdateProvider(ctx context.Context, provider v1alpha1.Provider) (*console.ClusterProviderFragment, error) { + // TODO: Read credential secrets and attributes + + if provider.Status.HasID() && p.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { + return p.ConsoleClient.UpdateProvider(ctx, provider.Status.GetID(), console.ClusterProviderUpdateAttributes{}) + } + + return p.ConsoleClient.CreateProvider(ctx, console.ClusterProviderAttributes{}) +} + +func (p *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (bool, error) { + // If 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 + // to registering our finalizer. + if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&provider, FinalizerName) { + controllerutil.AddFinalizer(&provider, FinalizerName) + if err := p.Update(ctx, &provider); err != nil { + return true, err + } + } + + // If object is being deleted + if !provider.ObjectMeta.DeletionTimestamp.IsZero() { + // Remove Provider from Console API if it exists + if p.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { + if err := p.ConsoleClient.DeleteProvider(ctx, provider.Status.GetID()); err != nil { + // if fail to delete the external dependency here, return with error + // so that it can be retried. + return true, err + } + } + + // If our finalizer is present, remove it + if controllerutil.ContainsFinalizer(&provider, FinalizerName) { + controllerutil.RemoveFinalizer(&provider, FinalizerName) + if err := p.Update(ctx, &provider); err != nil { + return true, err + } + } + + // Stop reconciliation as the item is being deleted + return true, nil + } + + return false, nil } // SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. func (p *Reconciler) SetupWithManager(mgr ctrl.Manager) error { - p.Log.Infow("starting reconciler", "reconciler", "provider_reconciler") + mgr.GetLogger().Info("starting reconciler", "reconciler", "provider_reconciler") return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Provider{}). Complete(p) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index b1f684b20..8856acecb 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,13 +2,13 @@ package servicecontroller import ( "context" + "github.com/go-logr/logr" "reflect" "time" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" - "go.uber.org/zap" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -20,7 +20,7 @@ import ( type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log *zap.SugaredLogger + Log logr.Logger } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -36,7 +36,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { return ctrl.Result{}, err } - if cluster.Status.Id == nil { + if cluster.Status.ID == nil { r.Log.Info("Cluster is not ready", service.Spec.ClusterRef.Name) return ctrl.Result{ // update status @@ -63,7 +63,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, nil } - existingService, err := r.ConsoleClient.GetService(*cluster.Status.Id, service.Name) + existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, err diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 4360b9f5b..84361c301 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -2,13 +2,13 @@ package types import ( "fmt" + "github.com/go-logr/logr" "github.com/pluralsh/console/controller/pkg/client" clustercontroller "github.com/pluralsh/console/controller/pkg/cluster_controller" gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" providercontroller "github.com/pluralsh/console/controller/pkg/provider_controller" servicecontroller "github.com/pluralsh/console/controller/pkg/service_controller" - "go.uber.org/zap" ctrl "sigs.k8s.io/controller-runtime" ) @@ -36,34 +36,33 @@ func ToReconciler(reconciler string) (Reconciler, error) { } // ToController creates Controller instance based on this Reconciler. -func (sc Reconciler) ToController(mgr ctrl.Manager, logger *zap.SugaredLogger, consoleClient client.ConsoleClient) (Controller, error) { +func (sc Reconciler) ToController(mgr ctrl.Manager, logger logr.Logger, consoleClient client.ConsoleClient) (Controller, error) { unsupported := fmt.Errorf("reconciler %q is not supported", sc) switch sc { case GitRepositoryReconciler: return &gitrepositorycontroller.Reconciler{ Client: mgr.GetClient(), - Log: logger, + Log: mgr.GetLogger(), ConsoleClient: consoleClient, }, nil case ServiceDeploymentReconciler: return &servicecontroller.Reconciler{ Client: mgr.GetClient(), - Log: logger, + Log: mgr.GetLogger(), ConsoleClient: consoleClient, }, nil case ClusterReconciler: return &clustercontroller.Reconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, - Log: logger, + Log: mgr.GetLogger(), Scheme: mgr.GetScheme(), }, nil case ProviderReconciler: return &providercontroller.Reconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, - Log: logger, Scheme: mgr.GetScheme(), }, nil default: @@ -82,7 +81,7 @@ func Reconcilers() ReconcilerList { } // ToControllers returns a list of Controller instances based on this Reconciler array. -func (rl ReconcilerList) ToControllers(mgr ctrl.Manager, logger *zap.SugaredLogger, consoleClient client.ConsoleClient) ([]Controller, error) { +func (rl ReconcilerList) ToControllers(mgr ctrl.Manager, logger logr.Logger, consoleClient client.ConsoleClient) ([]Controller, error) { result := make([]Controller, len(rl)) for i, r := range rl { controller, err := r.ToController(mgr, logger, consoleClient) From d4c967a59b0f191a90fb5ff9a60d688d8cc4498e Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 16:23:35 +0100 Subject: [PATCH 030/198] update cluster node pool cloud attributes --- .../apis/deployments/v1alpha1/cluster.go | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index c569fa46d..b51ad1df5 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -274,13 +274,13 @@ func (np *ClusterNodePool) Attributes() *console.NodePoolAttributes { } attrs := &console.NodePoolAttributes{ - Name: np.Name, - MinSize: np.MinSize, - MaxSize: np.MaxSize, - InstanceType: np.InstanceType, + Name: np.Name, + MinSize: np.MinSize, + MaxSize: np.MaxSize, + InstanceType: np.InstanceType, + CloudSettings: np.CloudSettings.Attributes(), Taints: algorithms.Map(np.Taints, func(t Taint) *console.TaintAttributes { return t.Attributes() }), - CloudSettings: nil, // TODO } if np.Labels != nil { @@ -299,6 +299,16 @@ type ClusterNodePoolCloudSettings struct { AWS *ClusterNodePoolAWSCloudSettings `json:"aws,omitempty"` } +func (cs *ClusterNodePoolCloudSettings) Attributes() *console.NodePoolCloudAttributes { + if cs == nil { + return nil + } + + return &console.NodePoolCloudAttributes{ + Aws: cs.AWS.Attributes(), + } +} + type ClusterNodePoolAWSCloudSettings struct { // LaunchTemplateId is an ID of custom launch template for your nodes. Useful for Golden AMI setups. // +kubebuilder:validation:Optional @@ -306,6 +316,16 @@ type ClusterNodePoolAWSCloudSettings struct { LaunchTemplateId *string `json:"launchTemplateId,omitempty"` } +func (cs *ClusterNodePoolAWSCloudSettings) Attributes() *console.AwsNodeCloudAttributes { + if cs == nil { + return nil + } + + return &console.AwsNodeCloudAttributes{ + LaunchTemplateID: cs.LaunchTemplateId, + } +} + type ClusterStatus struct { // ID from Console. // +kubebuilder:validation:Optional From bcbce5a22688fd440ff4565e8880dce1a473f87c Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 8 Dec 2023 16:28:47 +0100 Subject: [PATCH 031/198] rename provider controller to reconciler --- controller/pkg/cluster_controller/reconciler.go | 6 +++--- .../reconciler.go} | 4 ++-- controller/pkg/service_controller/controller.go | 4 ++-- controller/pkg/types/reconciler.go | 11 +++++------ 4 files changed, 12 insertions(+), 13 deletions(-) rename controller/pkg/{provider_controller/controller.go => provider_reconciler/reconciler.go} (98%) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index f4fff47bd..57c1e09a9 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -3,6 +3,7 @@ package cluster_controller import ( "context" "fmt" + "github.com/go-logr/logr" "reflect" "time" @@ -10,7 +11,6 @@ import ( consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" - "go.uber.org/zap" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" @@ -28,7 +28,7 @@ const ( type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log *zap.SugaredLogger + Log logr.Logger Scheme *runtime.Scheme } @@ -64,7 +64,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } if provider.Status.ID == nil { - r.Log.Info(fmt.Errorf("provider does not have ID set yet")) + r.Log.Info("provider does not have ID set yet") return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } diff --git a/controller/pkg/provider_controller/controller.go b/controller/pkg/provider_reconciler/reconciler.go similarity index 98% rename from controller/pkg/provider_controller/controller.go rename to controller/pkg/provider_reconciler/reconciler.go index ab6b7c784..e1f7272f3 100644 --- a/controller/pkg/provider_controller/controller.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -1,4 +1,4 @@ -package providercontroller +package providerreconciler import ( "context" @@ -59,7 +59,7 @@ func (p *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco // Sync back Provider ID to crd provider.Status.ID = &apiProvider.ID - // TODO: update CRD + // TODO: update CRD status return requeue, nil } diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index b92b06678..0e70e4eeb 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,6 +2,7 @@ package servicecontroller import ( "context" + "github.com/go-logr/logr" "reflect" "time" @@ -10,7 +11,6 @@ import ( consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" - "go.uber.org/zap" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" @@ -28,7 +28,7 @@ const ( type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log *zap.SugaredLogger + Log logr.Logger } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 84361c301..c9d507e4e 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -2,12 +2,11 @@ package types import ( "fmt" - "github.com/go-logr/logr" + providerreconciler "github.com/pluralsh/console/controller/pkg/provider_reconciler" "github.com/pluralsh/console/controller/pkg/client" clustercontroller "github.com/pluralsh/console/controller/pkg/cluster_controller" gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" - providercontroller "github.com/pluralsh/console/controller/pkg/provider_controller" servicecontroller "github.com/pluralsh/console/controller/pkg/service_controller" ctrl "sigs.k8s.io/controller-runtime" ) @@ -36,7 +35,7 @@ func ToReconciler(reconciler string) (Reconciler, error) { } // ToController creates Controller instance based on this Reconciler. -func (sc Reconciler) ToController(mgr ctrl.Manager, logger logr.Logger, consoleClient client.ConsoleClient) (Controller, error) { +func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.ConsoleClient) (Controller, error) { unsupported := fmt.Errorf("reconciler %q is not supported", sc) switch sc { @@ -60,7 +59,7 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, logger logr.Logger, consoleC Scheme: mgr.GetScheme(), }, nil case ProviderReconciler: - return &providercontroller.Reconciler{ + return &providerreconciler.Reconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), @@ -81,10 +80,10 @@ func Reconcilers() ReconcilerList { } // ToControllers returns a list of Controller instances based on this Reconciler array. -func (rl ReconcilerList) ToControllers(mgr ctrl.Manager, logger logr.Logger, consoleClient client.ConsoleClient) ([]Controller, error) { +func (rl ReconcilerList) ToControllers(mgr ctrl.Manager, consoleClient client.ConsoleClient) ([]Controller, error) { result := make([]Controller, len(rl)) for i, r := range rl { - controller, err := r.ToController(mgr, logger, consoleClient) + controller, err := r.ToController(mgr, consoleClient) if err != nil { return nil, err } From 05ee6234cec7551f3f8b8d7ae72909506d3cc1eb Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 8 Dec 2023 16:29:23 +0100 Subject: [PATCH 032/198] fix main --- controller/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/main.go b/controller/main.go index 7f9e16ded..ff8d027d1 100644 --- a/controller/main.go +++ b/controller/main.go @@ -83,7 +83,7 @@ func main() { } consoleClient := client.New(opt.consoleUrl, opt.consoleToken) - controllers, err := opt.reconcilers.ToControllers(mgr, setupLog, consoleClient) + controllers, err := opt.reconcilers.ToControllers(mgr, consoleClient) if err != nil { setupLog.Error(err, "error when creating controllers") os.Exit(1) From 2eecc3799f5d75c9056d02db216f330e61c10938 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 16:44:05 +0100 Subject: [PATCH 033/198] add cluster update attributes --- controller/apis/deployments/v1alpha1/cluster.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index b51ad1df5..59d85c258 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -73,6 +73,16 @@ func (c *Cluster) Attributes(providerId *string) console.ClusterAttributes { return attrs } +func (c *Cluster) UpdateAttributes() console.ClusterUpdateAttributes { + return console.ClusterUpdateAttributes{ + Handle: c.Spec.Handle, + Version: c.Spec.Version, + Protect: c.Spec.Protect, + NodePools: algorithms.Map(c.Spec.NodePools, + func(np ClusterNodePool) *console.NodePoolAttributes { return np.Attributes() }), + } +} + type ClusterSpec struct { // Handle is a short, unique human-readable name used to identify this cluster. // Does not necessarily map to the cloud resource name. From 975fec620ba41e58fb20fae9f11a06d53031b108 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 16:46:42 +0100 Subject: [PATCH 034/198] sort imports --- controller/pkg/cluster_controller/reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 57c1e09a9..2fba61638 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -3,10 +3,10 @@ package cluster_controller import ( "context" "fmt" - "github.com/go-logr/logr" "reflect" "time" + "github.com/go-logr/logr" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" From b8c382635c890f4218cef101763add499d72e4a8 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 8 Dec 2023 17:00:45 +0100 Subject: [PATCH 035/198] remove invalid validations --- controller/apis/deployments/v1alpha1/cluster.go | 7 ------- .../bases/deployments.plural.sh_clusters.yaml | 16 ---------------- 2 files changed, 23 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 59d85c258..7ce581444 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -31,13 +31,6 @@ type Cluster struct { metav1.ObjectMeta `json:"metadata,omitempty"` - // +kubebuilder:validation:XValidation:rule="self.cloud == 'aws' && (has(self.cloudSettings.aws)",message="AWS cloud settings are required" - // +kubebuilder:validation:XValidation:rule="self.cloud == 'azure' && (has(self.cloudSettings.azure)",message="Azure cloud settings are required" - // +kubebuilder:validation:XValidation:rule="self.cloud == 'gcp' && (has(self.cloudSettings.gcp)",message="GCP cloud settings are required" - // +kubebuilder:validation:XValidation:rule="self.cloud == 'aws' && (!has(self.cloudSettings.azure) && !has(self.cloudSettings.gcp)",message="Only AWS cloud settings can be specified" - // +kubebuilder:validation:XValidation:rule="self.cloud == 'azure' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.gcp)",message="Only Azure cloud settings can be specified" - // +kubebuilder:validation:XValidation:rule="self.cloud == 'gcp' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure)",message="Only GCP cloud settings can be specified" - // +kubebuilder:validation:XValidation:rule="self.cloud == 'byok' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure) && !has(self.cloudSettings.gcp))",message="Cloud settings can't be specified for BYOK" Spec ClusterSpec `json:"spec,omitempty"` Status ClusterStatus `json:"status,omitempty"` diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 80b1be1cb..6ef5b50f3 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -281,22 +281,6 @@ spec: required: - cloud type: object - x-kubernetes-validations: - - message: AWS cloud settings are required - rule: self.cloud == 'aws' && (has(self.cloudSettings.aws) - - message: Azure cloud settings are required - rule: self.cloud == 'azure' && (has(self.cloudSettings.azure) - - message: GCP cloud settings are required - rule: self.cloud == 'gcp' && (has(self.cloudSettings.gcp) - - message: Only AWS cloud settings can be specified - rule: self.cloud == 'aws' && (!has(self.cloudSettings.azure) && !has(self.cloudSettings.gcp) - - message: Only Azure cloud settings can be specified - rule: self.cloud == 'azure' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.gcp) - - message: Only GCP cloud settings can be specified - rule: self.cloud == 'gcp' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure) - - message: Cloud settings can't be specified for BYOK - rule: self.cloud == 'byok' && (!has(self.cloudSettings.aws) && !has(self.cloudSettings.azure) - && !has(self.cloudSettings.gcp)) status: properties: currentVersion: From cd8ef13c896b73f3d442ad0fc068695d78a02af8 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Mon, 11 Dec 2023 08:55:42 +0100 Subject: [PATCH 036/198] add hash function --- controller/pkg/service_controller/controller.go | 4 +++- controller/pkg/utils/sha.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 controller/pkg/utils/sha.go diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 0e70e4eeb..0ad92e8b9 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -90,6 +90,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + return ctrl.Result{}, err + } } err = r.updateReferences(ctx, service) if err != nil { @@ -164,7 +167,6 @@ func (r *Reconciler) genCreateAttr(ctx context.Context, service *v1alpha1.Servic Value: &value, }) } - } if service.Spec.Helm != nil { attr.Helm = &console.HelmConfigAttributes{ diff --git a/controller/pkg/utils/sha.go b/controller/pkg/utils/sha.go new file mode 100644 index 000000000..481b5de25 --- /dev/null +++ b/controller/pkg/utils/sha.go @@ -0,0 +1,16 @@ +package utils + +import ( + "crypto/sha256" + "encoding/base32" + "encoding/json" +) + +func HashObject(any interface{}) (string, error) { + out, err := json.Marshal(any) + if err != nil { + return "", err + } + sha := sha256.Sum256(out) + return base32.StdEncoding.EncodeToString(sha[:]), nil +} From 1557c4808cc129fca52cada26e8328bd937ab91a Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Mon, 11 Dec 2023 09:54:28 +0100 Subject: [PATCH 037/198] add sha for git repository and service --- .../deployments/v1alpha1/git_repository.go | 2 + .../apis/deployments/v1alpha1/service.go | 2 + ...deployments.plural.sh_gitrepositories.yaml | 2 + .../bases/deployments.plural.sh_services.yaml | 2 + .../pkg/service_controller/controller.go | 103 ++++++++++++++---- 5 files changed, 88 insertions(+), 23 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 5e4ad2e8b..6f66b0218 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -36,6 +36,8 @@ type GitRepositoryStatus struct { // Id of repo in console. // +optional Id *string `json:"id,omitempty"` + // +optional + Sha string `json:"sha,omitempty"` } // +kubebuilder:object:root=true diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index a001efec0..735b98e8b 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -75,6 +75,8 @@ type ServiceStatus struct { Errors []ServiceError `json:"errors,omitempty"` // +optional Components []ServiceComponent `json:"components,omitempty"` + // +optional + Sha string `json:"sha,omitempty"` } type ServiceError struct { diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index df8ac63ac..acd9b8e3c 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -78,6 +78,8 @@ spec: message: description: Message indicating details about last transition. type: string + sha: + type: string type: object type: object served: true diff --git a/controller/config/crd/bases/deployments.plural.sh_services.yaml b/controller/config/crd/bases/deployments.plural.sh_services.yaml index 2e7411d7e..a24a482a9 100644 --- a/controller/config/crd/bases/deployments.plural.sh_services.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_services.yaml @@ -338,6 +338,8 @@ spec: id: description: Id of service in console. type: string + sha: + type: string type: object type: object served: true diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 0ad92e8b9..2fb1802f7 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,15 +2,17 @@ package servicecontroller import ( "context" - "github.com/go-logr/logr" "reflect" "time" + "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" + "github.com/pluralsh/console/controller/pkg/utils" + "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" @@ -71,6 +73,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, nil } + attr, err := r.genServiceAttributes(ctx, service, repository.Status.Id) + if err != nil { + return ctrl.Result{}, err + } + sha, err := utils.HashObject(attr) + if err != nil { + return ctrl.Result{}, err + } + existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil { if !errors.IsNotFound(err) { @@ -78,10 +89,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } } if existingService == nil { - attr, err := r.genCreateAttr(ctx, service, repository.Status.Id) - if err != nil { - return ctrl.Result{}, err - } _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) if err != nil { return ctrl.Result{}, err @@ -94,11 +101,50 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } } - err = r.updateReferences(ctx, service) + err = r.addReferenceFinalizers(ctx, service) if err != nil { return ctrl.Result{}, err } + if service.Status.Sha != "" && service.Status.Sha != sha { + // update service + + } + + if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.Service) { + r.Status.Id = &existingService.ID + r.Status.Sha = sha + if existingService.Errors != nil { + r.Status.Errors = algorithms.Map(existingService.Errors, + func(b *console.ErrorFragment) v1alpha1.ServiceError { + return v1alpha1.ServiceError{ + Source: b.Source, + Message: b.Message, + } + }) + } + r.Status.Components = make([]v1alpha1.ServiceComponent, 0) + for _, c := range existingService.Components { + sc := v1alpha1.ServiceComponent{ + ID: c.ID, + Name: c.Name, + Group: c.Group, + Kind: c.Kind, + Namespace: c.Namespace, + Synced: c.Synced, + Version: c.Version, + } + if c.State != nil { + state := v1alpha1.ComponentState(*c.State) + sc.State = &state + } + r.Status.Components = append(r.Status.Components, sc) + } + + }); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{ // update status RequeueAfter: 30 * time.Second, @@ -112,7 +158,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *Reconciler) genCreateAttr(ctx context.Context, service *v1alpha1.Service, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { +func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.Service, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { attr := &console.ServiceDeploymentAttributes{ Name: service.Name, Namespace: service.Namespace, @@ -124,21 +170,10 @@ func (r *Reconciler) genCreateAttr(ctx context.Context, service *v1alpha1.Servic if service.Spec.Bindings != nil { attr.ReadBindings = make([]*console.PolicyBindingAttributes, 0) attr.WriteBindings = make([]*console.PolicyBindingAttributes, 0) - - for _, r := range service.Spec.Bindings.Read { - attr.ReadBindings = append(attr.ReadBindings, &console.PolicyBindingAttributes{ - ID: r.ID, - UserID: r.UserID, - GroupID: r.GroupID, - }) - } - for _, w := range service.Spec.Bindings.Write { - attr.WriteBindings = append(attr.WriteBindings, &console.PolicyBindingAttributes{ - ID: w.ID, - UserID: w.UserID, - GroupID: w.GroupID, - }) - } + attr.ReadBindings = algorithms.Map(service.Spec.Bindings.Read, + func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + attr.WriteBindings = algorithms.Map(service.Spec.Bindings.Write, + func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) } if service.Spec.Kustomize != nil { @@ -198,7 +233,7 @@ func (r *Reconciler) genCreateAttr(ctx context.Context, service *v1alpha1.Servic return attr, nil } -func (r *Reconciler) updateReferences(ctx context.Context, service *v1alpha1.Service) error { +func (r *Reconciler) addReferenceFinalizers(ctx context.Context, service *v1alpha1.Service) error { if service.Spec.ConfigurationRef != nil { configurationSecret, err := kubernetes.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) if err != nil { @@ -208,6 +243,28 @@ func (r *Reconciler) updateReferences(ctx context.Context, service *v1alpha1.Ser return err } } + if service.Spec.Helm.ValuesRef != nil { + configMap := &corev1.ConfigMap{} + name := types.NamespacedName{Name: service.Spec.Helm.ValuesRef.Name, Namespace: service.Namespace} + err := r.Get(ctx, name, configMap) + if err != nil { + return err + } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, configMap, ServiceFinalizer); err != nil { + return err + } + } + if service.Spec.Helm.ChartRef != nil { + configMap := &corev1.ConfigMap{} + name := types.NamespacedName{Name: service.Spec.Helm.ChartRef.Name, Namespace: service.Namespace} + err := r.Get(ctx, name, configMap) + if err != nil { + return err + } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, configMap, ServiceFinalizer); err != nil { + return err + } + } return nil } From 6ae2ce9cc8ec23e57fb05e2a0e936c98d14564e9 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Mon, 11 Dec 2023 10:23:01 +0100 Subject: [PATCH 038/198] add sha to git repo --- controller/pkg/client/console.go | 1 + controller/pkg/client/service.go | 8 + .../gitrepository_controller/controller.go | 27 +- .../pkg/service_controller/controller.go | 13 +- controller/pkg/test/mocks/ConsoleClient.go | 344 +++++++++++++++++- 5 files changed, 379 insertions(+), 14 deletions(-) diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 655b6ce7d..aee7d2a7b 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -42,6 +42,7 @@ type ConsoleClient interface { UpdateProvider(ctx context.Context, id string, attributes console.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) DeleteProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) error IsProviderExists(ctx context.Context, id string) bool + UpdateService(serviceId string, attributes console.ServiceUpdateAttributes) error } func New(url, token string) ConsoleClient { diff --git a/controller/pkg/client/service.go b/controller/pkg/client/service.go index 0dbf42ae3..eaf801045 100644 --- a/controller/pkg/client/service.go +++ b/controller/pkg/client/service.go @@ -40,6 +40,14 @@ func (c *client) CreateService(clusterId *string, attributes console.ServiceDepl } +func (c *client) UpdateService(serviceId string, attributes console.ServiceUpdateAttributes) error { + _, err := c.consoleClient.UpdateServiceDeployment(c.ctx, serviceId, attributes) + if err != nil { + return err + } + return nil +} + func (c *client) UpdateComponents(id string, components []*console.ComponentAttributes, errs []*console.ServiceErrorAttributes) error { _, err := c.consoleClient.UpdateServiceComponents(c.ctx, id, components, errs) return err diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index e48b9a157..e4cd369d6 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/go-logr/logr" + "github.com/pluralsh/console/controller/pkg/utils" "reflect" "time" @@ -55,6 +56,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if !repo.GetDeletionTimestamp().IsZero() { return r.handleDelete(ctx, repo) } + cred, err := r.getRepositoryCredentials(ctx, repo) + if err != nil { + return ctrl.Result{}, err + } + sha, err := utils.HashObject(cred) + if err != nil { + return ctrl.Result{}, err + } existingRepo, err := r.getRepository(repo.Spec.Url) if err != nil { @@ -63,10 +72,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } } if existingRepo == nil { - cred, err := r.getRepositoryCredentials(ctx, repo) - if err != nil { - return ctrl.Result{}, err - } resp, err := r.ConsoleClient.CreateRepository(repo.Spec.Url, cred.PrivateKey, cred.Passphrase, cred.Username, cred.Password) if err != nil { return ctrl.Result{}, err @@ -77,12 +82,26 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } + if repo.Status.Sha != "" && repo.Status.Sha != sha { + _, err := r.ConsoleClient.UpdateRepository(existingRepo.ID, console.GitAttributes{ + URL: repo.Spec.Url, + PrivateKey: cred.PrivateKey, + Passphrase: cred.Passphrase, + Username: cred.Username, + Password: cred.Password, + }) + if err != nil { + return ctrl.Result{}, err + } + } + if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { r.Status.Message = existingRepo.Error r.Status.Id = &existingRepo.ID if existingRepo.Health != nil { r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } + r.Status.Sha = sha }); err != nil { return ctrl.Result{}, err diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 2fb1802f7..3904ec4c4 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -77,6 +77,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } + sha, err := utils.HashObject(attr) if err != nil { return ctrl.Result{}, err @@ -108,7 +109,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if service.Status.Sha != "" && service.Status.Sha != sha { // update service - + updater := console.ServiceUpdateAttributes{ + Version: attr.Version, + Protect: attr.Protect, + Git: attr.Git, + Helm: attr.Helm, + Configuration: attr.Configuration, + Kustomize: attr.Kustomize, + } + if err := r.ConsoleClient.UpdateService(existingService.ID, updater); err != nil { + return ctrl.Result{}, err + } } if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.Service) { diff --git a/controller/pkg/test/mocks/ConsoleClient.go b/controller/pkg/test/mocks/ConsoleClient.go index 59b4db50b..c65fa2131 100644 --- a/controller/pkg/test/mocks/ConsoleClient.go +++ b/controller/pkg/test/mocks/ConsoleClient.go @@ -3,7 +3,11 @@ package mocks import ( + context "context" + + gqlgencclient "github.com/Yamashou/gqlgenc/client" gqlclient "github.com/pluralsh/console-client-go" + mock "github.com/stretchr/testify/mock" ) @@ -12,6 +16,73 @@ type ConsoleClient struct { mock.Mock } +// CreateCluster provides a mock function with given fields: attrs +func (_m *ConsoleClient) CreateCluster(attrs gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(attrs) + + if len(ret) == 0 { + panic("no return value specified for CreateCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error)); ok { + return rf(attrs) + } + if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) *gqlclient.ClusterFragment); ok { + r0 = rf(attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(gqlclient.ClusterAttributes) error); ok { + r1 = rf(attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateProvider provides a mock function with given fields: ctx, attributes, options +func (_m *ConsoleClient) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, attributes) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateProvider") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, attributes, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, attributes, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, attributes, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateRepository provides a mock function with given fields: url, privateKey, passphrase, username, password func (_m *ConsoleClient) CreateRepository(url string, privateKey *string, passphrase *string, username *string, password *string) (*gqlclient.CreateGitRepository, error) { ret := _m.Called(url, privateKey, passphrase, username, password) @@ -42,6 +113,91 @@ func (_m *ConsoleClient) CreateRepository(url string, privateKey *string, passph return r0, r1 } +// CreateService provides a mock function with given fields: clusterId, attributes +func (_m *ConsoleClient) CreateService(clusterId *string, attributes gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + ret := _m.Called(clusterId, attributes) + + if len(ret) == 0 { + panic("no return value specified for CreateService") + } + + var r0 *gqlclient.ServiceDeploymentFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string, gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error)); ok { + return rf(clusterId, attributes) + } + if rf, ok := ret.Get(0).(func(*string, gqlclient.ServiceDeploymentAttributes) *gqlclient.ServiceDeploymentFragment); ok { + r0 = rf(clusterId, attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string, gqlclient.ServiceDeploymentAttributes) error); ok { + r1 = rf(clusterId, attributes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteCluster provides a mock function with given fields: id +func (_m *ConsoleClient) DeleteCluster(id string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for DeleteCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(string) (*gqlclient.ClusterFragment, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(string) *gqlclient.ClusterFragment); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteProvider provides a mock function with given fields: ctx, id, options +func (_m *ConsoleClient) DeleteProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteProvider") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error); ok { + r0 = rf(ctx, id, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteRepository provides a mock function with given fields: id func (_m *ConsoleClient) DeleteRepository(id string) error { ret := _m.Called(id) @@ -60,6 +216,73 @@ func (_m *ConsoleClient) DeleteRepository(id string) error { return r0 } +// GetCluster provides a mock function with given fields: id +func (_m *ConsoleClient) GetCluster(id *string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for GetCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string) (*gqlclient.ClusterFragment, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(*string) *gqlclient.ClusterFragment); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProvider provides a mock function with given fields: ctx, id, options +func (_m *ConsoleClient) GetProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetProvider") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, id, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, id, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, id, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRepository provides a mock function with given fields: url func (_m *ConsoleClient) GetRepository(url *string) (*gqlclient.GetGitRepository, error) { ret := _m.Called(url) @@ -90,9 +313,9 @@ func (_m *ConsoleClient) GetRepository(url *string) (*gqlclient.GetGitRepository return r0, r1 } -// GetService provides a mock function with given fields: id -func (_m *ConsoleClient) GetService(id string) (*gqlclient.ServiceDeploymentExtended, error) { - ret := _m.Called(id) +// GetService provides a mock function with given fields: clusterID, serviceName +func (_m *ConsoleClient) GetService(clusterID string, serviceName string) (*gqlclient.ServiceDeploymentExtended, error) { + ret := _m.Called(clusterID, serviceName) if len(ret) == 0 { panic("no return value specified for GetService") @@ -100,19 +323,19 @@ func (_m *ConsoleClient) GetService(id string) (*gqlclient.ServiceDeploymentExte var r0 *gqlclient.ServiceDeploymentExtended var r1 error - if rf, ok := ret.Get(0).(func(string) (*gqlclient.ServiceDeploymentExtended, error)); ok { - return rf(id) + if rf, ok := ret.Get(0).(func(string, string) (*gqlclient.ServiceDeploymentExtended, error)); ok { + return rf(clusterID, serviceName) } - if rf, ok := ret.Get(0).(func(string) *gqlclient.ServiceDeploymentExtended); ok { - r0 = rf(id) + if rf, ok := ret.Get(0).(func(string, string) *gqlclient.ServiceDeploymentExtended); ok { + r0 = rf(clusterID, serviceName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*gqlclient.ServiceDeploymentExtended) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(id) + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(clusterID, serviceName) } else { r1 = ret.Error(1) } @@ -150,6 +373,54 @@ func (_m *ConsoleClient) GetServices() ([]*gqlclient.ServiceDeploymentBaseFragme return r0, r1 } +// IsProviderExists provides a mock function with given fields: ctx, id +func (_m *ConsoleClient) IsProviderExists(ctx context.Context, id string) bool { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for IsProviderExists") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ListClusters provides a mock function with given fields: +func (_m *ConsoleClient) ListClusters() (*gqlclient.ListClusters, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListClusters") + } + + var r0 *gqlclient.ListClusters + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListClusters, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListClusters); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListClusters) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListRepositories provides a mock function with given fields: func (_m *ConsoleClient) ListRepositories() (*gqlclient.ListGitRepositories, error) { ret := _m.Called() @@ -198,6 +469,43 @@ func (_m *ConsoleClient) UpdateComponents(id string, components []*gqlclient.Com return r0 } +// UpdateProvider provides a mock function with given fields: ctx, id, attributes, options +func (_m *ConsoleClient) UpdateProvider(ctx context.Context, id string, attributes gqlclient.ClusterProviderUpdateAttributes, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id, attributes) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for UpdateProvider") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, id, attributes, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, id, attributes, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, id, attributes, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateRepository provides a mock function with given fields: id, attrs func (_m *ConsoleClient) UpdateRepository(id string, attrs gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error) { ret := _m.Called(id, attrs) @@ -228,6 +536,24 @@ func (_m *ConsoleClient) UpdateRepository(id string, attrs gqlclient.GitAttribut return r0, r1 } +// UpdateService provides a mock function with given fields: serviceId, attributes +func (_m *ConsoleClient) UpdateService(serviceId string, attributes gqlclient.ServiceUpdateAttributes) error { + ret := _m.Called(serviceId, attributes) + + if len(ret) == 0 { + panic("no return value specified for UpdateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, gqlclient.ServiceUpdateAttributes) error); ok { + r0 = rf(serviceId, attributes) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewConsoleClient creates a new instance of ConsoleClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewConsoleClient(t interface { From a4ef594b3f0ef83f9ab48ffe24b73bd936686ff6 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 11 Dec 2023 11:29:52 +0100 Subject: [PATCH 039/198] sync cluster --- .../apis/deployments/v1alpha1/cluster.go | 5 ++++ .../bases/deployments.plural.sh_clusters.yaml | 3 +++ controller/pkg/client/cluster.go | 9 +++++++ controller/pkg/client/console.go | 4 ++- .../pkg/cluster_controller/reconciler.go | 26 +++++++++++++++---- .../gitrepository_controller/controller.go | 11 ++++---- 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 7ce581444..6af2add3b 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -335,6 +335,11 @@ type ClusterStatus struct { // +kubebuilder:validation:Type:=string ID *string `json:"id,omitempty"` + // SHA of last applied configuration. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + SHA *string `json:"sha,omitempty"` + // CurrentVersion contains current Kubernetes version this cluster is using. // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 6ef5b50f3..10f9bc800 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -297,6 +297,9 @@ spec: description: PingedAt contains timestamp of last successful cluster ping. type: string + sha: + description: SHA of last applied configuration. + type: string type: object type: object served: true diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go index 774bc31f0..692d7d85e 100644 --- a/controller/pkg/client/cluster.go +++ b/controller/pkg/client/cluster.go @@ -30,6 +30,15 @@ func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.Cluste }, nil } +func (c *client) UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) { + response, err := c.consoleClient.UpdateCluster(c.ctx, id, attrs) + if err != nil { + return nil, err + } + + return response.UpdateCluster, nil +} + func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { response, err := c.consoleClient.GetCluster(c.ctx, id) if err != nil { diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index aee7d2a7b..635cd2aa7 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -2,9 +2,10 @@ package client import ( "context" - gqlgenclient "github.com/Yamashou/gqlgenc/client" "net/http" + gqlgenclient "github.com/Yamashou/gqlgenc/client" + console "github.com/pluralsh/console-client-go" ) @@ -35,6 +36,7 @@ type ConsoleClient interface { CreateService(clusterId *string, attributes console.ServiceDeploymentAttributes) (*console.ServiceDeploymentFragment, error) GetCluster(id *string) (*console.ClusterFragment, error) CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) + UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) ListClusters() (*console.ListClusters, error) DeleteCluster(id string) (*console.ClusterFragment, error) CreateProvider(ctx context.Context, attributes console.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 2fba61638..077bd8d48 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -11,6 +11,7 @@ import ( consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" + "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" @@ -52,10 +53,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) if err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, err // TODO: Error handling? + return ctrl.Result{}, err } - // TODO: Move? var providerId *string if cluster.Spec.Cloud != "byok" { provider, err := r.getProvider(ctx, cluster) @@ -68,26 +68,42 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } + err = controllerutil.SetOwnerReference(provider, cluster, r.Scheme) + if err != nil { + return ctrl.Result{}, err + } + // TODO Patch cluster. + providerId = provider.Status.ID } if apiCluster == nil { - // TODO: Set owner ref. - response, err := r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) + apiCluster, err = r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) if err != nil { return ctrl.Result{}, err } - apiCluster = response } if err := kubernetes.TryAddFinalizer(ctx, r.Client, cluster, ClusterFinalizer); err != nil { return ctrl.Result{}, err } + sha, err := utils.HashObject(cluster.UpdateAttributes()) + if err != nil { + return ctrl.Result{}, err + } + if cluster.Status.ID != nil && cluster.Status.SHA != nil && cluster.Status.SHA != &sha { + apiCluster, err = r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) + if err != nil { + return ctrl.Result{}, err + } + } + if err := r.updateStatus(ctx, cluster, func(r *v1alpha1.Cluster) { r.Status.ID = &apiCluster.ID r.Status.KasURL = apiCluster.KasURL r.Status.CurrentVersion = apiCluster.CurrentVersion r.Status.PingedAt = apiCluster.PingedAt + r.Status.SHA = &sha }); err != nil { return ctrl.Result{}, err } diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index e4cd369d6..bdf2a4251 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -3,11 +3,12 @@ package gitrepositorycontroller import ( "context" "fmt" - "github.com/go-logr/logr" - "github.com/pluralsh/console/controller/pkg/utils" "reflect" "time" + "github.com/go-logr/logr" + "github.com/pluralsh/console/controller/pkg/utils" + console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" @@ -66,10 +67,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } existingRepo, err := r.getRepository(repo.Spec.Url) - if err != nil { - if !errors.IsNotFound(err) { - return ctrl.Result{}, err - } + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err } if existingRepo == nil { resp, err := r.ConsoleClient.CreateRepository(repo.Spec.Url, cred.PrivateKey, cred.Passphrase, cred.Username, cred.Password) From 6d960c16d0b4536a67926025cae57259a07bdab5 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 11 Dec 2023 13:35:45 +0100 Subject: [PATCH 040/198] add owner ref --- .../pkg/cluster_controller/reconciler.go | 19 +++++----- controller/pkg/utils/kubernetes.go | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 controller/pkg/utils/kubernetes.go diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 077bd8d48..c21d8255d 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -7,6 +7,7 @@ import ( "time" "github.com/go-logr/logr" + console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" @@ -51,9 +52,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return r.delete(ctx, cluster) } - apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) - if err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, err + var apiCluster *console.ClusterFragment + var err error + if cluster.Status.ID != nil { + apiCluster, err = r.ConsoleClient.GetCluster(cluster.Status.ID) + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } } var providerId *string @@ -63,18 +68,16 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - if provider.Status.ID == nil { + providerId = provider.Status.ID + if providerId == nil { r.Log.Info("provider does not have ID set yet") return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } - err = controllerutil.SetOwnerReference(provider, cluster, r.Scheme) + err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) if err != nil { return ctrl.Result{}, err } - // TODO Patch cluster. - - providerId = provider.Status.ID } if apiCluster == nil { diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go new file mode 100644 index 000000000..97b5c14b3 --- /dev/null +++ b/controller/pkg/utils/kubernetes.go @@ -0,0 +1,36 @@ +package utils + +import ( + "context" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, object ctrlruntimeclient.Object, scheme *runtime.Scheme) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(object), object); err != nil { + return err + } + + if owner.GetDeletionTimestamp() != nil || object.GetDeletionTimestamp() != nil { + return nil + } + + original := object.DeepCopyObject().(ctrlruntimeclient.Object) + + err := controllerutil.SetOwnerReference(owner, object, scheme) + if err != nil { + return err + } + + if reflect.DeepEqual(original.GetOwnerReferences(), object.GetOwnerReferences()) { + return nil + } + + return client.Patch(ctx, object, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) + }) +} From 0e453f84a7980dcc09269d98ed139349a5d58d10 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 11 Dec 2023 13:57:29 +0100 Subject: [PATCH 041/198] update codegen --- .../apis/deployments/v1alpha1/zz_generated.deepcopy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 94a0f0b3b..ebefb936f 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -388,6 +388,11 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = new(string) **out = **in } + if in.SHA != nil { + in, out := &in.SHA, &out.SHA + *out = new(string) + **out = **in + } if in.CurrentVersion != nil { in, out := &in.CurrentVersion, &out.CurrentVersion *out = new(string) From 55380bb6c7dfd75dea26a4fa7a76241a94d29580 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 11 Dec 2023 14:50:12 +0100 Subject: [PATCH 042/198] update examples --- controller/Makefile | 9 ++++++++- controller/config/crd/examples/cluster_aws.yaml | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index efd8215be..2bc352d61 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -56,9 +56,16 @@ docker-push: ## Push docker image with the driver. build: manifests generate fmt vet ## Build controller. go build -o bin/deployment-controller main.go -genmock: # generates mocks before running tests +genmock: ## generates mocks before running tests hack/gen-client-mocks.sh +apply-crds: ## applies CRDs + kubectl apply -f config/crd/bases + +apply-clusters: apply-crds ## applies cluster resources + kubectl apply -f config/crd/examples/cluster_byok.yaml + kubectl apply -f config/crd/examples/cluster_aws.yaml + # go-get-tool will 'go get' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) define go-get-tool diff --git a/controller/config/crd/examples/cluster_aws.yaml b/controller/config/crd/examples/cluster_aws.yaml index 150eeffa5..c8df9de0a 100644 --- a/controller/config/crd/examples/cluster_aws.yaml +++ b/controller/config/crd/examples/cluster_aws.yaml @@ -6,8 +6,9 @@ metadata: spec: handle: aws cloud: aws - version: 1.24 + version: "1.24" protect: false +# TODO: Add provider reference. # providerRef: # ... cloudSettings: From b2d6658738458c7376ecbc04eb4de585a4aa8ff0 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 11 Dec 2023 15:40:06 +0100 Subject: [PATCH 043/198] add missing fallthrough --- controller/Makefile | 3 +-- controller/pkg/cluster_controller/reconciler.go | 8 +++++--- controller/pkg/types/reconciler.go | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 2bc352d61..fc622de2a 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -62,9 +62,8 @@ genmock: ## generates mocks before running tests apply-crds: ## applies CRDs kubectl apply -f config/crd/bases -apply-clusters: apply-crds ## applies cluster resources +apply-cluster-byok: apply-crds ## applies BYOK cluster kubectl apply -f config/crd/examples/cluster_byok.yaml - kubectl apply -f config/crd/examples/cluster_aws.yaml # go-get-tool will 'go get' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index c21d8255d..4150a8b33 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -20,9 +20,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( + RequeueAfter = 30 * time.Second ClusterFinalizer = "deployments.plural.sh/cluster-protection" ) @@ -41,7 +43,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { r.Log.Error(err, "unable to fetch cluster") @@ -71,7 +73,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu providerId = provider.Status.ID if providerId == nil { r.Log.Info("provider does not have ID set yet") - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + return ctrl.Result{RequeueAfter: RequeueAfter}, nil } err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) @@ -111,7 +113,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + return ctrl.Result{RequeueAfter: RequeueAfter}, nil } func (r *Reconciler) delete(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index c9d507e4e..e306b8a79 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -2,11 +2,11 @@ package types import ( "fmt" - providerreconciler "github.com/pluralsh/console/controller/pkg/provider_reconciler" "github.com/pluralsh/console/controller/pkg/client" clustercontroller "github.com/pluralsh/console/controller/pkg/cluster_controller" gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" + providerreconciler "github.com/pluralsh/console/controller/pkg/provider_reconciler" servicecontroller "github.com/pluralsh/console/controller/pkg/service_controller" ctrl "sigs.k8s.io/controller-runtime" ) @@ -25,8 +25,11 @@ const ( func ToReconciler(reconciler string) (Reconciler, error) { switch Reconciler(reconciler) { case GitRepositoryReconciler: + fallthrough case ServiceDeploymentReconciler: + fallthrough case ClusterReconciler: + fallthrough case ProviderReconciler: return Reconciler(reconciler), nil } @@ -36,8 +39,6 @@ func ToReconciler(reconciler string) (Reconciler, error) { // ToController creates Controller instance based on this Reconciler. func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.ConsoleClient) (Controller, error) { - unsupported := fmt.Errorf("reconciler %q is not supported", sc) - switch sc { case GitRepositoryReconciler: return &gitrepositorycontroller.Reconciler{ @@ -65,7 +66,7 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.Console Scheme: mgr.GetScheme(), }, nil default: - return nil, unsupported + return nil, fmt.Errorf("reconciler %q is not supported", sc) } } From 0ec2495bdc0cccd18fbcc20753b9b86a2998d863 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 11 Dec 2023 16:32:50 +0100 Subject: [PATCH 044/198] update cluster deletion --- controller/main.go | 3 +- controller/pkg/client/cluster.go | 11 ++++ controller/pkg/client/console.go | 1 + .../pkg/cluster_controller/reconciler.go | 57 +++++++++++-------- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/controller/main.go b/controller/main.go index ff8d027d1..16b752e16 100644 --- a/controller/main.go +++ b/controller/main.go @@ -3,10 +3,11 @@ package main import ( "flag" "fmt" - "github.com/pluralsh/console/controller/pkg/types" "os" "strings" + "github.com/pluralsh/console/controller/pkg/types" + deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/client" corev1 "k8s.io/api/core/v1" diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go index 692d7d85e..524a533ac 100644 --- a/controller/pkg/client/cluster.go +++ b/controller/pkg/client/cluster.go @@ -2,6 +2,7 @@ package client import ( console "github.com/pluralsh/console-client-go" + "k8s.io/apimachinery/pkg/api/errors" ) func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) { @@ -60,3 +61,13 @@ func (c *client) DeleteCluster(id string) (*console.ClusterFragment, error) { return response.DeleteCluster, nil } + +func (c *client) ClusterExists(id *string) bool { + _, err := c.GetCluster(id) + if errors.IsNotFound(err) { + return false + } + + // We are assuming that if there is an error, and it is not ErrorNotFound then provider does not exist. + return err == nil +} diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 635cd2aa7..9be4b6228 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -39,6 +39,7 @@ type ConsoleClient interface { UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) ListClusters() (*console.ListClusters, error) DeleteCluster(id string) (*console.ClusterFragment, error) + ClusterExists(id *string) bool CreateProvider(ctx context.Context, attributes console.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) GetProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) UpdateProvider(ctx context.Context, id string, attributes console.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 4150a8b33..67b5f3cb3 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -11,7 +11,6 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/kubernetes" "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -24,8 +23,8 @@ import ( ) const ( - RequeueAfter = 30 * time.Second - ClusterFinalizer = "deployments.plural.sh/cluster-protection" + RequeueAfter = 30 * time.Second + FinalizerName = "deployments.plural.sh/cluster-protection" ) // Reconciler reconciles a Cluster object. @@ -44,18 +43,20 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + // Read cluster resource cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { r.Log.Error(err, "unable to fetch cluster") return ctrl.Result{}, client.IgnoreNotFound(err) } - if !cluster.GetDeletionTimestamp().IsZero() { - return r.delete(ctx, cluster) + // Handle cluster resource deletion + ret, err := r.addOrRemoveFinalizer(ctx, cluster) + if ret || err != nil { + return ctrl.Result{}, err } var apiCluster *console.ClusterFragment - var err error if cluster.Status.ID != nil { apiCluster, err = r.ConsoleClient.GetCluster(cluster.Status.ID) if err != nil && !errors.IsNotFound(err) { @@ -88,9 +89,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, err } } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, cluster, ClusterFinalizer); err != nil { - return ctrl.Result{}, err - } sha, err := utils.HashObject(cluster.UpdateAttributes()) if err != nil { @@ -116,30 +114,41 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{RequeueAfter: RequeueAfter}, nil } -func (r *Reconciler) delete(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { - if controllerutil.ContainsFinalizer(cluster, ClusterFinalizer) { - r.Log.Info("delete cluster") - if cluster.Status.ID == nil { - return ctrl.Result{}, fmt.Errorf("cluster ID can not be nil") +func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (bool, error) { + // If 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 to registering our finalizer. + if cluster.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(cluster, FinalizerName) { + controllerutil.AddFinalizer(cluster, FinalizerName) + if err := r.Update(ctx, cluster); err != nil { + return true, err } + } - apiCluster, err := r.ConsoleClient.GetCluster(cluster.Status.ID) - if err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, err - } + // If object is being deleted + if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { + // TODO: Add DeletedAt to queries, check if cluster is deleting and return if it is. - if apiCluster != nil { - if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, err + // Remove cluster from Console API if it exists + if cluster.Status.ID != nil && r.ConsoleClient.ClusterExists(cluster.Status.ID) { + if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil { + // If fail to delete the external dependency here, return with error so that it can be retried. + return true, err } } - if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, cluster, ClusterFinalizer); err != nil { - return ctrl.Result{}, err + // If our finalizer is present, remove it + if controllerutil.ContainsFinalizer(cluster, FinalizerName) { + controllerutil.RemoveFinalizer(cluster, FinalizerName) + if err := r.Update(ctx, cluster); err != nil { + return true, err + } } + + // Stop reconciliation as the item is being deleted + return true, nil } - return ctrl.Result{}, nil + return false, nil } func (r *Reconciler) updateStatus(ctx context.Context, cluster *v1alpha1.Cluster, patch func(cluster *v1alpha1.Cluster)) error { From 56c0ae22ab59da37298db1261aab2ca3767bde34 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Mon, 11 Dec 2023 16:55:08 +0100 Subject: [PATCH 045/198] update provider reconciler logic --- .../apis/deployments/v1alpha1/provider.go | 76 +++++++++++- .../v1alpha1/zz_generated.deepcopy.go | 5 + .../deployments.plural.sh_providers.yaml | 5 +- controller/main.go | 4 +- controller/pkg/client/console.go | 1 + controller/pkg/client/provider.go | 12 ++ .../pkg/provider_reconciler/reconciler.go | 115 +++++++++++++----- .../reconciler_attributes.go | 115 ++++++++++++++++++ 8 files changed, 297 insertions(+), 36 deletions(-) create mode 100644 controller/pkg/provider_reconciler/reconciler_attributes.go diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index 33a8dc3d1..1eaaada8d 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -1,6 +1,9 @@ package v1alpha1 import ( + "context" + console "github.com/pluralsh/console-client-go" + "github.com/samber/lo" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -9,12 +12,24 @@ func init() { SchemeBuilder.Register(&Provider{}, &ProviderList{}) } +// CloudSettingsGetter is just a helper function that can be implemented to properly +// build Console API attributes +// +kubebuilder:object:generate:=false +type CloudSettingsGetter func(context.Context, ProviderSpec) (*console.CloudProviderSettingsAttributes, error) + +// Hasher +// +kubebuilder:object:generate:=false +type Hasher func(interface{}) (string, error) + type CloudProvider string +func (c CloudProvider) Attribute() *string { + return lo.ToPtr(string(c)) +} + const ( AWS CloudProvider = "aws" Azure CloudProvider = "azure" - BYOK CloudProvider = "byok" GCP CloudProvider = "gcp" ) @@ -45,6 +60,20 @@ type Provider struct { Status ProviderStatus `json:"status,omitempty"` } +func (p *Provider) Diff(ctx context.Context, getter CloudSettingsGetter, hasher Hasher) (changed bool, sha string, err error) { + cloudSettings, err := getter(ctx, p.Spec) + if err != nil { + return false, "", err + } + + currentSha, err := hasher(cloudSettings) + if err != nil { + return false, "", err + } + + return !p.Status.IsSHAEqual(currentSha), currentSha, nil +} + // ProviderList ... // +kubebuilder:object:root=true type ProviderList struct { @@ -55,10 +84,10 @@ type ProviderList struct { } // ProviderSpec ... -// +kubebuilder:validation:Validation:rule="(self.cloud == 'byok' && !has(self.cloudSettings)) || (self.cloud != 'byok' && has(self.cloudSettings))",message="Cloud Settings must be provided only when Cloud is not set to BYOK." +// +kubebuilder:validation:Validation:rule="(self.cloud == 'aws' && has(self.cloudSettings.aws)) || (self.cloud == 'gcp' && has(self.cloudSettings.gcp)) || (self.cloud == 'azure' && has(self.cloudSettings.azure))",message="Cloud Settings must be provided only for matching Cloud." type ProviderSpec struct { // Cloud is the name of the cloud service for the Provider. - // One of (CloudProvider): [byok, gcp, aws, azure] + // One of (CloudProvider): [gcp, aws, azure] // +kubebuilder:example:=byok // +kubebuilder:validation:Required // +kubebuilder:validation:Type:=string @@ -82,11 +111,32 @@ type ProviderSpec struct { Namespace string `json:"namespace,omitempty"` } +func (p *ProviderSpec) Attributes(ctx context.Context, cloudSettingsGetter CloudSettingsGetter) (console.ClusterProviderAttributes, error) { + cloudSettings, err := cloudSettingsGetter(ctx, *p) + return console.ClusterProviderAttributes{ + Name: p.Name, + Namespace: &p.Namespace, + Cloud: p.Cloud.Attribute(), + CloudSettings: cloudSettings, + }, err +} + +func (p *ProviderSpec) UpdateAttributes(ctx context.Context, cloudSettingsGetter CloudSettingsGetter) (console.ClusterProviderUpdateAttributes, error) { + cloudSettings, err := cloudSettingsGetter(ctx, *p) + return console.ClusterProviderUpdateAttributes{ + CloudSettings: cloudSettings, + }, err +} + // ProviderStatus ... type ProviderStatus struct { // ID of the provider in the Console API. // +kubebuilder:validation:Optional ID *string `json:"id,omitempty"` + // SHA of last applied configuration. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + SHA *string `json:"sha,omitempty"` } func (p *ProviderStatus) GetID() string { @@ -100,3 +150,23 @@ func (p *ProviderStatus) GetID() string { func (p *ProviderStatus) HasID() bool { return p.ID != nil && len(*p.ID) > 0 } + +func (p *ProviderStatus) GetSHA() string { + if !p.HasSHA() { + return "" + } + + return *p.SHA +} + +func (p *ProviderStatus) HasSHA() bool { + return p.SHA != nil && len(*p.SHA) > 0 +} + +func (p *ProviderStatus) IsSHAEqual(sha string) bool { + if !p.HasSHA() { + return false + } + + return p.GetSHA() == sha +} diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index ebefb936f..e1a6da700 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -626,6 +626,11 @@ func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { *out = new(string) **out = **in } + if in.SHA != nil { + in, out := &in.SHA, &out.SHA + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index 57184aa24..a7283fa8e 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -50,7 +50,7 @@ spec: properties: cloud: description: 'Cloud is the name of the cloud service for the Provider. - One of (CloudProvider): [byok, gcp, aws, azure]' + One of (CloudProvider): [gcp, aws, azure]' enum: - byok - gcp @@ -126,6 +126,9 @@ spec: id: description: ID of the provider in the Console API. type: string + sha: + description: SHA of last applied configuration. + type: string type: object required: - spec diff --git a/controller/main.go b/controller/main.go index ff8d027d1..e606fa6d4 100644 --- a/controller/main.go +++ b/controller/main.go @@ -52,8 +52,8 @@ func main() { flag.BoolVar(&opt.enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.StringVar(&opt.consoleUrl, "console-url", "", "the url of the console api to fetch services from") - flag.StringVar(&opt.consoleToken, "console-token", "", "the deploy token to auth to console api with") + flag.StringVar(&opt.consoleUrl, "console-url", "", "The url of the console api to fetch services from") + flag.StringVar(&opt.consoleToken, "console-token", "", "The console token to auth to console api with") flag.Func("reconcilers", "Comma delimited list of reconciler names. Available reconcilers: gitrepository,cluster,provider,servicedeployment", func(reconcilersStr string) (err error) { opt.reconcilers, err = parseReconcilers(reconcilersStr) return err diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 635cd2aa7..a9ae7d415 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -44,6 +44,7 @@ type ConsoleClient interface { UpdateProvider(ctx context.Context, id string, attributes console.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) DeleteProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) error IsProviderExists(ctx context.Context, id string) bool + IsProviderDeleting(ctx context.Context, id string) bool UpdateService(serviceId string, attributes console.ServiceUpdateAttributes) error } diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go index bb94cf3cc..d2e048243 100644 --- a/controller/pkg/client/provider.go +++ b/controller/pkg/client/provider.go @@ -45,3 +45,15 @@ func (c *client) IsProviderExists(ctx context.Context, id string) bool { // We are assuming that if there is an error, and it is not ErrorNotFound then provider does not exist. return err == nil } + +func (c *client) IsProviderDeleting(ctx context.Context, id string) bool { + _, err := c.GetProvider(ctx, id) + if err != nil { + return false + } + + return false + + // TODO: Update client to support DeletedAt + //return provider != nil && provider.DeletedAt != nil +} diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index e1f7272f3..2e21c9164 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -2,10 +2,14 @@ package providerreconciler import ( "context" + "fmt" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" + "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -34,87 +38,138 @@ var ( // Reconcile ... // TODO: Add kubebuilder rbac annotation -func (p *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := log.FromContext(ctx) // Read Provider CRD from the K8S API var provider v1alpha1.Provider - if err := p.Get(ctx, req.NamespacedName, &provider); err != nil { + if err := r.Get(ctx, req.NamespacedName, &provider); err != nil { log.Error(err, "unable to fetch provider") return ctrl.Result{}, client.IgnoreNotFound(err) } // Handle resource deletion - ret, err := p.addOrRemoveFinalizer(ctx, provider) - if ret || err != nil { + result, err := r.addOrRemoveFinalizer(ctx, provider) + if result != nil { + return *result, err + } + + // Get Provider SHA that can be saved back in the status to check for changes + _, sha, err := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) + if err != nil { + log.Error(err, "unable to calculate provider SHA") return ctrl.Result{}, err } // Sync Provider CRD with the Console API - apiProvider, err := p.createOrUpdateProvider(ctx, provider) + apiProvider, err := r.updateOrGetProvider(ctx, provider) if err != nil { log.Error(err, "unable to create or update provider") return ctrl.Result{}, err } - // Sync back Provider ID to crd - provider.Status.ID = &apiProvider.ID - // TODO: update CRD status - - return requeue, nil -} - -func (p *Reconciler) createOrUpdateProvider(ctx context.Context, provider v1alpha1.Provider) (*console.ClusterProviderFragment, error) { - // TODO: Read credential secrets and attributes - - if provider.Status.HasID() && p.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { - return p.ConsoleClient.UpdateProvider(ctx, provider.Status.GetID(), console.ClusterProviderUpdateAttributes{}) + // Sync back Provider to crd status + if err = r.updateStatus(ctx, &provider, func(p *v1alpha1.Provider) { + p.Status.ID = &apiProvider.ID + p.Status.SHA = &sha + }); err != nil { + return ctrl.Result{}, err } - return p.ConsoleClient.CreateProvider(ctx, console.ClusterProviderAttributes{}) + return requeue, nil } -func (p *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (bool, error) { +func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { // If 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 // to registering our finalizer. if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&provider, FinalizerName) { controllerutil.AddFinalizer(&provider, FinalizerName) - if err := p.Update(ctx, &provider); err != nil { - return true, err + if err := r.Update(ctx, &provider); err != nil { + return &ctrl.Result{}, err } } // If object is being deleted if !provider.ObjectMeta.DeletionTimestamp.IsZero() { + // If object is already being deleted from Console API requeue + if r.ConsoleClient.IsProviderDeleting(ctx, provider.Status.GetID()) { + return &requeue, nil + } + // Remove Provider from Console API if it exists - if p.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { - if err := p.ConsoleClient.DeleteProvider(ctx, provider.Status.GetID()); err != nil { + if r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { + if err := r.ConsoleClient.DeleteProvider(ctx, provider.Status.GetID()); err != nil { // if fail to delete the external dependency here, return with error // so that it can be retried. - return true, err + return &ctrl.Result{}, err } } // If our finalizer is present, remove it if controllerutil.ContainsFinalizer(&provider, FinalizerName) { controllerutil.RemoveFinalizer(&provider, FinalizerName) - if err := p.Update(ctx, &provider); err != nil { - return true, err + if err := r.Update(ctx, &provider); err != nil { + return &ctrl.Result{}, err } } // Stop reconciliation as the item is being deleted - return true, nil + return &ctrl.Result{}, nil } - return false, nil + return nil, nil +} + +func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1.Provider) (*console.ClusterProviderFragment, error) { + changed, _, _ := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) + exists := r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) + + // Update only if Provider has changed + if changed && exists { + attributes, err := provider.Spec.UpdateAttributes(ctx, r.toCloudProviderSettingsAttributes) + if err != nil { + return nil, err + } + + return r.ConsoleClient.UpdateProvider(ctx, provider.Status.GetID(), attributes) + } + + // Read the Provider from Console API if it already exists + if exists { + return r.ConsoleClient.GetProvider(ctx, provider.Status.GetID()) + } + + // Create the Provider in Console API if it doesn't exist + attributes, err := provider.Spec.Attributes(ctx, r.toCloudProviderSettingsAttributes) + if err != nil { + return nil, err + } + + return r.ConsoleClient.CreateProvider(ctx, attributes) +} + +func (r *Reconciler) updateStatus(ctx context.Context, provider *v1alpha1.Provider, patch func(provider *v1alpha1.Provider)) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := r.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil { + return fmt.Errorf("could not fetch current provider state, got error: %+v", err) + } + + original := provider.DeepCopy() + patch(provider) + + if reflect.DeepEqual(original.Status, provider.Status) { + return nil + } + + return r.Client.Status().Patch(ctx, provider, client.MergeFrom(original)) + }) } // SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. -func (p *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { mgr.GetLogger().Info("starting reconciler", "reconciler", "provider_reconciler") return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Provider{}). - Complete(p) + Complete(r) } diff --git a/controller/pkg/provider_reconciler/reconciler_attributes.go b/controller/pkg/provider_reconciler/reconciler_attributes.go new file mode 100644 index 000000000..d6a93b959 --- /dev/null +++ b/controller/pkg/provider_reconciler/reconciler_attributes.go @@ -0,0 +1,115 @@ +package providerreconciler + +import ( + "context" + "fmt" + console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/kubernetes" + corev1 "k8s.io/api/core/v1" +) + +func (r *Reconciler) missingCredentialKeyError(key string) error { + return fmt.Errorf("%q key does not exist in referenced credential secret", key) +} + +func (r *Reconciler) toCloudProviderSettingsAttributes(ctx context.Context, spec v1alpha1.ProviderSpec) (*console.CloudProviderSettingsAttributes, error) { + switch spec.Cloud { + case v1alpha1.AWS: + return r.toCloudProviderAWSSettingsAttributes(ctx, spec.CloudSettings.AWS) + case v1alpha1.Azure: + return r.toCloudProviderAzureSettingsAttributes(ctx, spec.CloudSettings.Azure) + case v1alpha1.GCP: + return r.toCloudProviderGCPSettingsAttributes(ctx, spec.CloudSettings.GCP) + } + + return nil, fmt.Errorf("unsupported cloud: %q", spec.Cloud) +} + +func (r *Reconciler) toCloudProviderAWSSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { + const accessKeyIDKeyName = "accessKeyId" + const secretAccessKeyName = "secretAccessKey" + + secret, err := kubernetes.GetSecret(ctx, r.Client, ref) + if err != nil { + return nil, err + } + + accessKeyID, exists := secret.Data[accessKeyIDKeyName] + if !exists { + return nil, r.missingCredentialKeyError(accessKeyIDKeyName) + } + + secretAccessKey, exists := secret.Data[secretAccessKeyName] + if !exists { + return nil, r.missingCredentialKeyError(secretAccessKeyName) + } + + return &console.CloudProviderSettingsAttributes{ + Aws: &console.AwsSettingsAttributes{ + AccessKeyID: string(accessKeyID), + SecretAccessKey: string(secretAccessKey), + }, + }, nil +} + +func (r *Reconciler) toCloudProviderAzureSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { + const tenantIDKeyName = "tenantId" + const subscriptionIDKeyName = "subscriptionId" + const clientIDKeyName = "clientId" + const clientSecretKeyName = "clientSecret" + + secret, err := kubernetes.GetSecret(ctx, r.Client, ref) + if err != nil { + return nil, err + } + + tenantID, exists := secret.Data[tenantIDKeyName] + if !exists { + return nil, r.missingCredentialKeyError(tenantIDKeyName) + } + + subscriptionID, exists := secret.Data[subscriptionIDKeyName] + if !exists { + return nil, r.missingCredentialKeyError(subscriptionIDKeyName) + } + + clientID, exists := secret.Data[clientIDKeyName] + if !exists { + return nil, r.missingCredentialKeyError(clientIDKeyName) + } + + clientSecret, exists := secret.Data[clientSecretKeyName] + if !exists { + return nil, r.missingCredentialKeyError(clientSecretKeyName) + } + + return &console.CloudProviderSettingsAttributes{ + Azure: &console.AzureSettingsAttributes{ + TenantID: string(tenantID), + SubscriptionID: string(subscriptionID), + ClientID: string(clientID), + ClientSecret: string(clientSecret), + }, + }, nil +} + +func (r *Reconciler) toCloudProviderGCPSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { + const applicationCredentialsKeyName = "applicationCredentials" + + secret, err := kubernetes.GetSecret(ctx, r.Client, ref) + if err != nil { + return nil, err + } + + applicationCredentials, exists := secret.Data[applicationCredentialsKeyName] + if !exists { + return nil, r.missingCredentialKeyError(applicationCredentialsKeyName) + } + + return &console.CloudProviderSettingsAttributes{ + Gcp: &console.GcpSettingsAttributes{ + ApplicationCredentials: string(applicationCredentials), + }, + }, nil +} From d9757304272bb7cb3fb684dcae889c61659f3644 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Mon, 11 Dec 2023 17:22:30 +0100 Subject: [PATCH 046/198] service changes --- controller/main.go | 3 +- .../gitrepository_controller/controller.go | 30 +++---------- .../pkg/service_controller/controller.go | 45 +++++++++++++------ 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/controller/main.go b/controller/main.go index eba33ca3a..048ec9a47 100644 --- a/controller/main.go +++ b/controller/main.go @@ -6,10 +6,9 @@ import ( "os" "strings" - "github.com/pluralsh/console/controller/pkg/types" - deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index bdf2a4251..9e10d2646 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -7,15 +7,15 @@ import ( "time" "github.com/go-logr/logr" - "github.com/pluralsh/console/controller/pkg/utils" - console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/kubernetes" + "github.com/pluralsh/console/controller/pkg/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" @@ -44,6 +44,7 @@ type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient Log logr.Logger + Scheme *runtime.Scheme } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -120,28 +121,6 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { - if repo.Spec.CredentialsRef != nil { - secret := &corev1.Secret{} - name := types.NamespacedName{Name: repo.Spec.CredentialsRef.Name, Namespace: repo.Spec.CredentialsRef.Namespace} - err := r.Get(ctx, name, secret) - if err != nil { - if !apierrors.IsNotFound(err) { - return ctrl.Result{}, err - } - } - if secret.Name != "" { - if controllerutil.ContainsFinalizer(secret, RepoFinalizer) { - r.Log.Info("delete credential secret") - err := kubernetes.DeleteSecret(ctx, r.Client, repo.Spec.CredentialsRef.Namespace, repo.Spec.CredentialsRef.Name) - if err != nil { - return ctrl.Result{}, err - } - if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, secret, RepoFinalizer); err != nil { - return ctrl.Result{}, err - } - } - } - } if controllerutil.ContainsFinalizer(repo, RepoFinalizer) { r.Log.Info("delete git repository") @@ -178,7 +157,8 @@ func (r *Reconciler) getRepositoryCredentials(ctx context.Context, repo *v1alpha return nil, err } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, secret, RepoFinalizer); err != nil { + err = utils.TryAddOwnerRef(ctx, r.Client, repo, secret, r.Scheme) + if err != nil { return nil, err } diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 3904ec4c4..1e76388a8 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -14,6 +14,7 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" @@ -31,6 +32,7 @@ type Reconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient Log logr.Logger + Scheme *runtime.Scheme } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -47,7 +49,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if cluster.Status.ID == nil { - r.Log.Info("Cluster is not ready", service.Spec.ClusterRef.Name) + r.Log.Info("Cluster is not ready") return ctrl.Result{ // update status RequeueAfter: 30 * time.Second, @@ -59,14 +61,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if repository.Status.Id == nil { - r.Log.Info("Repository is not ready", service.Spec.RepositoryRef.Name) + r.Log.Info("Repository is not ready") return ctrl.Result{ // update status RequeueAfter: 30 * time.Second, }, nil } if repository.Status.Health == v1alpha1.GitHealthFailed { - r.Log.Info("Repository is not healthy", service.Spec.RepositoryRef.Name) + r.Log.Info("Repository is not healthy") return ctrl.Result{ // update status RequeueAfter: 30 * time.Second, @@ -98,11 +100,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { - return ctrl.Result{}, err - } } - err = r.addReferenceFinalizers(ctx, service) + if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + return ctrl.Result{}, err + } + + err = r.addOwnerReferences(ctx, service) if err != nil { return ctrl.Result{}, err } @@ -122,6 +125,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } } + err = utils.TryAddOwnerRef(ctx, r.Client, cluster, service, r.Scheme) + if err != nil { + return ctrl.Result{}, err + } + err = utils.TryAddOwnerRef(ctx, r.Client, repository, service, r.Scheme) + if err != nil { + return ctrl.Result{}, err + } + if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.Service) { r.Status.Id = &existingService.ID r.Status.Sha = sha @@ -244,35 +256,40 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 return attr, nil } -func (r *Reconciler) addReferenceFinalizers(ctx context.Context, service *v1alpha1.Service) error { +func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.Service) error { if service.Spec.ConfigurationRef != nil { configurationSecret, err := kubernetes.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) if err != nil { return err } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, configurationSecret, ServiceFinalizer); err != nil { + err = utils.TryAddOwnerRef(ctx, r.Client, service, configurationSecret, r.Scheme) + if err != nil { return err } + } - if service.Spec.Helm.ValuesRef != nil { + + if service.Spec.Helm != nil && service.Spec.Helm.ValuesRef != nil { configMap := &corev1.ConfigMap{} name := types.NamespacedName{Name: service.Spec.Helm.ValuesRef.Name, Namespace: service.Namespace} err := r.Get(ctx, name, configMap) if err != nil { return err } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, configMap, ServiceFinalizer); err != nil { + err = utils.TryAddOwnerRef(ctx, r.Client, service, configMap, r.Scheme) + if err != nil { return err } } - if service.Spec.Helm.ChartRef != nil { + if service.Spec.Helm != nil && service.Spec.Helm.ChartRef != nil { configMap := &corev1.ConfigMap{} name := types.NamespacedName{Name: service.Spec.Helm.ChartRef.Name, Namespace: service.Namespace} err := r.Get(ctx, name, configMap) if err != nil { return err } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, configMap, ServiceFinalizer); err != nil { + err = utils.TryAddOwnerRef(ctx, r.Client, service, configMap, r.Scheme) + if err != nil { return err } } @@ -281,7 +298,7 @@ func (r *Reconciler) addReferenceFinalizers(ctx context.Context, service *v1alph } func (r *Reconciler) handleDelete(ctx context.Context, service *v1alpha1.Service) (ctrl.Result, error) { - if controllerutil.ContainsFinalizer(service, "") { + if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { } return ctrl.Result{}, nil From b794b3f12911c4eb5823e11cd6fb708bb32002be Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 09:14:14 +0100 Subject: [PATCH 047/198] bump console client --- controller/go.mod | 6 +++--- controller/go.sum | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/controller/go.mod b/controller/go.mod index 5613e0c07..9543bf23a 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -6,8 +6,10 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 - github.com/pluralsh/console-client-go v0.0.53 + github.com/go-logr/logr v1.2.3 + github.com/pluralsh/console-client-go v0.0.54 github.com/pluralsh/polly v0.1.4 + github.com/samber/lo v1.33.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 @@ -28,7 +30,6 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/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 @@ -59,7 +60,6 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/samber/lo v1.33.0 // indirect github.com/schollz/progressbar/v3 v3.8.6 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/vektah/gqlparser/v2 v2.5.10 // indirect diff --git a/controller/go.sum b/controller/go.sum index 249310031..5d3f1bd6d 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -230,8 +230,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/pluralsh/console-client-go v0.0.53 h1:Vawo9pP/nrXC8kSP7mUazMIo5YEigRNchDi/RZWnpVc= -github.com/pluralsh/console-client-go v0.0.53/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= +github.com/pluralsh/console-client-go v0.0.54 h1:hZ7yjXuRHvoGiQ9HgbVpidNHWlf7KapxhwAMN3lP+zc= +github.com/pluralsh/console-client-go v0.0.54/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 11c5c8dca2a2ef1a3855d32e846769defa24e233 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 09:43:08 +0100 Subject: [PATCH 048/198] change service to deploymentservice --- .../apis/deployments/v1alpha1/service.go | 8 +- .../v1alpha1/zz_generated.deepcopy.go | 118 +++++++++--------- ...oyments.plural.sh_deploymentservices.yaml} | 10 +- .../gitrepository_controller/controller.go | 8 +- .../pkg/service_controller/controller.go | 16 +-- 5 files changed, 80 insertions(+), 80 deletions(-) rename controller/config/crd/bases/{deployments.plural.sh_services.yaml => deployments.plural.sh_deploymentservices.yaml} (98%) diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index 735b98e8b..186bafac8 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -6,7 +6,7 @@ import ( ) func init() { - SchemeBuilder.Register(&Service{}, &ServiceList{}) + SchemeBuilder.Register(&DeploymentService{}, &DeploymentServiceList{}) } type ComponentState string @@ -105,7 +105,7 @@ type ServiceComponent struct { // +kubebuilder:resource:scope=Namespaced // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console repo Id" -type Service struct { +type DeploymentService struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -116,8 +116,8 @@ type Service struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ServiceList struct { +type DeploymentServiceList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []Service `json:"items"` + Items []DeploymentService `json:"items"` } diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index e1a6da700..f56ac7e1f 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -420,6 +420,65 @@ func (in *ClusterStatus) DeepCopy() *ClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentService) DeepCopyInto(out *DeploymentService) { + *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 DeploymentService. +func (in *DeploymentService) DeepCopy() *DeploymentService { + if in == nil { + return nil + } + out := new(DeploymentService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeploymentService) 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 *DeploymentServiceList) DeepCopyInto(out *DeploymentServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeploymentService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentServiceList. +func (in *DeploymentServiceList) DeepCopy() *DeploymentServiceList { + if in == nil { + return nil + } + out := new(DeploymentServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeploymentServiceList) 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 *GitRepository) DeepCopyInto(out *GitRepository) { *out = *in @@ -643,33 +702,6 @@ func (in *ProviderStatus) DeepCopy() *ProviderStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Service) DeepCopyInto(out *Service) { - *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 Service. -func (in *Service) DeepCopy() *Service { - if in == nil { - return nil - } - out := new(Service) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Service) 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 *ServiceComponent) DeepCopyInto(out *ServiceComponent) { *out = *in @@ -796,38 +828,6 @@ func (in *ServiceKustomize) DeepCopy() *ServiceKustomize { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceList) DeepCopyInto(out *ServiceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Service, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceList. -func (in *ServiceList) DeepCopy() *ServiceList { - if in == nil { - return nil - } - out := new(ServiceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ServiceList) 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 *ServiceSpec) DeepCopyInto(out *ServiceSpec) { *out = *in diff --git a/controller/config/crd/bases/deployments.plural.sh_services.yaml b/controller/config/crd/bases/deployments.plural.sh_deploymentservices.yaml similarity index 98% rename from controller/config/crd/bases/deployments.plural.sh_services.yaml rename to controller/config/crd/bases/deployments.plural.sh_deploymentservices.yaml index a24a482a9..f160baa04 100644 --- a/controller/config/crd/bases/deployments.plural.sh_services.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_deploymentservices.yaml @@ -5,14 +5,14 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.11.3 creationTimestamp: null - name: services.deployments.plural.sh + name: deploymentservices.deployments.plural.sh spec: group: deployments.plural.sh names: - kind: Service - listKind: ServiceList - plural: services - singular: service + kind: DeploymentService + listKind: DeploymentServiceList + plural: deploymentservices + singular: deploymentservice scope: Namespaced versions: - additionalPrinterColumns: diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 9e10d2646..d1ab16660 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -77,12 +77,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } existingRepo = resp.CreateGitRepository - } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { - return ctrl.Result{}, err + if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { + return ctrl.Result{}, err + } } - if repo.Status.Sha != "" && repo.Status.Sha != sha { + if repo.Status.Sha != "" && repo.Status.Sha != sha && controllerutil.ContainsFinalizer(repo, RepoFinalizer) { _, err := r.ConsoleClient.UpdateRepository(existingRepo.ID, console.GitAttributes{ URL: repo.Spec.Url, PrivateKey: cred.PrivateKey, diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 1e76388a8..7d4cafadf 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -36,7 +36,7 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - service := &v1alpha1.Service{} + service := &v1alpha1.DeploymentService{} if err := r.Get(ctx, req.NamespacedName, service); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -134,7 +134,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.Service) { + if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.DeploymentService) { r.Status.Id = &existingService.ID r.Status.Sha = sha if existingService.Errors != nil { @@ -177,11 +177,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.Service{}). + For(&v1alpha1.DeploymentService{}). Complete(r) } -func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.Service, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { +func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.DeploymentService, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { attr := &console.ServiceDeploymentAttributes{ Name: service.Name, Namespace: service.Namespace, @@ -256,7 +256,7 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 return attr, nil } -func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.Service) error { +func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.DeploymentService) error { if service.Spec.ConfigurationRef != nil { configurationSecret, err := kubernetes.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) if err != nil { @@ -297,16 +297,16 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S return nil } -func (r *Reconciler) handleDelete(ctx context.Context, service *v1alpha1.Service) (ctrl.Result, error) { +func (r *Reconciler) handleDelete(ctx context.Context, service *v1alpha1.DeploymentService) (ctrl.Result, error) { if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { } return ctrl.Result{}, nil } -type RepoPatchFunc func(service *v1alpha1.Service) +type RepoPatchFunc func(service *v1alpha1.DeploymentService) -func UpdateServiceStatus(ctx context.Context, client ctrlruntimeclient.Client, service *v1alpha1.Service, patch RepoPatchFunc) error { +func UpdateServiceStatus(ctx context.Context, client ctrlruntimeclient.Client, service *v1alpha1.DeploymentService, patch RepoPatchFunc) error { key := ctrlruntimeclient.ObjectKeyFromObject(service) return retry.RetryOnConflict(retry.DefaultRetry, func() error { From fafc7d4ff14909ee2373c656e86c521bf7f44506 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 09:53:14 +0100 Subject: [PATCH 049/198] add scheme --- controller/pkg/types/reconciler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index e306b8a79..ce802bfc7 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -45,12 +45,14 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.Console Client: mgr.GetClient(), Log: mgr.GetLogger(), ConsoleClient: consoleClient, + Scheme: mgr.GetScheme(), }, nil case ServiceDeploymentReconciler: return &servicecontroller.Reconciler{ Client: mgr.GetClient(), Log: mgr.GetLogger(), ConsoleClient: consoleClient, + Scheme: mgr.GetScheme(), }, nil case ClusterReconciler: return &clustercontroller.Reconciler{ From d58fd458e3709077c8642ad2dc91839fca721f23 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 10:30:14 +0100 Subject: [PATCH 050/198] service deployment --- .../apis/deployments/v1alpha1/service.go | 8 +- .../v1alpha1/zz_generated.deepcopy.go | 118 +++++++++--------- ...oyments.plural.sh_servicedeployments.yaml} | 10 +- controller/pkg/client/console.go | 1 + controller/pkg/client/service.go | 8 ++ .../gitrepository_controller/controller.go | 6 +- .../pkg/service_controller/controller.go | 49 +++++--- 7 files changed, 109 insertions(+), 91 deletions(-) rename controller/config/crd/bases/{deployments.plural.sh_deploymentservices.yaml => deployments.plural.sh_servicedeployments.yaml} (98%) diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index 186bafac8..312931f97 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -6,7 +6,7 @@ import ( ) func init() { - SchemeBuilder.Register(&DeploymentService{}, &DeploymentServiceList{}) + SchemeBuilder.Register(&ServiceDeployment{}, &ServiceDeploymentList{}) } type ComponentState string @@ -105,7 +105,7 @@ type ServiceComponent struct { // +kubebuilder:resource:scope=Namespaced // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Id",type="string",JSONPath=".status.id",description="Console repo Id" -type DeploymentService struct { +type ServiceDeployment struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -116,8 +116,8 @@ type DeploymentService struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DeploymentServiceList struct { +type ServiceDeploymentList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []DeploymentService `json:"items"` + Items []ServiceDeployment `json:"items"` } diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index f56ac7e1f..0edfaa5cb 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -420,65 +420,6 @@ func (in *ClusterStatus) DeepCopy() *ClusterStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DeploymentService) DeepCopyInto(out *DeploymentService) { - *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 DeploymentService. -func (in *DeploymentService) DeepCopy() *DeploymentService { - if in == nil { - return nil - } - out := new(DeploymentService) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DeploymentService) 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 *DeploymentServiceList) DeepCopyInto(out *DeploymentServiceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DeploymentService, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentServiceList. -func (in *DeploymentServiceList) DeepCopy() *DeploymentServiceList { - if in == nil { - return nil - } - out := new(DeploymentServiceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DeploymentServiceList) 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 *GitRepository) DeepCopyInto(out *GitRepository) { *out = *in @@ -737,6 +678,65 @@ func (in *ServiceComponent) DeepCopy() *ServiceComponent { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceDeployment) DeepCopyInto(out *ServiceDeployment) { + *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 ServiceDeployment. +func (in *ServiceDeployment) DeepCopy() *ServiceDeployment { + if in == nil { + return nil + } + out := new(ServiceDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceDeployment) 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 *ServiceDeploymentList) DeepCopyInto(out *ServiceDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceDeploymentList. +func (in *ServiceDeploymentList) DeepCopy() *ServiceDeploymentList { + if in == nil { + return nil + } + out := new(ServiceDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceDeploymentList) 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 *ServiceError) DeepCopyInto(out *ServiceError) { *out = *in diff --git a/controller/config/crd/bases/deployments.plural.sh_deploymentservices.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml similarity index 98% rename from controller/config/crd/bases/deployments.plural.sh_deploymentservices.yaml rename to controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index f160baa04..3ee7aef07 100644 --- a/controller/config/crd/bases/deployments.plural.sh_deploymentservices.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -5,14 +5,14 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.11.3 creationTimestamp: null - name: deploymentservices.deployments.plural.sh + name: servicedeployments.deployments.plural.sh spec: group: deployments.plural.sh names: - kind: DeploymentService - listKind: DeploymentServiceList - plural: deploymentservices - singular: deploymentservice + kind: ServiceDeployment + listKind: ServiceDeploymentList + plural: servicedeployments + singular: servicedeployment scope: Namespaced versions: - additionalPrinterColumns: diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 265d9fbad..002f6693f 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -47,6 +47,7 @@ type ConsoleClient interface { IsProviderExists(ctx context.Context, id string) bool IsProviderDeleting(ctx context.Context, id string) bool UpdateService(serviceId string, attributes console.ServiceUpdateAttributes) error + DeleteService(serviceId string) error } func New(url, token string) ConsoleClient { diff --git a/controller/pkg/client/service.go b/controller/pkg/client/service.go index eaf801045..4ee869923 100644 --- a/controller/pkg/client/service.go +++ b/controller/pkg/client/service.go @@ -52,3 +52,11 @@ func (c *client) UpdateComponents(id string, components []*console.ComponentAttr _, err := c.consoleClient.UpdateServiceComponents(c.ctx, id, components, errs) return err } + +func (c *client) DeleteService(serviceId string) error { + _, err := c.consoleClient.DeleteServiceDeployment(c.ctx, serviceId) + if err != nil { + return err + } + return nil +} diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index d1ab16660..641bd1e26 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -72,14 +72,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if existingRepo == nil { + if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { + return ctrl.Result{}, err + } resp, err := r.ConsoleClient.CreateRepository(repo.Spec.Url, cred.PrivateKey, cred.Passphrase, cred.Username, cred.Password) if err != nil { return ctrl.Result{}, err } existingRepo = resp.CreateGitRepository - if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { - return ctrl.Result{}, err - } } if repo.Status.Sha != "" && repo.Status.Sha != sha && controllerutil.ContainsFinalizer(repo, RepoFinalizer) { diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 7d4cafadf..5e8372589 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -36,14 +36,10 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - service := &v1alpha1.DeploymentService{} + service := &v1alpha1.ServiceDeployment{} if err := r.Get(ctx, req.NamespacedName, service); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - if !service.GetDeletionTimestamp().IsZero() { - return r.handleDelete(ctx, service) - } - cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { return ctrl.Result{}, err @@ -55,6 +51,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu RequeueAfter: 30 * time.Second, }, nil } + if !service.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, cluster, service) + } repository := &v1alpha1.GitRepository{} if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.RepositoryRef.Name, Namespace: service.Spec.RepositoryRef.Namespace}, repository); err != nil { @@ -86,10 +85,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) - if err != nil { - if !errors.IsNotFound(err) { - return ctrl.Result{}, err - } + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err } if existingService == nil { _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) @@ -100,9 +97,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { - return ctrl.Result{}, err + if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + return ctrl.Result{}, err + } } err = r.addOwnerReferences(ctx, service) @@ -134,7 +131,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.DeploymentService) { + if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment) { r.Status.Id = &existingService.ID r.Status.Sha = sha if existingService.Errors != nil { @@ -177,11 +174,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.DeploymentService{}). + For(&v1alpha1.ServiceDeployment{}). Complete(r) } -func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.DeploymentService, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { +func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.ServiceDeployment, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { attr := &console.ServiceDeploymentAttributes{ Name: service.Name, Namespace: service.Namespace, @@ -256,7 +253,7 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 return attr, nil } -func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.DeploymentService) error { +func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.ServiceDeployment) error { if service.Spec.ConfigurationRef != nil { configurationSecret, err := kubernetes.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) if err != nil { @@ -297,16 +294,28 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.D return nil } -func (r *Reconciler) handleDelete(ctx context.Context, service *v1alpha1.DeploymentService) (ctrl.Result, error) { +func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { - + r.Log.Info("delete service") + existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + if existingService != nil { + if err := r.ConsoleClient.DeleteService(*service.Status.Id); err != nil { + return ctrl.Result{}, err + } + } + if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + return ctrl.Result{}, err + } } return ctrl.Result{}, nil } -type RepoPatchFunc func(service *v1alpha1.DeploymentService) +type RepoPatchFunc func(service *v1alpha1.ServiceDeployment) -func UpdateServiceStatus(ctx context.Context, client ctrlruntimeclient.Client, service *v1alpha1.DeploymentService, patch RepoPatchFunc) error { +func UpdateServiceStatus(ctx context.Context, client ctrlruntimeclient.Client, service *v1alpha1.ServiceDeployment, patch RepoPatchFunc) error { key := ctrlruntimeclient.ObjectKeyFromObject(service) return retry.RetryOnConflict(retry.DefaultRetry, func() error { From 2031240d7ec4e4cc094ac8a0894b3a197c100d1b Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 10:33:34 +0100 Subject: [PATCH 051/198] add existing to git repo --- controller/apis/deployments/v1alpha1/git_repository.go | 2 ++ .../crd/bases/deployments.plural.sh_gitrepositories.yaml | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 6f66b0218..a44e7bb40 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -38,6 +38,8 @@ type GitRepositoryStatus struct { Id *string `json:"id,omitempty"` // +optional Sha string `json:"sha,omitempty"` + // +kubebuilder:default:=false + Existing bool `json:"existing"` } // +kubebuilder:object:root=true diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index acd9b8e3c..3b67151e4 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -66,6 +66,9 @@ spec: type: object status: properties: + existing: + default: false + type: boolean health: description: Health status. enum: @@ -80,6 +83,8 @@ spec: type: string sha: type: string + required: + - existing type: object type: object served: true From 51e84f6b6fdfbb20328ecb84f67f76902e22f019 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 10:40:20 +0100 Subject: [PATCH 052/198] add existing to git repo --- controller/pkg/gitrepository_controller/controller.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 641bd1e26..855310af9 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -48,6 +48,7 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + existing := true repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { if apierrors.IsNotFound(err) { @@ -80,9 +81,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } existingRepo = resp.CreateGitRepository + existing = false } - if repo.Status.Sha != "" && repo.Status.Sha != sha && controllerutil.ContainsFinalizer(repo, RepoFinalizer) { + if repo.Status.Sha != "" && repo.Status.Sha != sha && !existing { _, err := r.ConsoleClient.UpdateRepository(existingRepo.ID, console.GitAttributes{ URL: repo.Spec.Url, PrivateKey: cred.PrivateKey, @@ -102,6 +104,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } r.Status.Sha = sha + r.Status.Existing = existing }); err != nil { return ctrl.Result{}, err From dcf7e933def871e587c17854a5111ef43d684d33 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 10:50:02 +0100 Subject: [PATCH 053/198] improve git repo controller --- .../pkg/gitrepository_controller/controller.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 855310af9..edf2e827e 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -72,6 +72,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err } + if existingRepo == nil && repo.Status.Existing == true { + msg := "existing Git repository was deleted from console" + r.Log.Info(msg) + if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + r.Status.Message = &msg + r.Status.Id = nil + r.Status.Existing = existing + }); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } if existingRepo == nil { if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { return ctrl.Result{}, err From a7b6ec3924eba3c272d380bc9b6b13acd0359ac4 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 10:58:40 +0100 Subject: [PATCH 054/198] update cluster deletion --- controller/pkg/client/cluster.go | 24 +++++++--- controller/pkg/client/console.go | 3 +- .../pkg/cluster_controller/reconciler.go | 44 ++++++++++++------- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go index 524a533ac..7a3d5ebe9 100644 --- a/controller/pkg/client/cluster.go +++ b/controller/pkg/client/cluster.go @@ -3,6 +3,7 @@ package client import ( console "github.com/pluralsh/console-client-go" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" ) func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) { @@ -33,20 +34,20 @@ func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.Cluste func (c *client) UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) { response, err := c.consoleClient.UpdateCluster(c.ctx, id, attrs) - if err != nil { - return nil, err + if err == nil && (response == nil || response.UpdateCluster == nil) { + return nil, errors.NewNotFound(schema.GroupResource{}, id) } - return response.UpdateCluster, nil + return response.UpdateCluster, err } func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { response, err := c.consoleClient.GetCluster(c.ctx, id) - if err != nil { - return nil, err + if err == nil && (response == nil || response.Cluster == nil) { + return nil, errors.NewNotFound(schema.GroupResource{}, *id) } - return response.Cluster, nil + return response.Cluster, err } func (c *client) ListClusters() (*console.ListClusters, error) { @@ -62,7 +63,7 @@ func (c *client) DeleteCluster(id string) (*console.ClusterFragment, error) { return response.DeleteCluster, nil } -func (c *client) ClusterExists(id *string) bool { +func (c *client) IsClusterExisting(id *string) bool { _, err := c.GetCluster(id) if errors.IsNotFound(err) { return false @@ -71,3 +72,12 @@ func (c *client) ClusterExists(id *string) bool { // We are assuming that if there is an error, and it is not ErrorNotFound then provider does not exist. return err == nil } + +func (c *client) IsClusterDeleting(id *string) bool { + cluster, err := c.GetCluster(id) + if err != nil { + return false + } + + return cluster != nil && cluster.DeletedAt != nil +} diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 265d9fbad..5601aad6e 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -39,7 +39,8 @@ type ConsoleClient interface { UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) ListClusters() (*console.ListClusters, error) DeleteCluster(id string) (*console.ClusterFragment, error) - ClusterExists(id *string) bool + IsClusterExisting(id *string) bool + IsClusterDeleting(id *string) bool CreateProvider(ctx context.Context, attributes console.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) GetProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) UpdateProvider(ctx context.Context, id string, attributes console.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 67b5f3cb3..c9495dbce 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -27,6 +27,10 @@ const ( FinalizerName = "deployments.plural.sh/cluster-protection" ) +var ( + requeue = ctrl.Result{RequeueAfter: RequeueAfter} +) + // Reconciler reconciles a Cluster object. type Reconciler struct { client.Client @@ -51,9 +55,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Handle cluster resource deletion - ret, err := r.addOrRemoveFinalizer(ctx, cluster) - if ret || err != nil { - return ctrl.Result{}, err + result, err := r.addOrRemoveFinalizer(ctx, cluster) + if result != nil { + return *result, err } var apiCluster *console.ClusterFragment @@ -74,7 +78,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco providerId = provider.Status.ID if providerId == nil { r.Log.Info("provider does not have ID set yet") - return ctrl.Result{RequeueAfter: RequeueAfter}, nil + return requeue, nil } err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) @@ -107,32 +111,38 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco r.Status.CurrentVersion = apiCluster.CurrentVersion r.Status.PingedAt = apiCluster.PingedAt r.Status.SHA = &sha + // TODO: Conditions, i.e. readonly, exists. }); err != nil { return ctrl.Result{}, err } - return ctrl.Result{RequeueAfter: RequeueAfter}, nil + return requeue, nil } -func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (bool, error) { - // If 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 to registering our finalizer. +func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (*ctrl.Result, error) { + // If 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 + // to registering our finalizer. if cluster.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(cluster, FinalizerName) { controllerutil.AddFinalizer(cluster, FinalizerName) if err := r.Update(ctx, cluster); err != nil { - return true, err + return &ctrl.Result{}, err } } // If object is being deleted if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { - // TODO: Add DeletedAt to queries, check if cluster is deleting and return if it is. + // If object is already being deleted from Console API requeue + if r.ConsoleClient.IsClusterDeleting(cluster.Status.ID) { + return &requeue, nil + } - // Remove cluster from Console API if it exists - if cluster.Status.ID != nil && r.ConsoleClient.ClusterExists(cluster.Status.ID) { + // Remove Cluster from Console API if it exists + if r.ConsoleClient.IsClusterExisting(cluster.Status.ID) { if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil { - // If fail to delete the external dependency here, return with error so that it can be retried. - return true, err + // if fail to delete the external dependency here, return with error + // so that it can be retried. + return &ctrl.Result{}, err } } @@ -140,15 +150,15 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 if controllerutil.ContainsFinalizer(cluster, FinalizerName) { controllerutil.RemoveFinalizer(cluster, FinalizerName) if err := r.Update(ctx, cluster); err != nil { - return true, err + return &ctrl.Result{}, err } } // Stop reconciliation as the item is being deleted - return true, nil + return &ctrl.Result{}, nil } - return false, nil + return nil, nil } func (r *Reconciler) updateStatus(ctx context.Context, cluster *v1alpha1.Cluster, patch func(cluster *v1alpha1.Cluster)) error { From b8d12e9bf9bb451694ccc184d20d6e2860d19148 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 11:42:38 +0100 Subject: [PATCH 055/198] refactor cluster reconciler --- .../apis/deployments/v1alpha1/cluster.go | 8 ++ .../pkg/cluster_controller/reconciler.go | 89 +++++++++---------- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 6af2add3b..474ba9071 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -126,6 +126,14 @@ type ClusterSpec struct { NodePools []ClusterNodePool `json:"nodePools"` } +func (cs *ClusterSpec) IsProviderRefRequired() bool { + return cs.Cloud != "byok" +} + +func (cs *ClusterSpec) HasProviderRef() bool { + return cs.ProviderRef != nil +} + type ClusterCloudSettings struct { // AWS cluster customizations. // +kubebuilder:validation:Optional diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index c9495dbce..3d0e56394 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -47,19 +47,25 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { - // Read cluster resource + // Read resource from Kubernetes cluster. cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { r.Log.Error(err, "unable to fetch cluster") return ctrl.Result{}, client.IgnoreNotFound(err) } - // Handle cluster resource deletion + // Handle resource deletion both in Kubernetes cluster and in Console. result, err := r.addOrRemoveFinalizer(ctx, cluster) if result != nil { return *result, err } + // Get Provider ID from the reference if it is set and ensure that owner reference is set properly. + providerId, result, err := r.getProviderIdAndSetOwnerRef(ctx, cluster) + if result != nil { + return *result, err + } + var apiCluster *console.ClusterFragment if cluster.Status.ID != nil { apiCluster, err = r.ConsoleClient.GetCluster(cluster.Status.ID) @@ -68,25 +74,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } } - var providerId *string - if cluster.Spec.Cloud != "byok" { - provider, err := r.getProvider(ctx, cluster) - if err != nil { - return ctrl.Result{}, err - } - - providerId = provider.Status.ID - if providerId == nil { - r.Log.Info("provider does not have ID set yet") - return requeue, nil - } - - err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) - if err != nil { - return ctrl.Result{}, err - } - } - if apiCluster == nil { apiCluster, err = r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) if err != nil { @@ -120,9 +107,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (*ctrl.Result, error) { - // If 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 - // to registering our finalizer. + // If 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 to registering our finalizer. if cluster.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(cluster, FinalizerName) { controllerutil.AddFinalizer(cluster, FinalizerName) if err := r.Update(ctx, cluster); err != nil { @@ -130,14 +116,14 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 } } - // If object is being deleted + // If object is being deleted. if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { - // If object is already being deleted from Console API requeue + // If object is already being deleted from Console API requeue. if r.ConsoleClient.IsClusterDeleting(cluster.Status.ID) { return &requeue, nil } - // Remove Cluster from Console API if it exists + // Remove Cluster from Console API if it exists. if r.ConsoleClient.IsClusterExisting(cluster.Status.ID) { if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil { // if fail to delete the external dependency here, return with error @@ -146,7 +132,7 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 } } - // If our finalizer is present, remove it + // If our finalizer is present, remove it. if controllerutil.ContainsFinalizer(cluster, FinalizerName) { controllerutil.RemoveFinalizer(cluster, FinalizerName) if err := r.Update(ctx, cluster); err != nil { @@ -154,13 +140,41 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 } } - // Stop reconciliation as the item is being deleted + // Stop reconciliation as the item is being deleted. return &ctrl.Result{}, nil } return nil, nil } +func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { + if cluster.Spec.IsProviderRefRequired() { + if !cluster.Spec.HasProviderRef() { + return nil, &ctrl.Result{}, fmt.Errorf("could not get provider, reference is not set but required") + } + + provider := &v1alpha1.Provider{} + if err := r.Get(ctx, types.NamespacedName{Name: cluster.Spec.ProviderRef.Name, Namespace: cluster.Spec.ProviderRef.Namespace}, provider); err != nil { + return nil, &ctrl.Result{}, fmt.Errorf("could not get provider, got error: %+v", err) + } + + providerId = provider.Status.ID + if providerId == nil { + r.Log.Info("provider does not have ID set yet") + return nil, &requeue, nil + } + + err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) + if err != nil { + return nil, &ctrl.Result{}, err + } + + return providerId, nil, nil + } + + return nil, nil, nil +} + func (r *Reconciler) updateStatus(ctx context.Context, cluster *v1alpha1.Cluster, patch func(cluster *v1alpha1.Cluster)) error { return retry.RetryOnConflict(retry.DefaultRetry, func() error { if err := r.Client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(cluster), cluster); err != nil { @@ -178,20 +192,3 @@ func (r *Reconciler) updateStatus(ctx context.Context, cluster *v1alpha1.Cluster return r.Client.Status().Patch(ctx, cluster, ctrlruntimeclient.MergeFrom(original)) }) } - -func (r *Reconciler) getProvider(ctx context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Provider, error) { - if cluster.Spec.ProviderRef == nil { - return nil, fmt.Errorf("could not get provider, reference is not set") - } - - provider := &v1alpha1.Provider{} - err := r.Get(ctx, types.NamespacedName{ - Name: cluster.Spec.ProviderRef.Name, - Namespace: cluster.Spec.ProviderRef.Namespace, - }, provider) - if err != nil { - return nil, fmt.Errorf("could not get provider, got error: %+v", err) - } - - return provider, nil -} From 84ca16aa9135ca4318ce81cf283853b9a4419ef0 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 12 Dec 2023 11:49:53 +0100 Subject: [PATCH 056/198] add generic update status method and optimize provider reconciler --- .../apis/deployments/v1alpha1/common.go | 8 ++++ .../apis/deployments/v1alpha1/provider.go | 10 ++++ controller/pkg/client/provider.go | 8 ++-- .../pkg/provider_reconciler/reconciler.go | 46 +++++++------------ .../reconciler_attributes.go | 4 +- controller/pkg/utils/kubernetes.go | 46 +++++++++++++++++++ 6 files changed, 86 insertions(+), 36 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index d9fb509fb..4a2fcfdca 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -65,3 +65,11 @@ func (t *Taint) Attributes() *console.TaintAttributes { // TaintEffect is the effect for a Kubernetes taint. type TaintEffect string + +// ConditionType TODO ... +type ConditionType string + +const ( + ReadOnlyConditionType = "readonly" + SynchronizedConditionType = "synchronized" +) diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index 1eaaada8d..faf486a69 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -2,6 +2,7 @@ package v1alpha1 import ( "context" + console "github.com/pluralsh/console-client-go" "github.com/samber/lo" v1 "k8s.io/api/core/v1" @@ -60,6 +61,10 @@ type Provider struct { Status ProviderStatus `json:"status,omitempty"` } +func (p *Provider) GetStatus() ProviderStatus { + return p.Status +} + func (p *Provider) Diff(ctx context.Context, getter CloudSettingsGetter, hasher Hasher) (changed bool, sha string, err error) { cloudSettings, err := getter(ctx, p.Spec) if err != nil { @@ -132,11 +137,16 @@ func (p *ProviderSpec) UpdateAttributes(ctx context.Context, cloudSettingsGetter type ProviderStatus struct { // ID of the provider in the Console API. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string ID *string `json:"id,omitempty"` // SHA of last applied configuration. // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string SHA *string `json:"sha,omitempty"` + // Existing flag. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + Existing *string `json:"existing,omitempty"` } func (p *ProviderStatus) GetID() string { diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go index d2e048243..42f6d7eed 100644 --- a/controller/pkg/client/provider.go +++ b/controller/pkg/client/provider.go @@ -2,6 +2,7 @@ package client import ( "context" + gqlgenclient "github.com/Yamashou/gqlgenc/client" gqlclient "github.com/pluralsh/console-client-go" "k8s.io/apimachinery/pkg/api/errors" @@ -47,13 +48,10 @@ func (c *client) IsProviderExists(ctx context.Context, id string) bool { } func (c *client) IsProviderDeleting(ctx context.Context, id string) bool { - _, err := c.GetProvider(ctx, id) + provider, err := c.GetProvider(ctx, id) if err != nil { return false } - return false - - // TODO: Update client to support DeletedAt - //return provider != nil && provider.DeletedAt != nil + return provider != nil && provider.DeletedAt != nil } diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index 2e21c9164..015a327a1 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -2,20 +2,20 @@ package providerreconciler import ( "context" - "fmt" + "reflect" + "time" + console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/util/retry" - "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "time" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/utils" ) // Reconciler reconciles a v1alpha1.Provider object. @@ -48,30 +48,34 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, client.IgnoreNotFound(err) } - // Handle resource deletion + // TODO: Try reading from API to see if object already exists + + // Handle proper resource deletion via finalizer result, err := r.addOrRemoveFinalizer(ctx, provider) if result != nil { return *result, err } // Get Provider SHA that can be saved back in the status to check for changes - _, sha, err := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) + changed, sha, err := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) if err != nil { log.Error(err, "unable to calculate provider SHA") return ctrl.Result{}, err } // Sync Provider CRD with the Console API - apiProvider, err := r.updateOrGetProvider(ctx, provider) + apiProvider, err := r.updateOrGetProvider(ctx, provider, changed) if err != nil { log.Error(err, "unable to create or update provider") return ctrl.Result{}, err } // Sync back Provider to crd status - if err = r.updateStatus(ctx, &provider, func(p *v1alpha1.Provider) { + if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) bool { p.Status.ID = &apiProvider.ID p.Status.SHA = &sha + + return reflect.DeepEqual(original.Status, p.Status) }); err != nil { return ctrl.Result{}, err } @@ -121,8 +125,7 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1 return nil, nil } -func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1.Provider) (*console.ClusterProviderFragment, error) { - changed, _, _ := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) +func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { exists := r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) // Update only if Provider has changed @@ -149,23 +152,6 @@ func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1. return r.ConsoleClient.CreateProvider(ctx, attributes) } -func (r *Reconciler) updateStatus(ctx context.Context, provider *v1alpha1.Provider, patch func(provider *v1alpha1.Provider)) error { - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := r.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil { - return fmt.Errorf("could not fetch current provider state, got error: %+v", err) - } - - original := provider.DeepCopy() - patch(provider) - - if reflect.DeepEqual(original.Status, provider.Status) { - return nil - } - - return r.Client.Status().Patch(ctx, provider, client.MergeFrom(original)) - }) -} - // SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { mgr.GetLogger().Info("starting reconciler", "reconciler", "provider_reconciler") diff --git a/controller/pkg/provider_reconciler/reconciler_attributes.go b/controller/pkg/provider_reconciler/reconciler_attributes.go index d6a93b959..9b2eca1d6 100644 --- a/controller/pkg/provider_reconciler/reconciler_attributes.go +++ b/controller/pkg/provider_reconciler/reconciler_attributes.go @@ -3,10 +3,12 @@ package providerreconciler import ( "context" "fmt" + console "github.com/pluralsh/console-client-go" + corev1 "k8s.io/api/core/v1" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/kubernetes" - corev1 "k8s.io/api/core/v1" ) func (r *Reconciler) missingCredentialKeyError(key string) error { diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index 97b5c14b3..f5ab61056 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -2,6 +2,7 @@ package utils import ( "context" + "fmt" "reflect" "k8s.io/apimachinery/pkg/runtime" @@ -34,3 +35,48 @@ func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner return client.Patch(ctx, object, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) }) } + +func TryAddControllerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, controlled ctrlruntimeclient.Object, scheme *runtime.Scheme) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(controlled), controlled); err != nil { + return err + } + + if owner.GetDeletionTimestamp() != nil || controlled.GetDeletionTimestamp() != nil { + return nil + } + + original := controlled.DeepCopyObject().(ctrlruntimeclient.Object) + + err := controllerutil.SetControllerReference(owner, controlled, scheme) + if err != nil { + return err + } + + if reflect.DeepEqual(original.GetOwnerReferences(), controlled.GetOwnerReferences()) { + return nil + } + + return client.Patch(ctx, controlled, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) + }) +} + +// Patcher TODO ... +type Patcher[PatchObject ctrlruntimeclient.Object] func(object PatchObject, original PatchObject) bool + +// TryUpdateStatus TODO ... +func TryUpdateStatus[PatchObject ctrlruntimeclient.Object](ctx context.Context, client ctrlruntimeclient.Client, object PatchObject, patch Patcher[PatchObject]) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(object), object); err != nil { + return fmt.Errorf("could not fetch current %s/%s state, got error: %+v", object.GetName(), object.GetNamespace(), err) + } + + original := object.DeepCopyObject().(PatchObject) + + if patch(object, original) { + return nil + } + + return client.Status().Patch(ctx, object, ctrlruntimeclient.MergeFrom(original)) + }) +} From 2ca0d19a78139cdb0f56e56e155142a6ffd2a6ba Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 11:59:20 +0100 Subject: [PATCH 057/198] refactor cluster reconciler --- controller/apis/deployments/v1alpha1/cluster.go | 8 ++++++++ controller/pkg/cluster_controller/reconciler.go | 9 ++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 474ba9071..c8f1ad992 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -363,3 +363,11 @@ type ClusterStatus struct { // +kubebuilder:validation:Type:=string PingedAt *string `json:"pingedAt,omitempty"` } + +func (cs *ClusterStatus) HasID() bool { + return cs.ID != nil && len(*cs.ID) > 0 +} + +func (cs *ClusterStatus) HasSHA() bool { + return cs.SHA != nil && len(*cs.SHA) > 0 +} diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 3d0e56394..d8acb3e34 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -67,7 +67,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } var apiCluster *console.ClusterFragment - if cluster.Status.ID != nil { + if cluster.Status.HasID() { apiCluster, err = r.ConsoleClient.GetCluster(cluster.Status.ID) if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err @@ -85,7 +85,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco if err != nil { return ctrl.Result{}, err } - if cluster.Status.ID != nil && cluster.Status.SHA != nil && cluster.Status.SHA != &sha { + if cluster.Status.HasID() && cluster.Status.HasSHA() && cluster.Status.SHA != &sha { apiCluster, err = r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) if err != nil { return ctrl.Result{}, err @@ -158,8 +158,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, &ctrl.Result{}, fmt.Errorf("could not get provider, got error: %+v", err) } - providerId = provider.Status.ID - if providerId == nil { + if !provider.Status.HasID() { r.Log.Info("provider does not have ID set yet") return nil, &requeue, nil } @@ -169,7 +168,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, &ctrl.Result{}, err } - return providerId, nil, nil + return provider.Status.ID, nil, nil } return nil, nil, nil From e8e8b546199aaa1ac2b24df6f674d4974538b4db Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 12 Dec 2023 12:00:39 +0100 Subject: [PATCH 058/198] update tryUpdateStatus --- .../apis/deployments/v1alpha1/zz_generated.deepcopy.go | 5 +++++ .../config/crd/bases/deployments.plural.sh_providers.yaml | 3 +++ controller/pkg/provider_reconciler/reconciler.go | 5 ++--- controller/pkg/utils/kubernetes.go | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 0edfaa5cb..bea5f9395 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -631,6 +631,11 @@ func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { *out = new(string) **out = **in } + if in.Existing != nil { + in, out := &in.Existing, &out.Existing + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index a7283fa8e..d90a6aee1 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -123,6 +123,9 @@ spec: status: description: ProviderStatus ... properties: + existing: + description: Existing flag. + type: string id: description: ID of the provider in the Console API. type: string diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index 015a327a1..67b6c69e4 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -2,7 +2,6 @@ package providerreconciler import ( "context" - "reflect" "time" console "github.com/pluralsh/console-client-go" @@ -71,11 +70,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Sync back Provider to crd status - if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) bool { + if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) (any, any) { p.Status.ID = &apiProvider.ID p.Status.SHA = &sha - return reflect.DeepEqual(original.Status, p.Status) + return original.Status, p.Status }); err != nil { return ctrl.Result{}, err } diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index f5ab61056..0d62fed28 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -62,7 +62,7 @@ func TryAddControllerRef(ctx context.Context, client ctrlruntimeclient.Client, o } // Patcher TODO ... -type Patcher[PatchObject ctrlruntimeclient.Object] func(object PatchObject, original PatchObject) bool +type Patcher[PatchObject ctrlruntimeclient.Object] func(object PatchObject, original PatchObject) (any, any) // TryUpdateStatus TODO ... func TryUpdateStatus[PatchObject ctrlruntimeclient.Object](ctx context.Context, client ctrlruntimeclient.Client, object PatchObject, patch Patcher[PatchObject]) error { @@ -73,7 +73,7 @@ func TryUpdateStatus[PatchObject ctrlruntimeclient.Object](ctx context.Context, original := object.DeepCopyObject().(PatchObject) - if patch(object, original) { + if reflect.DeepEqual(patch(object, original)) { return nil } From 966ed9e0c33fff620db199306aa92d89137fb6db Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 12:07:12 +0100 Subject: [PATCH 059/198] use generic status update method --- .../pkg/cluster_controller/reconciler.go | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index d8acb3e34..37925491f 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -3,7 +3,6 @@ package cluster_controller import ( "context" "fmt" - "reflect" "time" "github.com/go-logr/logr" @@ -14,10 +13,8 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -46,6 +43,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +// TODO: Conditions, i.e. readonly, exists. func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { // Read resource from Kubernetes cluster. cluster := &v1alpha1.Cluster{} @@ -92,13 +90,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } } - if err := r.updateStatus(ctx, cluster, func(r *v1alpha1.Cluster) { - r.Status.ID = &apiCluster.ID - r.Status.KasURL = apiCluster.KasURL - r.Status.CurrentVersion = apiCluster.CurrentVersion - r.Status.PingedAt = apiCluster.PingedAt - r.Status.SHA = &sha - // TODO: Conditions, i.e. readonly, exists. + // Update resource status. + if err = utils.TryUpdateStatus[*v1alpha1.Cluster](ctx, r.Client, cluster, func(c *v1alpha1.Cluster, original *v1alpha1.Cluster) (any, any) { + c.Status.ID = &apiCluster.ID + c.Status.KasURL = apiCluster.KasURL + c.Status.CurrentVersion = apiCluster.CurrentVersion + c.Status.PingedAt = apiCluster.PingedAt + c.Status.SHA = &sha + + return original.Status, c.Status }); err != nil { return ctrl.Result{}, err } @@ -173,21 +173,3 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, nil, nil } - -func (r *Reconciler) updateStatus(ctx context.Context, cluster *v1alpha1.Cluster, patch func(cluster *v1alpha1.Cluster)) error { - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := r.Client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(cluster), cluster); err != nil { - return fmt.Errorf("could not fetch current cluster state, got error: %+v", err) - } - - original := cluster.DeepCopy() - - patch(cluster) - - if reflect.DeepEqual(original.Status, cluster.Status) { - return nil - } - - return r.Client.Status().Patch(ctx, cluster, ctrlruntimeclient.MergeFrom(original)) - }) -} From aa44cc22139a1d9788f91b5dc71b1a84b368ff88 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 12:20:05 +0100 Subject: [PATCH 060/198] add existing field for cluster --- controller/apis/deployments/v1alpha1/cluster.go | 5 +++++ .../deployments/v1alpha1/zz_generated.deepcopy.go | 5 +++++ .../crd/bases/deployments.plural.sh_clusters.yaml | 5 +++++ controller/pkg/cluster_controller/reconciler.go | 12 +++++++----- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index c8f1ad992..fc9d375b0 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -348,6 +348,11 @@ type ClusterStatus struct { // +kubebuilder:validation:Type:=string SHA *string `json:"sha,omitempty"` + // Existing if set to true, then Console will not be synced with the data from this resource. + // It can be used to read already existing resources. + // +kubebuilder:validation:Optional + Existing *bool `json:"existing,omitempty"` + // CurrentVersion contains current Kubernetes version this cluster is using. // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index bea5f9395..7e059f5b6 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -393,6 +393,11 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = new(string) **out = **in } + if in.Existing != nil { + in, out := &in.Existing, &out.Existing + *out = new(bool) + **out = **in + } if in.CurrentVersion != nil { in, out := &in.CurrentVersion, &out.CurrentVersion *out = new(string) diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 10f9bc800..1934e9fe5 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -287,6 +287,11 @@ spec: description: CurrentVersion contains current Kubernetes version this cluster is using. type: string + existing: + description: Existing if set to true, then Console will not be synced + with the data from this resource. It can be used to read already + existing resources. + type: boolean id: description: ID from Console. type: string diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 37925491f..0f52c9e97 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -43,7 +43,6 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -// TODO: Conditions, i.e. readonly, exists. func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { // Read resource from Kubernetes cluster. cluster := &v1alpha1.Cluster{} @@ -64,6 +63,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return *result, err } + // Calculate SHA to detect changes that should be applied in the console. + sha, err := utils.HashObject(cluster.UpdateAttributes()) + if err != nil { + return ctrl.Result{}, err + } + var apiCluster *console.ClusterFragment if cluster.Status.HasID() { apiCluster, err = r.ConsoleClient.GetCluster(cluster.Status.ID) @@ -79,10 +84,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } } - sha, err := utils.HashObject(cluster.UpdateAttributes()) - if err != nil { - return ctrl.Result{}, err - } if cluster.Status.HasID() && cluster.Status.HasSHA() && cluster.Status.SHA != &sha { apiCluster, err = r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) if err != nil { @@ -97,6 +98,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco c.Status.CurrentVersion = apiCluster.CurrentVersion c.Status.PingedAt = apiCluster.PingedAt c.Status.SHA = &sha + // TODO: Existing. return original.Status, c.Status }); err != nil { From 83c8fc31aad6a4efdcbde0b42a56a2a5c951b1b6 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 12:26:32 +0100 Subject: [PATCH 061/198] fix git repo controller --- .../deployments/v1alpha1/git_repository.go | 2 +- ...deployments.plural.sh_gitrepositories.yaml | 2 +- .../gitrepository_controller/controller.go | 27 +++++++++++++------ .../pkg/service_controller/controller.go | 3 +++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index a44e7bb40..59bd3f89f 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -38,7 +38,7 @@ type GitRepositoryStatus struct { Id *string `json:"id,omitempty"` // +optional Sha string `json:"sha,omitempty"` - // +kubebuilder:default:=false + // +kubebuilder:default:=true Existing bool `json:"existing"` } diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index 3b67151e4..29d8abfe4 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -67,7 +67,7 @@ spec: status: properties: existing: - default: false + default: true type: boolean health: description: Health status. diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index edf2e827e..c66fd44a9 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -48,7 +48,6 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - existing := true repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { if apierrors.IsNotFound(err) { @@ -73,16 +72,24 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if existingRepo == nil && repo.Status.Existing == true { - msg := "existing Git repository was deleted from console" + msg := "existing Git repository was deleted from the console" r.Log.Info(msg) if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { r.Status.Message = &msg - r.Status.Id = nil - r.Status.Existing = existing + r.Status.Health = v1alpha1.GitHealthFailed + }); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{ + RequeueAfter: 30 * time.Second, + }, nil + } + if repo.Status.Id == nil { + if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + r.Status.Existing = true }); err != nil { return ctrl.Result{}, err } - return ctrl.Result{}, nil } if existingRepo == nil { if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { @@ -92,11 +99,16 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } + if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + r.Status.Existing = false + }); err != nil { + return ctrl.Result{}, err + } existingRepo = resp.CreateGitRepository - existing = false + } - if repo.Status.Sha != "" && repo.Status.Sha != sha && !existing { + if repo.Status.Sha != "" && repo.Status.Sha != sha && !repo.Status.Existing { _, err := r.ConsoleClient.UpdateRepository(existingRepo.ID, console.GitAttributes{ URL: repo.Spec.Url, PrivateKey: cred.PrivateKey, @@ -116,7 +128,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } r.Status.Sha = sha - r.Status.Existing = existing }); err != nil { return ctrl.Result{}, err diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 5e8372589..9b2cfb36d 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -89,6 +89,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if existingService == nil { + if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + return ctrl.Result{}, err + } _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) if err != nil { return ctrl.Result{}, err From a58689b7f93d1e7d06ad641024f0ae75bf50941a Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 13:16:26 +0100 Subject: [PATCH 062/198] use generic update satatus --- .../deployments/v1alpha1/git_repository.go | 5 +-- ...deployments.plural.sh_gitrepositories.yaml | 1 - .../gitrepository_controller/controller.go | 41 ++++--------------- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 59bd3f89f..031a772b3 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -37,9 +37,8 @@ type GitRepositoryStatus struct { // +optional Id *string `json:"id,omitempty"` // +optional - Sha string `json:"sha,omitempty"` - // +kubebuilder:default:=true - Existing bool `json:"existing"` + Sha string `json:"sha,omitempty"` + Existing bool `json:"existing"` } // +kubebuilder:object:root=true diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index 29d8abfe4..206b6435b 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -67,7 +67,6 @@ spec: status: properties: existing: - default: true type: boolean health: description: Health status. diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index c66fd44a9..c2be7b8af 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -3,7 +3,6 @@ package gitrepositorycontroller import ( "context" "fmt" - "reflect" "time" "github.com/go-logr/logr" @@ -17,10 +16,8 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -74,9 +71,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if existingRepo == nil && repo.Status.Existing == true { msg := "existing Git repository was deleted from the console" r.Log.Info(msg) - if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { r.Status.Message = &msg r.Status.Health = v1alpha1.GitHealthFailed + return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } @@ -85,8 +83,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, nil } if repo.Status.Id == nil { - if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { r.Status.Existing = true + return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } @@ -99,8 +98,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { r.Status.Existing = false + return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } @@ -121,14 +121,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } } - if err := UpdateReposStatus(ctx, r.Client, repo, func(r *v1alpha1.GitRepository) { + if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { r.Status.Message = existingRepo.Error r.Status.Id = &existingRepo.ID if existingRepo.Health != nil { r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } r.Status.Sha = sha - + return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } @@ -216,28 +216,3 @@ func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, return existingRepos.GitRepository, nil } - -type RepoPatchFunc func(repo *v1alpha1.GitRepository) - -func UpdateReposStatus(ctx context.Context, client ctrlruntimeclient.Client, repository *v1alpha1.GitRepository, patch RepoPatchFunc) error { - key := ctrlruntimeclient.ObjectKeyFromObject(repository) - - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - // fetch the current state of the cluster - if err := client.Get(ctx, key, repository); err != nil { - return err - } - - // modify it - original := repository.DeepCopy() - patch(repository) - - // save some work - if reflect.DeepEqual(original.Status, repository.Status) { - return nil - } - - // update the status - return client.Status().Patch(ctx, repository, ctrlruntimeclient.MergeFrom(original)) - }) -} From 18dab37e34882605540f881187919fb212fb4c17 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 13:24:20 +0100 Subject: [PATCH 063/198] handle existing clusters --- .../apis/deployments/v1alpha1/cluster.go | 9 ++++ .../bases/deployments.plural.sh_clusters.yaml | 2 +- controller/pkg/client/cluster.go | 9 ++++ controller/pkg/client/console.go | 1 + .../pkg/cluster_controller/reconciler.go | 53 ++++++++++++++++++- 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index fc9d375b0..50d29aabe 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -79,6 +79,7 @@ func (c *Cluster) UpdateAttributes() console.ClusterUpdateAttributes { type ClusterSpec struct { // Handle is a short, unique human-readable name used to identify this cluster. // Does not necessarily map to the cloud resource name. + // This has to be specified in order to adopt existing cluster. // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string // +kubebuilder:example:=myclusterhandle @@ -126,6 +127,10 @@ type ClusterSpec struct { NodePools []ClusterNodePool `json:"nodePools"` } +func (cs *ClusterSpec) HasHandle() bool { + return cs.Handle != nil +} + func (cs *ClusterSpec) IsProviderRefRequired() bool { return cs.Cloud != "byok" } @@ -376,3 +381,7 @@ func (cs *ClusterStatus) HasID() bool { func (cs *ClusterStatus) HasSHA() bool { return cs.SHA != nil && len(*cs.SHA) > 0 } + +func (cs *ClusterStatus) IsExisting() bool { + return cs.Existing != nil && *cs.Existing +} diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 1934e9fe5..d5c58f95e 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -147,7 +147,7 @@ spec: handle: description: Handle is a short, unique human-readable name used to identify this cluster. Does not necessarily map to the cloud resource - name. + name. This has to be specified in order to adopt existing cluster. example: myclusterhandle type: string nodePools: diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go index 7a3d5ebe9..8ee507ac4 100644 --- a/controller/pkg/client/cluster.go +++ b/controller/pkg/client/cluster.go @@ -50,6 +50,15 @@ func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { return response.Cluster, err } +func (c *client) GetClusterByHandle(handle *string) (*console.ClusterFragment, error) { + response, err := c.consoleClient.GetClusterByHandle(c.ctx, handle) + if err == nil && (response == nil || response.Cluster == nil) { + return nil, errors.NewNotFound(schema.GroupResource{}, *handle) + } + + return response.Cluster, err +} + func (c *client) ListClusters() (*console.ListClusters, error) { return c.consoleClient.ListClusters(c.ctx, nil, nil, nil) } diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index d8d8020a8..d4891b7d8 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -35,6 +35,7 @@ type ConsoleClient interface { GetRepository(url *string) (*console.GetGitRepository, error) CreateService(clusterId *string, attributes console.ServiceDeploymentAttributes) (*console.ServiceDeploymentFragment, error) GetCluster(id *string) (*console.ClusterFragment, error) + GetClusterByHandle(handle *string) (*console.ClusterFragment, error) CreateCluster(attrs console.ClusterAttributes) (*console.ClusterFragment, error) UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) ListClusters() (*console.ListClusters, error) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 0f52c9e97..beed834b3 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -11,6 +11,7 @@ import ( consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/utils" + "github.com/samber/lo" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -51,6 +52,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, client.IgnoreNotFound(err) } + // Handle existing resource. + exists, err := r.isExistingResource(cluster) + if err != nil { + return ctrl.Result{}, err + } + if exists { + return r.handleExistingResource(ctx, cluster) + } + // Handle resource deletion both in Kubernetes cluster and in Console. result, err := r.addOrRemoveFinalizer(ctx, cluster) if result != nil { @@ -98,7 +108,48 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco c.Status.CurrentVersion = apiCluster.CurrentVersion c.Status.PingedAt = apiCluster.PingedAt c.Status.SHA = &sha - // TODO: Existing. + c.Status.Existing = lo.ToPtr(false) + + return original.Status, c.Status + }); err != nil { + return ctrl.Result{}, err + } + + return requeue, nil +} + +func (r *Reconciler) isExistingResource(cluster *v1alpha1.Cluster) (bool, error) { + if cluster.Status.IsExisting() { + return true, nil + } + + if !cluster.Spec.HasHandle() { + return false, nil + } + + _, err := r.ConsoleClient.GetClusterByHandle(cluster.Spec.Handle) + if errors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + + return !cluster.Status.HasID(), nil +} + +func (r *Reconciler) handleExistingResource(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { + apiCluster, err := r.ConsoleClient.GetClusterByHandle(cluster.Spec.Handle) + if err != nil { + return ctrl.Result{}, err + } + + if err = utils.TryUpdateStatus[*v1alpha1.Cluster](ctx, r.Client, cluster, func(c *v1alpha1.Cluster, original *v1alpha1.Cluster) (any, any) { + c.Status.ID = &apiCluster.ID + c.Status.KasURL = apiCluster.KasURL + c.Status.CurrentVersion = apiCluster.CurrentVersion + c.Status.PingedAt = apiCluster.PingedAt + c.Status.Existing = lo.ToPtr(true) return original.Status, c.Status }); err != nil { From 0bf49dbdd746d5d2950f383313acd3be08a33a3e Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 14:04:38 +0100 Subject: [PATCH 064/198] update cluster sync --- .../pkg/cluster_controller/reconciler.go | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index beed834b3..20fad4973 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -53,15 +53,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Handle existing resource. - exists, err := r.isExistingResource(cluster) + existing, err := r.isExistingResource(cluster) if err != nil { return ctrl.Result{}, err } - if exists { + if existing { return r.handleExistingResource(ctx, cluster) } - // Handle resource deletion both in Kubernetes cluster and in Console. + // Handle resource deletion both in Kubernetes cluster and in Console API. result, err := r.addOrRemoveFinalizer(ctx, cluster) if result != nil { return *result, err @@ -73,32 +73,16 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return *result, err } - // Calculate SHA to detect changes that should be applied in the console. + // Calculate SHA to detect changes that should be applied in the Console API. sha, err := utils.HashObject(cluster.UpdateAttributes()) if err != nil { return ctrl.Result{}, err } - var apiCluster *console.ClusterFragment - if cluster.Status.HasID() { - apiCluster, err = r.ConsoleClient.GetCluster(cluster.Status.ID) - if err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, err - } - } - - if apiCluster == nil { - apiCluster, err = r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) - if err != nil { - return ctrl.Result{}, err - } - } - - if cluster.Status.HasID() && cluster.Status.HasSHA() && cluster.Status.SHA != &sha { - apiCluster, err = r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) - if err != nil { - return ctrl.Result{}, err - } + // Sync resource with Console API. + apiCluster, err := r.syncCluster(cluster, providerId, &sha) + if err != nil { + return ctrl.Result{}, err } // Update resource status. @@ -226,3 +210,15 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, nil, nil } + +func (r *Reconciler) syncCluster(cluster *v1alpha1.Cluster, providerId *string, sha *string) (*console.ClusterFragment, error) { + if !cluster.Status.HasID() { + return r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) + } + + if cluster.Status.HasSHA() && cluster.Status.SHA != sha { + return r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) + } + + return r.ConsoleClient.GetCluster(cluster.Status.ID) +} From 77ef03d92064bfb30567fb98c78c3558b061fd05 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 14:07:27 +0100 Subject: [PATCH 065/198] make cloud optional for existing resources --- controller/apis/deployments/v1alpha1/cluster.go | 2 +- controller/config/crd/bases/deployments.plural.sh_clusters.yaml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 50d29aabe..982d58e1b 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -96,7 +96,7 @@ type ClusterSpec struct { ProviderRef *corev1.ObjectReference `json:"providerRef,omitempty"` // Cloud provider to use for this cluster. - // +kubebuilder:validation:Required + // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string // +kubebuilder:validation:Enum=aws;azure;gcp;byok // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Cloud is immutable" diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index d5c58f95e..421b2116d 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -278,8 +278,6 @@ spec: skipped only for BYOK. example: 1.25.11 type: string - required: - - cloud type: object status: properties: From f0ed5cb6960280db191e954160a5445a19836dac Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 14:20:23 +0100 Subject: [PATCH 066/198] minor improvements --- controller/config/crd/examples/cluster_aws.yaml | 13 ++++++++++--- controller/pkg/cluster_controller/reconciler.go | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/controller/config/crd/examples/cluster_aws.yaml b/controller/config/crd/examples/cluster_aws.yaml index c8df9de0a..0af90f11e 100644 --- a/controller/config/crd/examples/cluster_aws.yaml +++ b/controller/config/crd/examples/cluster_aws.yaml @@ -1,4 +1,12 @@ apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: aws +spec: + cloud: aws + name: aws +--- +apiVersion: deployments.plural.sh/v1alpha1 kind: Cluster metadata: name: aws @@ -8,9 +16,8 @@ spec: cloud: aws version: "1.24" protect: false -# TODO: Add provider reference. -# providerRef: -# ... + providerRef: + name: aws cloudSettings: aws: region: us-east-1 diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 20fad4973..291aaa6ac 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -191,7 +191,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v } provider := &v1alpha1.Provider{} - if err := r.Get(ctx, types.NamespacedName{Name: cluster.Spec.ProviderRef.Name, Namespace: cluster.Spec.ProviderRef.Namespace}, provider); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: cluster.Spec.ProviderRef.Name}, provider); err != nil { return nil, &ctrl.Result{}, fmt.Errorf("could not get provider, got error: %+v", err) } From 4a10cca5fa863366983dad529c35d9d39c6a4064 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 12 Dec 2023 14:20:39 +0100 Subject: [PATCH 067/198] update provider schema and logic --- .../apis/deployments/v1alpha1/common.go | 8 --- .../apis/deployments/v1alpha1/provider.go | 59 ++++++++-------- .../v1alpha1/zz_generated.deepcopy.go | 2 +- .../deployments.plural.sh_providers.yaml | 10 +-- .../config/crd/examples/provider_aws.yaml | 14 ++++ .../config/crd/examples/provider_byok.yaml | 8 --- controller/go.mod | 2 +- controller/go.sum | 4 +- controller/pkg/client/console.go | 3 + controller/pkg/client/provider.go | 23 +++++++ .../pkg/provider_reconciler/reconciler.go | 67 ++++++++++++++++++- .../reconciler_attributes.go | 25 +++++-- controller/pkg/utils/kubernetes.go | 2 +- 13 files changed, 163 insertions(+), 64 deletions(-) create mode 100644 controller/config/crd/examples/provider_aws.yaml delete mode 100644 controller/config/crd/examples/provider_byok.yaml diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index 4a2fcfdca..d9fb509fb 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -65,11 +65,3 @@ func (t *Taint) Attributes() *console.TaintAttributes { // TaintEffect is the effect for a Kubernetes taint. type TaintEffect string - -// ConditionType TODO ... -type ConditionType string - -const ( - ReadOnlyConditionType = "readonly" - SynchronizedConditionType = "synchronized" -) diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index faf486a69..ebdd118e4 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -16,7 +16,7 @@ func init() { // CloudSettingsGetter is just a helper function that can be implemented to properly // build Console API attributes // +kubebuilder:object:generate:=false -type CloudSettingsGetter func(context.Context, ProviderSpec) (*console.CloudProviderSettingsAttributes, error) +type CloudSettingsGetter func(context.Context, Provider) (*console.CloudProviderSettingsAttributes, error) // Hasher // +kubebuilder:object:generate:=false @@ -61,12 +61,25 @@ type Provider struct { Status ProviderStatus `json:"status,omitempty"` } -func (p *Provider) GetStatus() ProviderStatus { - return p.Status +func (p *Provider) Attributes(ctx context.Context, cloudSettingsGetter CloudSettingsGetter) (console.ClusterProviderAttributes, error) { + cloudSettings, err := cloudSettingsGetter(ctx, *p) + return console.ClusterProviderAttributes{ + Name: p.Spec.Name, + Namespace: &p.Spec.Namespace, + Cloud: p.Spec.Cloud.Attribute(), + CloudSettings: cloudSettings, + }, err +} + +func (p *Provider) UpdateAttributes(ctx context.Context, cloudSettingsGetter CloudSettingsGetter) (console.ClusterProviderUpdateAttributes, error) { + cloudSettings, err := cloudSettingsGetter(ctx, *p) + return console.ClusterProviderUpdateAttributes{ + CloudSettings: cloudSettings, + }, err } func (p *Provider) Diff(ctx context.Context, getter CloudSettingsGetter, hasher Hasher) (changed bool, sha string, err error) { - cloudSettings, err := getter(ctx, p.Spec) + cloudSettings, err := getter(ctx, *p) if err != nil { return false, "", err } @@ -89,24 +102,24 @@ type ProviderList struct { } // ProviderSpec ... -// +kubebuilder:validation:Validation:rule="(self.cloud == 'aws' && has(self.cloudSettings.aws)) || (self.cloud == 'gcp' && has(self.cloudSettings.gcp)) || (self.cloud == 'azure' && has(self.cloudSettings.azure))",message="Cloud Settings must be provided only for matching Cloud." type ProviderSpec struct { // Cloud is the name of the cloud service for the Provider. // One of (CloudProvider): [gcp, aws, azure] - // +kubebuilder:example:=byok + // +kubebuilder:example:=aws // +kubebuilder:validation:Required // +kubebuilder:validation:Type:=string - // +kubebuilder:validation:Enum:=byok;gcp;aws;azure + // +kubebuilder:validation:Enum:=gcp;aws;azure // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Cloud is immutable" Cloud CloudProvider `json:"cloud"` // CloudSettings reference cloud provider credentials secrets used for provisioning the Cluster. // Not required when Cloud is set to CloudProvider(BYOK). // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=object // +structType=atomic - CloudSettings *CloudProviderSettings `json:"cloudSettings,omitempty"` + CloudSettings *CloudProviderSettings `json:"cloudSettings"` // Name is a human-readable name of the Provider. // +kubebuilder:example:=gcp-provider - // +kubebuilder:validation:Required + // +kubebuilder:validation:Optional // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Name is immutable" Name string `json:"name"` // Namespace is the namespace ClusterAPI resources are deployed into. @@ -116,23 +129,6 @@ type ProviderSpec struct { Namespace string `json:"namespace,omitempty"` } -func (p *ProviderSpec) Attributes(ctx context.Context, cloudSettingsGetter CloudSettingsGetter) (console.ClusterProviderAttributes, error) { - cloudSettings, err := cloudSettingsGetter(ctx, *p) - return console.ClusterProviderAttributes{ - Name: p.Name, - Namespace: &p.Namespace, - Cloud: p.Cloud.Attribute(), - CloudSettings: cloudSettings, - }, err -} - -func (p *ProviderSpec) UpdateAttributes(ctx context.Context, cloudSettingsGetter CloudSettingsGetter) (console.ClusterProviderUpdateAttributes, error) { - cloudSettings, err := cloudSettingsGetter(ctx, *p) - return console.ClusterProviderUpdateAttributes{ - CloudSettings: cloudSettings, - }, err -} - // ProviderStatus ... type ProviderStatus struct { // ID of the provider in the Console API. @@ -143,10 +139,11 @@ type ProviderStatus struct { // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string SHA *string `json:"sha,omitempty"` - // Existing flag. + // Existing flag is set to true when Console API object already exists when CRD is created. + // CRD is then set to read-only mode and does not update Console API from CRD. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Type:=string - Existing *string `json:"existing,omitempty"` + // +kubebuilder:validation:Type:=boolean + Existing *bool `json:"existing,omitempty"` } func (p *ProviderStatus) GetID() string { @@ -180,3 +177,7 @@ func (p *ProviderStatus) IsSHAEqual(sha string) bool { return p.GetSHA() == sha } + +func (p *ProviderStatus) IsExisting() bool { + return p.Existing != nil && *p.Existing +} diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index bea5f9395..130d8f58c 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -633,7 +633,7 @@ func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { } if in.Existing != nil { in, out := &in.Existing, &out.Existing - *out = new(string) + *out = new(bool) **out = **in } } diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index d90a6aee1..015d47adf 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -52,11 +52,10 @@ spec: description: 'Cloud is the name of the cloud service for the Provider. One of (CloudProvider): [gcp, aws, azure]' enum: - - byok - gcp - aws - azure - example: byok + example: aws type: string cloudSettings: description: CloudSettings reference cloud provider credentials secrets @@ -118,14 +117,15 @@ spec: type: string required: - cloud - - name type: object status: description: ProviderStatus ... properties: existing: - description: Existing flag. - type: string + description: Existing flag is set to true when Console API object + already exists when CRD is created. CRD is then set to read-only + mode and does not update Console API from CRD. + type: boolean id: description: ID of the provider in the Console API. type: string diff --git a/controller/config/crd/examples/provider_aws.yaml b/controller/config/crd/examples/provider_aws.yaml new file mode 100644 index 000000000..c5fe7af83 --- /dev/null +++ b/controller/config/crd/examples/provider_aws.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: gcp +spec: + name: gcp + namespace: gcp-capi + cloud: gcp + cloudSettings: + gcp: + name: credentials + namespace: gcp-capi + diff --git a/controller/config/crd/examples/provider_byok.yaml b/controller/config/crd/examples/provider_byok.yaml deleted file mode 100644 index acf860069..000000000 --- a/controller/config/crd/examples/provider_byok.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -apiVersion: deployments.plural.sh/v1alpha1 -kind: Provider -metadata: - name: byok -spec: - cloud: byok - name: byok diff --git a/controller/go.mod b/controller/go.mod index 9543bf23a..b7d54f259 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 github.com/go-logr/logr v1.2.3 - github.com/pluralsh/console-client-go v0.0.54 + github.com/pluralsh/console-client-go v0.0.55 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.33.0 github.com/spf13/pflag v1.0.5 diff --git a/controller/go.sum b/controller/go.sum index 5d3f1bd6d..6a86615d1 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -230,8 +230,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/pluralsh/console-client-go v0.0.54 h1:hZ7yjXuRHvoGiQ9HgbVpidNHWlf7KapxhwAMN3lP+zc= -github.com/pluralsh/console-client-go v0.0.54/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= +github.com/pluralsh/console-client-go v0.0.55 h1:+j1Ur8ixNx4se4NEfTcul87/oVhUqFs+ZdsvCzvPYFM= +github.com/pluralsh/console-client-go v0.0.55/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index d8d8020a8..f5286d43b 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -7,6 +7,8 @@ import ( gqlgenclient "github.com/Yamashou/gqlgenc/client" console "github.com/pluralsh/console-client-go" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" ) type authedTransport struct { @@ -43,6 +45,7 @@ type ConsoleClient interface { IsClusterDeleting(id *string) bool CreateProvider(ctx context.Context, attributes console.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) GetProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) + GetProviderByCloud(ctx context.Context, cloud v1alpha1.CloudProvider, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) UpdateProvider(ctx context.Context, id string, attributes console.ClusterProviderUpdateAttributes, options ...gqlgenclient.HTTPRequestOption) (*console.ClusterProviderFragment, error) DeleteProvider(ctx context.Context, id string, options ...gqlgenclient.HTTPRequestOption) error IsProviderExists(ctx context.Context, id string) bool diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go index 42f6d7eed..bf1d89621 100644 --- a/controller/pkg/client/provider.go +++ b/controller/pkg/client/provider.go @@ -7,6 +7,8 @@ import ( gqlclient "github.com/pluralsh/console-client-go" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" ) func (c *client) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { @@ -20,6 +22,23 @@ func (c *client) GetProvider(ctx context.Context, id string, options ...gqlgencl return nil, errors.NewNotFound(schema.GroupResource{}, id) } + if response == nil { + return nil, err + } + + return response.ClusterProvider, err +} + +func (c *client) GetProviderByCloud(ctx context.Context, cloud v1alpha1.CloudProvider, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + response, err := c.consoleClient.GetClusterProviderByCloud(ctx, string(cloud), options...) + if err == nil && (response == nil || response.ClusterProvider == nil) { + return nil, errors.NewNotFound(schema.GroupResource{}, string(cloud)) + } + + if response == nil { + return nil, err + } + return response.ClusterProvider, err } @@ -29,6 +48,10 @@ func (c *client) UpdateProvider(ctx context.Context, id string, attributes gqlcl return nil, errors.NewNotFound(schema.GroupResource{}, id) } + if response == nil { + return nil, err + } + return response.UpdateClusterProvider, err } diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index 67b6c69e4..30030b93e 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -5,6 +5,8 @@ import ( "time" console "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -14,6 +16,7 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/kubernetes" "github.com/pluralsh/console/controller/pkg/utils" ) @@ -47,7 +50,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, client.IgnoreNotFound(err) } - // TODO: Try reading from API to see if object already exists + // Check if resource already exists in the API and only sync the ID + exists, err := r.isAlreadyExists(ctx, provider) + if err != nil { + return ctrl.Result{}, err + } + if exists { + return r.handleExistingProvider(ctx, provider) + } // Handle proper resource deletion via finalizer result, err := r.addOrRemoveFinalizer(ctx, provider) @@ -55,6 +65,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return *result, err } + err = r.tryAddControllerRef(ctx, provider) + if err != nil { + return ctrl.Result{}, nil + } + // Get Provider SHA that can be saved back in the status to check for changes changed, sha, err := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) if err != nil { @@ -73,6 +88,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) (any, any) { p.Status.ID = &apiProvider.ID p.Status.SHA = &sha + p.Status.Existing = lo.ToPtr(false) return original.Status, p.Status }); err != nil { @@ -82,6 +98,41 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return requeue, nil } +func (r *Reconciler) handleExistingProvider(ctx context.Context, provider v1alpha1.Provider) (reconcile.Result, error) { + apiProvider, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) + if err != nil { + return ctrl.Result{}, err + } + + if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) (any, any) { + p.Status.ID = &apiProvider.ID + p.Status.Existing = lo.ToPtr(true) + + return original.Status, p.Status + }); err != nil { + return ctrl.Result{}, err + } + + return requeue, nil +} + +func (r *Reconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Provider) (bool, error) { + if provider.Status.IsExisting() { + return true, nil + } + + _, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) + if errors.IsNotFound(err) { + return false, nil + } + + if err != nil { + return false, err + } + + return !provider.Status.HasID(), nil +} + func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { // If 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 @@ -129,7 +180,7 @@ func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1. // Update only if Provider has changed if changed && exists { - attributes, err := provider.Spec.UpdateAttributes(ctx, r.toCloudProviderSettingsAttributes) + attributes, err := provider.UpdateAttributes(ctx, r.toCloudProviderSettingsAttributes) if err != nil { return nil, err } @@ -143,7 +194,7 @@ func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1. } // Create the Provider in Console API if it doesn't exist - attributes, err := provider.Spec.Attributes(ctx, r.toCloudProviderSettingsAttributes) + attributes, err := provider.Attributes(ctx, r.toCloudProviderSettingsAttributes) if err != nil { return nil, err } @@ -158,3 +209,13 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { For(&v1alpha1.Provider{}). Complete(r) } + +func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1.Provider) error { + secretRef := r.getCloudProviderSettingsSecretRef(provider) + secret, err := kubernetes.GetSecret(ctx, r.Client, secretRef) + if err != nil { + return err + } + + return utils.TryAddControllerRef(ctx, r.Client, &provider, secret, r.Scheme) +} diff --git a/controller/pkg/provider_reconciler/reconciler_attributes.go b/controller/pkg/provider_reconciler/reconciler_attributes.go index 9b2eca1d6..f186abb55 100644 --- a/controller/pkg/provider_reconciler/reconciler_attributes.go +++ b/controller/pkg/provider_reconciler/reconciler_attributes.go @@ -15,17 +15,30 @@ func (r *Reconciler) missingCredentialKeyError(key string) error { return fmt.Errorf("%q key does not exist in referenced credential secret", key) } -func (r *Reconciler) toCloudProviderSettingsAttributes(ctx context.Context, spec v1alpha1.ProviderSpec) (*console.CloudProviderSettingsAttributes, error) { - switch spec.Cloud { +func (r *Reconciler) getCloudProviderSettingsSecretRef(provider v1alpha1.Provider) *corev1.SecretReference { + switch provider.Spec.Cloud { case v1alpha1.AWS: - return r.toCloudProviderAWSSettingsAttributes(ctx, spec.CloudSettings.AWS) + return provider.Spec.CloudSettings.AWS case v1alpha1.Azure: - return r.toCloudProviderAzureSettingsAttributes(ctx, spec.CloudSettings.Azure) + return provider.Spec.CloudSettings.Azure case v1alpha1.GCP: - return r.toCloudProviderGCPSettingsAttributes(ctx, spec.CloudSettings.GCP) + return provider.Spec.CloudSettings.GCP } - return nil, fmt.Errorf("unsupported cloud: %q", spec.Cloud) + return nil +} + +func (r *Reconciler) toCloudProviderSettingsAttributes(ctx context.Context, provider v1alpha1.Provider) (*console.CloudProviderSettingsAttributes, error) { + switch provider.Spec.Cloud { + case v1alpha1.AWS: + return r.toCloudProviderAWSSettingsAttributes(ctx, provider.Spec.CloudSettings.AWS) + case v1alpha1.Azure: + return r.toCloudProviderAzureSettingsAttributes(ctx, provider.Spec.CloudSettings.Azure) + case v1alpha1.GCP: + return r.toCloudProviderGCPSettingsAttributes(ctx, provider.Spec.CloudSettings.GCP) + } + + return nil, fmt.Errorf("unsupported cloud: %q", provider.Spec.Cloud) } func (r *Reconciler) toCloudProviderAWSSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index 0d62fed28..6ace48ee6 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -62,7 +62,7 @@ func TryAddControllerRef(ctx context.Context, client ctrlruntimeclient.Client, o } // Patcher TODO ... -type Patcher[PatchObject ctrlruntimeclient.Object] func(object PatchObject, original PatchObject) (any, any) +type Patcher[PatchObject ctrlruntimeclient.Object] func(object PatchObject, original PatchObject) (compare any, compareTo any) // TryUpdateStatus TODO ... func TryUpdateStatus[PatchObject ctrlruntimeclient.Object](ctx context.Context, client ctrlruntimeclient.Client, object PatchObject, patch Patcher[PatchObject]) error { From 53c505e7f83cd4bc3bf14238c3fc5302e9b91962 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 14:24:12 +0100 Subject: [PATCH 068/198] service waits for console --- .../gitrepository_controller/controller.go | 1 + .../pkg/service_controller/controller.go | 112 +++++++++--------- 2 files changed, 54 insertions(+), 59 deletions(-) diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index c2be7b8af..1bdb25d59 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -45,6 +45,7 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log.WithName(fmt.Sprintf("%s-%s", req.NamespacedName, req.Name)) repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { if apierrors.IsNotFound(err) { diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 9b2cfb36d..a0b09dd70 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,7 +2,7 @@ package servicecontroller import ( "context" - "reflect" + "fmt" "time" "github.com/go-logr/logr" @@ -16,10 +16,8 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -37,6 +35,7 @@ type Reconciler struct { func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { service := &v1alpha1.ServiceDeployment{} + r.Log.WithName(fmt.Sprintf("%s-%s", req.NamespacedName, req.Name)) if err := r.Get(ctx, req.NamespacedName, service); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -44,6 +43,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { return ctrl.Result{}, err } + cid := "ab39b5e6-690d-4620-83c4-d105ca40ecbb" + cluster.Status.ID = &cid if cluster.Status.ID == nil { r.Log.Info("Cluster is not ready") return ctrl.Result{ @@ -134,36 +135,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - if err := UpdateServiceStatus(ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment) { - r.Status.Id = &existingService.ID - r.Status.Sha = sha - if existingService.Errors != nil { - r.Status.Errors = algorithms.Map(existingService.Errors, - func(b *console.ErrorFragment) v1alpha1.ServiceError { - return v1alpha1.ServiceError{ - Source: b.Source, - Message: b.Message, - } - }) - } - r.Status.Components = make([]v1alpha1.ServiceComponent, 0) - for _, c := range existingService.Components { - sc := v1alpha1.ServiceComponent{ - ID: c.ID, - Name: c.Name, - Group: c.Group, - Kind: c.Kind, - Namespace: c.Namespace, - Synced: c.Synced, - Version: c.Version, - } - if c.State != nil { - state := v1alpha1.ComponentState(*c.State) - sc.State = &state - } - r.Status.Components = append(r.Status.Components, sc) - } - + if err = utils.TryUpdateStatus[*v1alpha1.ServiceDeployment](ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment, original *v1alpha1.ServiceDeployment) (any, any) { + updateStatus(r, existingService, sha) + return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } @@ -174,6 +148,37 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, nil } +func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.ServiceDeploymentExtended, sha string) { + r.Status.Id = &existingService.ID + r.Status.Sha = sha + if existingService.Errors != nil { + r.Status.Errors = algorithms.Map(existingService.Errors, + func(b *console.ErrorFragment) v1alpha1.ServiceError { + return v1alpha1.ServiceError{ + Source: b.Source, + Message: b.Message, + } + }) + } + r.Status.Components = make([]v1alpha1.ServiceComponent, 0) + for _, c := range existingService.Components { + sc := v1alpha1.ServiceComponent{ + ID: c.ID, + Name: c.Name, + Group: c.Group, + Kind: c.Kind, + Namespace: c.Namespace, + Synced: c.Synced, + Version: c.Version, + } + if c.State != nil { + state := v1alpha1.ComponentState(*c.State) + sc.State = &state + } + r.Status.Components = append(r.Status.Components, sc) + } +} + // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -299,15 +304,29 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { - r.Log.Info("delete service") + r.Log.Info(fmt.Sprintf("try to delete service %s-%s", service.Namespace, service.Name)) existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err } + if existingService != nil && existingService.DeletedAt != nil { + if err = utils.TryUpdateStatus[*v1alpha1.ServiceDeployment](ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment, original *v1alpha1.ServiceDeployment) (any, any) { + updateStatus(r, existingService, "") + return original.Status, r.Status + }); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{ + RequeueAfter: 30 * time.Second, + }, nil + } if existingService != nil { if err := r.ConsoleClient.DeleteService(*service.Status.Id); err != nil { return ctrl.Result{}, err } + return ctrl.Result{ + RequeueAfter: 30 * time.Second, + }, nil } if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { return ctrl.Result{}, err @@ -315,28 +334,3 @@ func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster } return ctrl.Result{}, nil } - -type RepoPatchFunc func(service *v1alpha1.ServiceDeployment) - -func UpdateServiceStatus(ctx context.Context, client ctrlruntimeclient.Client, service *v1alpha1.ServiceDeployment, patch RepoPatchFunc) error { - key := ctrlruntimeclient.ObjectKeyFromObject(service) - - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - // fetch the current state of the cluster - if err := client.Get(ctx, key, service); err != nil { - return err - } - - // modify it - original := service.DeepCopy() - patch(service) - - // save some work - if reflect.DeepEqual(original.Status, service.Status) { - return nil - } - - // update the status - return client.Status().Patch(ctx, service, ctrlruntimeclient.MergeFrom(original)) - }) -} From 9b23b687f857e4340c14c9cbf0cb4d8a2c5a9277 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 14:28:03 +0100 Subject: [PATCH 069/198] service waits for console --- controller/pkg/service_controller/controller.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index a0b09dd70..59e575f46 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -43,10 +43,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { return ctrl.Result{}, err } - cid := "ab39b5e6-690d-4620-83c4-d105ca40ecbb" - cluster.Status.ID = &cid + if cluster.Status.ID == nil { - r.Log.Info("Cluster is not ready") + r.Log.Info(fmt.Sprintf("Cluster %s/%s is not ready", cluster.Namespace, cluster.Name)) return ctrl.Result{ // update status RequeueAfter: 30 * time.Second, @@ -304,7 +303,7 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { - r.Log.Info(fmt.Sprintf("try to delete service %s-%s", service.Namespace, service.Name)) + r.Log.Info(fmt.Sprintf("try to delete service %s/%s", service.Namespace, service.Name)) existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err From ce5974813515f8b6b83b22c9876025fc06cc7e6c Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 14:30:29 +0100 Subject: [PATCH 070/198] fix null pointer errror --- controller/pkg/client/cluster.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/controller/pkg/client/cluster.go b/controller/pkg/client/cluster.go index 8ee507ac4..65f48dfbd 100644 --- a/controller/pkg/client/cluster.go +++ b/controller/pkg/client/cluster.go @@ -37,6 +37,9 @@ func (c *client) UpdateCluster(id string, attrs console.ClusterUpdateAttributes) if err == nil && (response == nil || response.UpdateCluster == nil) { return nil, errors.NewNotFound(schema.GroupResource{}, id) } + if response == nil { + return nil, err + } return response.UpdateCluster, err } @@ -46,6 +49,9 @@ func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { if err == nil && (response == nil || response.Cluster == nil) { return nil, errors.NewNotFound(schema.GroupResource{}, *id) } + if response == nil { + return nil, err + } return response.Cluster, err } @@ -55,6 +61,9 @@ func (c *client) GetClusterByHandle(handle *string) (*console.ClusterFragment, e if err == nil && (response == nil || response.Cluster == nil) { return nil, errors.NewNotFound(schema.GroupResource{}, *handle) } + if response == nil { + return nil, err + } return response.Cluster, err } @@ -68,6 +77,9 @@ func (c *client) DeleteCluster(id string) (*console.ClusterFragment, error) { if err != nil { return nil, err } + if response == nil { + return nil, err + } return response.DeleteCluster, nil } From 3993cd941a1872fbcfca32574380b11c82fc1a68 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 14:44:02 +0100 Subject: [PATCH 071/198] add git and service examples --- .../config/crd/examples/git_repository.yaml | 6 ++++++ .../crd/examples/service_deployment.yaml | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 controller/config/crd/examples/git_repository.yaml create mode 100644 controller/config/crd/examples/service_deployment.yaml diff --git a/controller/config/crd/examples/git_repository.yaml b/controller/config/crd/examples/git_repository.yaml new file mode 100644 index 000000000..cf08c23e2 --- /dev/null +++ b/controller/config/crd/examples/git_repository.yaml @@ -0,0 +1,6 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: k8shelm +spec: + url: https://github.com/zreigz/k8s-helm.git diff --git a/controller/config/crd/examples/service_deployment.yaml b/controller/config/crd/examples/service_deployment.yaml new file mode 100644 index 000000000..1de5ef9cf --- /dev/null +++ b/controller/config/crd/examples/service_deployment.yaml @@ -0,0 +1,18 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: ServiceDeployment +metadata: + name: k8saws + namespace: default +spec: + version: 0.0.1 + git: + folder: nginx + ref: master + repositoryRef: + kind: GitRepository + name: k8shelm + namespace: default + clusterRef: + kind: Cluster + name: aws + namespace: default From 6d02729cb0bebc12a7e81d6673d0a960970c6636 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 15:05:18 +0100 Subject: [PATCH 072/198] switch logger --- controller/config/crd/examples/cluster_aws.yaml | 2 +- controller/pkg/cluster_controller/reconciler.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/controller/config/crd/examples/cluster_aws.yaml b/controller/config/crd/examples/cluster_aws.yaml index 0af90f11e..a8869b0ec 100644 --- a/controller/config/crd/examples/cluster_aws.yaml +++ b/controller/config/crd/examples/cluster_aws.yaml @@ -14,7 +14,7 @@ metadata: spec: handle: aws cloud: aws - version: "1.24" + version: "1.25" protect: false providerRef: name: aws diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 291aaa6ac..6f8779955 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -17,6 +17,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -45,10 +46,12 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx) + // Read resource from Kubernetes cluster. cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { - r.Log.Error(err, "unable to fetch cluster") + logger.Error(err, "unable to fetch cluster") return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -185,6 +188,8 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 } func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { + logger := log.FromContext(ctx) + if cluster.Spec.IsProviderRefRequired() { if !cluster.Spec.HasProviderRef() { return nil, &ctrl.Result{}, fmt.Errorf("could not get provider, reference is not set but required") @@ -196,7 +201,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v } if !provider.Status.HasID() { - r.Log.Info("provider does not have ID set yet") + logger.Info("provider does not have ID set yet") return nil, &requeue, nil } From 0bc7e08c3b5382dcafe4f95626a6dd130abf1f3a Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 12 Dec 2023 15:22:26 +0100 Subject: [PATCH 073/198] add some basic logging --- .../apis/deployments/v1alpha1/provider.go | 6 ++-- .../deployments.plural.sh_providers.yaml | 9 +++++ .../crd/examples/provider_aws_readonly.yaml | 8 +++++ .../{provider_aws.yaml => provider_gcp.yaml} | 0 .../pkg/provider_reconciler/reconciler.go | 35 +++++++++++++------ 5 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 controller/config/crd/examples/provider_aws_readonly.yaml rename controller/config/crd/examples/{provider_aws.yaml => provider_gcp.yaml} (100%) diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index ebdd118e4..3338cc351 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -109,7 +109,7 @@ type ProviderSpec struct { // +kubebuilder:validation:Required // +kubebuilder:validation:Type:=string // +kubebuilder:validation:Enum:=gcp;aws;azure - // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Cloud is immutable" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Cloud is immutable" Cloud CloudProvider `json:"cloud"` // CloudSettings reference cloud provider credentials secrets used for provisioning the Cluster. // Not required when Cloud is set to CloudProvider(BYOK). @@ -120,12 +120,12 @@ type ProviderSpec struct { // Name is a human-readable name of the Provider. // +kubebuilder:example:=gcp-provider // +kubebuilder:validation:Optional - // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Name is immutable" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Name is immutable" Name string `json:"name"` // Namespace is the namespace ClusterAPI resources are deployed into. // +kubebuilder:example:=capi-gcp // +kubebuilder:validation:Optional - // +kubebuilder:validation:Validation:rule="self == oldSelf",message="Namespace is immutable" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Namespace is immutable" Namespace string `json:"namespace,omitempty"` } diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index 015d47adf..8ed657bca 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -57,6 +57,9 @@ spec: - azure example: aws type: string + x-kubernetes-validations: + - message: Cloud is immutable + rule: self == oldSelf cloudSettings: description: CloudSettings reference cloud provider credentials secrets used for provisioning the Cluster. Not required when Cloud is set @@ -110,11 +113,17 @@ spec: description: Name is a human-readable name of the Provider. example: gcp-provider type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf namespace: description: Namespace is the namespace ClusterAPI resources are deployed into. example: capi-gcp type: string + x-kubernetes-validations: + - message: Namespace is immutable + rule: self == oldSelf required: - cloud type: object diff --git a/controller/config/crd/examples/provider_aws_readonly.yaml b/controller/config/crd/examples/provider_aws_readonly.yaml new file mode 100644 index 000000000..83fb5ea9b --- /dev/null +++ b/controller/config/crd/examples/provider_aws_readonly.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: aws +spec: + cloud: aws + diff --git a/controller/config/crd/examples/provider_aws.yaml b/controller/config/crd/examples/provider_gcp.yaml similarity index 100% rename from controller/config/crd/examples/provider_aws.yaml rename to controller/config/crd/examples/provider_gcp.yaml diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index 30030b93e..7c8b0a038 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -42,6 +42,7 @@ var ( // TODO: Add kubebuilder rbac annotation func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := log.FromContext(ctx) + log.Info("Reconciling") // Read Provider CRD from the K8S API var provider v1alpha1.Provider @@ -56,6 +57,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, err } if exists { + log.Info("Provider already exists in the API, running in read-only mode") return r.handleExistingProvider(ctx, provider) } @@ -67,7 +69,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco err = r.tryAddControllerRef(ctx, provider) if err != nil { - return ctrl.Result{}, nil + return ctrl.Result{}, err } // Get Provider SHA that can be saved back in the status to check for changes @@ -78,7 +80,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Sync Provider CRD with the Console API - apiProvider, err := r.updateOrGetProvider(ctx, provider, changed) + apiProvider, err := r.sync(ctx, provider, changed) if err != nil { log.Error(err, "unable to create or update provider") return ctrl.Result{}, err @@ -134,6 +136,8 @@ func (r *Reconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Prov } func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { + log := log.FromContext(ctx) + // If 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 // to registering our finalizer. @@ -148,16 +152,22 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1 if !provider.ObjectMeta.DeletionTimestamp.IsZero() { // If object is already being deleted from Console API requeue if r.ConsoleClient.IsProviderDeleting(ctx, provider.Status.GetID()) { + log.Info("Waiting for provider to be deleted from Console API") return &requeue, nil } // Remove Provider from Console API if it exists if r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { + log.Info("Deleting provider") if err := r.ConsoleClient.DeleteProvider(ctx, provider.Status.GetID()); err != nil { // if fail to delete the external dependency here, return with error // so that it can be retried. return &ctrl.Result{}, err } + + // If deletion process started requeue so that we can make sure provider + // has been deleted from Console API before removing the finalizer. + return &requeue, nil } // If our finalizer is present, remove it @@ -175,7 +185,8 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1 return nil, nil } -func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { +func (r *Reconciler) sync(ctx context.Context, provider v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { + log := log.FromContext(ctx) exists := r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) // Update only if Provider has changed @@ -185,6 +196,7 @@ func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1. return nil, err } + log.Info("Updating provider") return r.ConsoleClient.UpdateProvider(ctx, provider.Status.GetID(), attributes) } @@ -199,17 +211,10 @@ func (r *Reconciler) updateOrGetProvider(ctx context.Context, provider v1alpha1. return nil, err } + log.Info("Creating provider") return r.ConsoleClient.CreateProvider(ctx, attributes) } -// SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { - mgr.GetLogger().Info("starting reconciler", "reconciler", "provider_reconciler") - return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.Provider{}). - Complete(r) -} - func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1.Provider) error { secretRef := r.getCloudProviderSettingsSecretRef(provider) secret, err := kubernetes.GetSecret(ctx, r.Client, secretRef) @@ -219,3 +224,11 @@ func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1. return utils.TryAddControllerRef(ctx, r.Client, &provider, secret, r.Scheme) } + +// SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + mgr.GetLogger().Info("Starting reconciler", "reconciler", "provider_reconciler") + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Provider{}). + Complete(r) +} From 36b72927370c1d2816eb497142661c6d8d3156fd Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 15:32:38 +0100 Subject: [PATCH 074/198] update sync cluster logic --- .../apis/deployments/v1alpha1/cluster.go | 4 ++++ .../pkg/cluster_controller/reconciler.go | 22 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 982d58e1b..e11cbde93 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -382,6 +382,10 @@ func (cs *ClusterStatus) HasSHA() bool { return cs.SHA != nil && len(*cs.SHA) > 0 } +func (cs *ClusterStatus) IsSHAChanged(sha *string) bool { + return cs.HasSHA() && cs.SHA != sha +} + func (cs *ClusterStatus) IsExisting() bool { return cs.Existing != nil && *cs.Existing } diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 6f8779955..70cc25953 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -83,7 +83,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Sync resource with Console API. - apiCluster, err := r.syncCluster(cluster, providerId, &sha) + apiCluster, err := r.syncCluster(ctx, cluster, providerId, &sha) if err != nil { return ctrl.Result{}, err } @@ -207,7 +207,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) if err != nil { - return nil, &ctrl.Result{}, err + return nil, &ctrl.Result{}, fmt.Errorf("could not set cluster owner reference, got error: %+v", err) } return provider.Status.ID, nil, nil @@ -216,14 +216,20 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, nil, nil } -func (r *Reconciler) syncCluster(cluster *v1alpha1.Cluster, providerId *string, sha *string) (*console.ClusterFragment, error) { - if !cluster.Status.HasID() { - return r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) - } +func (r *Reconciler) syncCluster(ctx context.Context, cluster *v1alpha1.Cluster, providerId *string, sha *string) (*console.ClusterFragment, error) { + exists := r.ConsoleClient.IsClusterExisting(cluster.Status.ID) + logger := log.FromContext(ctx) - if cluster.Status.HasSHA() && cluster.Status.SHA != sha { + if cluster.Status.IsSHAChanged(sha) && exists { + logger.Info("detected changes, updating cluster") return r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) } - return r.ConsoleClient.GetCluster(cluster.Status.ID) + if exists { + logger.Info("no changes detected, updating cluster") + return r.ConsoleClient.GetCluster(cluster.Status.ID) + } + + logger.Info("cluster does not exist, creating new one") + return r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) } From fce60ca49a7694b3fb863a291c4df583004abb76 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 12 Dec 2023 15:36:30 +0100 Subject: [PATCH 075/198] add logs --- .../gitrepository_controller/controller.go | 25 +++++----- .../pkg/service_controller/controller.go | 47 ++++++++----------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 1bdb25d59..cb61f0e54 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -19,16 +19,22 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" ) const ( RepoFinalizer = "deployments.plural.sh/gitrepo-protection" + RequeueAfter = 30 * time.Second privateKey = "privateKey" passphrase = "passphrase" username = "username" password = "password" ) +var ( + requeue = ctrl.Result{RequeueAfter: RequeueAfter} +) + type GitRepoCred struct { PrivateKey *string Passphrase *string @@ -45,7 +51,7 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Log.WithName(fmt.Sprintf("%s-%s", req.NamespacedName, req.Name)) + log := log.FromContext(ctx) repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { if apierrors.IsNotFound(err) { @@ -71,7 +77,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } if existingRepo == nil && repo.Status.Existing == true { msg := "existing Git repository was deleted from the console" - r.Log.Info(msg) + log.Info(msg) if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { r.Status.Message = &msg r.Status.Health = v1alpha1.GitHealthFailed @@ -79,9 +85,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }); err != nil { return ctrl.Result{}, err } - return ctrl.Result{ - RequeueAfter: 30 * time.Second, - }, nil + return requeue, nil } if repo.Status.Id == nil { if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { @@ -99,6 +103,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } + log.Info("repository created") if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { r.Status.Existing = false return original.Status, r.Status @@ -120,6 +125,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } + log.Info("repository updated") } if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { @@ -134,10 +140,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - return ctrl.Result{ - // update status - RequeueAfter: 30 * time.Second, - }, nil + return requeue, nil } // SetupWithManager sets up the controller with the Manager. @@ -148,9 +151,9 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { - + log := log.FromContext(ctx) if controllerutil.ContainsFinalizer(repo, RepoFinalizer) { - r.Log.Info("delete git repository") + log.Info("delete git repository") if repo.Status.Id == nil { return ctrl.Result{}, fmt.Errorf("the repoository ID can not be nil") } diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 59e575f46..d57d5ed0a 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,7 +2,6 @@ package servicecontroller import ( "context" - "fmt" "time" "github.com/go-logr/logr" @@ -19,10 +18,16 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" ) const ( ServiceFinalizer = "deployments.plural.sh/service-protection" + RequeueAfter = 30 * time.Second +) + +var ( + requeue = ctrl.Result{RequeueAfter: RequeueAfter} ) // Reconciler reconciles a Service object @@ -34,8 +39,8 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) service := &v1alpha1.ServiceDeployment{} - r.Log.WithName(fmt.Sprintf("%s-%s", req.NamespacedName, req.Name)) if err := r.Get(ctx, req.NamespacedName, service); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -45,11 +50,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } if cluster.Status.ID == nil { - r.Log.Info(fmt.Sprintf("Cluster %s/%s is not ready", cluster.Namespace, cluster.Name)) - return ctrl.Result{ - // update status - RequeueAfter: 30 * time.Second, - }, nil + log.Info("Cluster is not ready") + return requeue, nil } if !service.GetDeletionTimestamp().IsZero() { return r.handleDelete(ctx, cluster, service) @@ -60,18 +62,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if repository.Status.Id == nil { - r.Log.Info("Repository is not ready") - return ctrl.Result{ - // update status - RequeueAfter: 30 * time.Second, - }, nil + log.Info("Repository is not ready") + return requeue, nil } if repository.Status.Health == v1alpha1.GitHealthFailed { - r.Log.Info("Repository is not healthy") - return ctrl.Result{ - // update status - RequeueAfter: 30 * time.Second, - }, nil + log.Info("Repository is not healthy") + return requeue, nil } attr, err := r.genServiceAttributes(ctx, service, repository.Status.Id) @@ -141,10 +137,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - return ctrl.Result{ - // update status - RequeueAfter: 30 * time.Second, - }, nil + return requeue, nil } func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.ServiceDeploymentExtended, sha string) { @@ -302,30 +295,28 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S } func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { + log := log.FromContext(ctx) if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { - r.Log.Info(fmt.Sprintf("try to delete service %s/%s", service.Namespace, service.Name)) + log.Info("try to delete service") existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err } if existingService != nil && existingService.DeletedAt != nil { + log.Info("waiting for the console") if err = utils.TryUpdateStatus[*v1alpha1.ServiceDeployment](ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment, original *v1alpha1.ServiceDeployment) (any, any) { updateStatus(r, existingService, "") return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } - return ctrl.Result{ - RequeueAfter: 30 * time.Second, - }, nil + return requeue, nil } if existingService != nil { if err := r.ConsoleClient.DeleteService(*service.Status.Id); err != nil { return ctrl.Result{}, err } - return ctrl.Result{ - RequeueAfter: 30 * time.Second, - }, nil + return requeue, nil } if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { return ctrl.Result{}, err From 4cd01f9ebe0b0545e5de5169e56f3057ca8af5cc Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 15:39:01 +0100 Subject: [PATCH 076/198] add todo --- controller/pkg/cluster_controller/reconciler.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 70cc25953..b49809974 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -51,16 +51,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco // Read resource from Kubernetes cluster. cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { - logger.Error(err, "unable to fetch cluster") + logger.Error(err, "Unable to fetch cluster") return ctrl.Result{}, client.IgnoreNotFound(err) } // Handle existing resource. existing, err := r.isExistingResource(cluster) if err != nil { - return ctrl.Result{}, err + return ctrl.Result{}, fmt.Errorf("could not check if cluster is existing resource, got error: %+v", err) } if existing { + logger.Info("Cluster already exists in the API, running in read-only mode") return r.handleExistingResource(ctx, cluster) } @@ -77,6 +78,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Calculate SHA to detect changes that should be applied in the Console API. + // TODO: Ensure that element order stays the same to avoid fake diffs. sha, err := utils.HashObject(cluster.UpdateAttributes()) if err != nil { return ctrl.Result{}, err @@ -201,7 +203,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v } if !provider.Status.HasID() { - logger.Info("provider does not have ID set yet") + logger.Info("Provider does not have ID set yet") return nil, &requeue, nil } @@ -221,15 +223,15 @@ func (r *Reconciler) syncCluster(ctx context.Context, cluster *v1alpha1.Cluster, logger := log.FromContext(ctx) if cluster.Status.IsSHAChanged(sha) && exists { - logger.Info("detected changes, updating cluster") + logger.Info("Detected changes, updating cluster") return r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) } if exists { - logger.Info("no changes detected, updating cluster") + logger.Info("No changes detected, updating cluster") return r.ConsoleClient.GetCluster(cluster.Status.ID) } - logger.Info("cluster does not exist, creating new one") + logger.Info("Cluster does not exist, creating new one") return r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) } From 9ce5a9e3c748d5850ae52c243ba177fbc37c1d8f Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 12 Dec 2023 15:53:41 +0100 Subject: [PATCH 077/198] add existing cluster example --- controller/config/crd/examples/cluster_existing.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 controller/config/crd/examples/cluster_existing.yaml diff --git a/controller/config/crd/examples/cluster_existing.yaml b/controller/config/crd/examples/cluster_existing.yaml new file mode 100644 index 000000000..9dbecaae6 --- /dev/null +++ b/controller/config/crd/examples/cluster_existing.yaml @@ -0,0 +1,7 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: existing + namespace: default +spec: + handle: mgmt \ No newline at end of file From 01ba5b84ba0f83f31753b977429bce269cc54df7 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 09:21:56 +0100 Subject: [PATCH 078/198] update repo --- .../deployments/v1alpha1/git_repository.go | 16 ++- .../v1alpha1/zz_generated.deepcopy.go | 5 + ...deployments.plural.sh_gitrepositories.yaml | 5 +- .../gitrepository_controller/controller.go | 100 +++++++++++++----- 4 files changed, 94 insertions(+), 32 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 031a772b3..8217328e7 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -37,8 +37,12 @@ type GitRepositoryStatus struct { // +optional Id *string `json:"id,omitempty"` // +optional - Sha string `json:"sha,omitempty"` - Existing bool `json:"existing"` + Sha string `json:"sha,omitempty"` + // Existing flag is set to true when Console API object already exists when CRD is created. + // CRD is then set to read-only mode and does not update Console API from CRD. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=boolean + Existing *bool `json:"existing,omitempty"` } // +kubebuilder:object:root=true @@ -61,3 +65,11 @@ type GitRepositoryList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []GitRepository `json:"items"` } + +func (p *GitRepositoryStatus) IsExisting() bool { + return p.Existing != nil && *p.Existing +} + +func (p *GitRepositoryStatus) HasID() bool { + return p.Id != nil && len(*p.Id) > 0 +} diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index b1adc65de..1a1a7f988 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -517,6 +517,11 @@ func (in *GitRepositoryStatus) DeepCopyInto(out *GitRepositoryStatus) { *out = new(string) **out = **in } + if in.Existing != nil { + in, out := &in.Existing, &out.Existing + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositoryStatus. diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index 206b6435b..ad2349035 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -67,6 +67,9 @@ spec: status: properties: existing: + description: Existing flag is set to true when Console API object + already exists when CRD is created. CRD is then set to read-only + mode and does not update Console API from CRD. type: boolean health: description: Health status. @@ -82,8 +85,6 @@ spec: type: string sha: type: string - required: - - existing type: object type: object served: true diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index cb61f0e54..1bba6df9f 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -5,6 +5,9 @@ import ( "fmt" "time" + "github.com/samber/lo" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" @@ -51,7 +54,7 @@ type Reconciler struct { } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { if apierrors.IsNotFound(err) { @@ -66,35 +69,25 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - sha, err := utils.HashObject(cred) + + // Check if resource already exists in the API and only sync the ID + exists, err := r.isAlreadyExists(repo) if err != nil { return ctrl.Result{}, err } + if exists { + logger.Info("repository already exists in the API, running in read-only mode") + return r.handleExistingRepo(ctx, repo) + } + sha, err := utils.HashObject(cred) + if err != nil { + return ctrl.Result{}, err + } existingRepo, err := r.getRepository(repo.Spec.Url) if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err } - if existingRepo == nil && repo.Status.Existing == true { - msg := "existing Git repository was deleted from the console" - log.Info(msg) - if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Message = &msg - r.Status.Health = v1alpha1.GitHealthFailed - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err - } - return requeue, nil - } - if repo.Status.Id == nil { - if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Existing = true - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err - } - } if existingRepo == nil { if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { return ctrl.Result{}, err @@ -103,18 +96,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - log.Info("repository created") if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Existing = false + r.Status.Existing = lo.ToPtr(false) return original.Status, r.Status }); err != nil { return ctrl.Result{}, err } + logger.Info("repository created") existingRepo = resp.CreateGitRepository } - if repo.Status.Sha != "" && repo.Status.Sha != sha && !repo.Status.Existing { + if repo.Status.Sha != "" && repo.Status.Sha != sha { _, err := r.ConsoleClient.UpdateRepository(existingRepo.ID, console.GitAttributes{ URL: repo.Spec.Url, PrivateKey: cred.PrivateKey, @@ -125,7 +118,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - log.Info("repository updated") + logger.Info("repository updated") } if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { @@ -135,6 +128,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } r.Status.Sha = sha + r.Status.Existing = lo.ToPtr(false) return original.Status, r.Status }); err != nil { return ctrl.Result{}, err @@ -151,9 +145,9 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) if controllerutil.ContainsFinalizer(repo, RepoFinalizer) { - log.Info("delete git repository") + logger.Info("delete git repository") if repo.Status.Id == nil { return ctrl.Result{}, fmt.Errorf("the repoository ID can not be nil") } @@ -220,3 +214,53 @@ func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, return existingRepos.GitRepository, nil } + +func (r *Reconciler) isAlreadyExists(repository *v1alpha1.GitRepository) (bool, error) { + if repository.Status.IsExisting() { + return true, nil + } + + repo, err := r.getRepository(repository.Spec.Url) + if err == nil && repo == nil { + return false, nil + } + + if err != nil { + return false, err + } + + return !repository.Status.HasID(), nil +} + +func (r *Reconciler) handleExistingRepo(ctx context.Context, repo *v1alpha1.GitRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx) + existingRepo, err := r.getRepository(repo.Spec.Url) + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + if existingRepo == nil { + msg := "existing Git repository was deleted from the console" + logger.Info(msg) + if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { + r.Status.Message = &msg + r.Status.Health = v1alpha1.GitHealthFailed + return original.Status, r.Status + }); err != nil { + return ctrl.Result{}, err + } + return requeue, nil + } + + if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { + r.Status.Message = existingRepo.Error + r.Status.Id = &existingRepo.ID + if existingRepo.Health != nil { + r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) + } + r.Status.Existing = lo.ToPtr(true) + return original.Status, r.Status + }); err != nil { + return ctrl.Result{}, err + } + return requeue, nil +} From 35e8df379e4af11b83a4e46ec884ec21ff3cb3b3 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 09:51:49 +0100 Subject: [PATCH 079/198] set controller ref for service --- controller/pkg/service_controller/controller.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index d57d5ed0a..c2f68cdbd 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -259,8 +259,7 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S if err != nil { return err } - err = utils.TryAddOwnerRef(ctx, r.Client, service, configurationSecret, r.Scheme) - if err != nil { + if err := utils.TryAddControllerRef(ctx, r.Client, service, configurationSecret, r.Scheme); err != nil { return err } @@ -273,7 +272,7 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S if err != nil { return err } - err = utils.TryAddOwnerRef(ctx, r.Client, service, configMap, r.Scheme) + err = utils.TryAddControllerRef(ctx, r.Client, service, configMap, r.Scheme) if err != nil { return err } @@ -285,7 +284,7 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S if err != nil { return err } - err = utils.TryAddOwnerRef(ctx, r.Client, service, configMap, r.Scheme) + err = utils.TryAddControllerRef(ctx, r.Client, service, configMap, r.Scheme) if err != nil { return err } From 75cbc5650eb503b24bd337f0dd46964eb74f1c8c Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 09:57:12 +0100 Subject: [PATCH 080/198] add owns for service --- controller/pkg/service_controller/controller.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index c2f68cdbd..0075a1f91 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -175,6 +175,8 @@ func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.Servic func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.ServiceDeployment{}). + Owns(&corev1.Secret{}). + Owns(&corev1.ConfigMap{}). Complete(r) } From 5e9c96625e482c4c4a456a9e7edf11b2a6839263 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 10:35:00 +0100 Subject: [PATCH 081/198] ensure same item order when calculating SHA --- .../apis/deployments/v1alpha1/cluster.go | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index e11cbde93..dc66eb7ca 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -2,6 +2,8 @@ package v1alpha1 import ( "bytes" + "slices" + "strings" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/polly/algorithms" @@ -67,12 +69,14 @@ func (c *Cluster) Attributes(providerId *string) console.ClusterAttributes { } func (c *Cluster) UpdateAttributes() console.ClusterUpdateAttributes { + nodePools := algorithms.Map(c.Spec.NodePools, func(np ClusterNodePool) *console.NodePoolAttributes { return np.Attributes() }) + slices.SortFunc(nodePools, func(a, b *console.NodePoolAttributes) int { return strings.Compare(a.Name, b.Name) }) + return console.ClusterUpdateAttributes{ - Handle: c.Spec.Handle, - Version: c.Spec.Version, - Protect: c.Spec.Protect, - NodePools: algorithms.Map(c.Spec.NodePools, - func(np ClusterNodePool) *console.NodePoolAttributes { return np.Attributes() }), + Handle: c.Spec.Handle, + Version: c.Spec.Version, + Protect: c.Spec.Protect, + NodePools: nodePools, } } @@ -289,14 +293,16 @@ func (np *ClusterNodePool) Attributes() *console.NodePoolAttributes { return nil } + taints := algorithms.Map(np.Taints, func(t Taint) *console.TaintAttributes { return t.Attributes() }) + slices.SortFunc(taints, func(a, b *console.TaintAttributes) int { return strings.Compare(a.Key, b.Key) }) + attrs := &console.NodePoolAttributes{ Name: np.Name, MinSize: np.MinSize, MaxSize: np.MaxSize, InstanceType: np.InstanceType, CloudSettings: np.CloudSettings.Attributes(), - Taints: algorithms.Map(np.Taints, - func(t Taint) *console.TaintAttributes { return t.Attributes() }), + Taints: taints, } if np.Labels != nil { From d5b7b883ef93f5a82819ccca33ee9949328a4765 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 10:35:05 +0100 Subject: [PATCH 082/198] ensure same item order when calculating SHA --- controller/pkg/cluster_controller/reconciler.go | 1 - 1 file changed, 1 deletion(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index b49809974..b2318ec19 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -78,7 +78,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Calculate SHA to detect changes that should be applied in the Console API. - // TODO: Ensure that element order stays the same to avoid fake diffs. sha, err := utils.HashObject(cluster.UpdateAttributes()) if err != nil { return ctrl.Result{}, err From dbf4baf674bbd51427bdfda1d40607d1dd840fd6 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 10:56:03 +0100 Subject: [PATCH 083/198] update sha --- .../apis/deployments/v1alpha1/service.go | 11 +++- .../v1alpha1/zz_generated.deepcopy.go | 34 +++++++++++ ...loyments.plural.sh_servicedeployments.yaml | 11 ++++ .../pkg/service_controller/controller.go | 56 ++++++++++++++----- 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/apis/deployments/v1alpha1/service.go index 312931f97..956d21a06 100644 --- a/controller/apis/deployments/v1alpha1/service.go +++ b/controller/apis/deployments/v1alpha1/service.go @@ -39,6 +39,14 @@ type ServiceHelm struct { Repository *NamespacedName `json:"repository,omitempty"` } +type SyncConfigAttributes struct { + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + type ServiceSpec struct { // +optional DocsPath *string `json:"docsPath,omitempty"` @@ -52,7 +60,8 @@ type ServiceSpec struct { Git *ServiceGit `json:"git,omitempty"` // +optional Helm *ServiceHelm `json:"helm,omitempty"` - + // +optional + SyncConfig *SyncConfigAttributes `json:"syncConfig,omitempty"` // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Repository is immutable" RepositoryRef corev1.ObjectReference `json:"repositoryRef"` diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 1a1a7f988..a0e54e997 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -866,6 +866,11 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { *out = new(ServiceHelm) (*in).DeepCopyInto(*out) } + if in.SyncConfig != nil { + in, out := &in.SyncConfig, &out.SyncConfig + *out = new(SyncConfigAttributes) + (*in).DeepCopyInto(*out) + } out.RepositoryRef = in.RepositoryRef out.ClusterRef = in.ClusterRef if in.ConfigurationRef != nil { @@ -922,6 +927,35 @@ func (in *ServiceStatus) DeepCopy() *ServiceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SyncConfigAttributes) DeepCopyInto(out *SyncConfigAttributes) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncConfigAttributes. +func (in *SyncConfigAttributes) DeepCopy() *SyncConfigAttributes { + if in == nil { + return nil + } + out := new(SyncConfigAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Taint) DeepCopyInto(out *Taint) { *out = *in diff --git a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index 3ee7aef07..bfaebda4b 100644 --- a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -282,6 +282,17 @@ spec: x-kubernetes-validations: - message: Repository is immutable rule: self == oldSelf + syncConfig: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object version: default: '''0.0.1''' type: string diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 0075a1f91..1da50a4fc 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -2,6 +2,7 @@ package servicecontroller import ( "context" + "encoding/json" "time" "github.com/go-logr/logr" @@ -75,11 +76,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - sha, err := utils.HashObject(attr) - if err != nil { - return ctrl.Result{}, err - } - existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err @@ -100,22 +96,27 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } } - err = r.addOwnerReferences(ctx, service) if err != nil { return ctrl.Result{}, err } + updater := console.ServiceUpdateAttributes{ + Version: attr.Version, + Protect: attr.Protect, + Git: attr.Git, + Helm: attr.Helm, + Configuration: attr.Configuration, + Kustomize: attr.Kustomize, + } + + sha, err := utils.HashObject(updater) + if err != nil { + return ctrl.Result{}, err + } + if service.Status.Sha != "" && service.Status.Sha != sha { // update service - updater := console.ServiceUpdateAttributes{ - Version: attr.Version, - Protect: attr.Protect, - Git: attr.Git, - Helm: attr.Helm, - Configuration: attr.Configuration, - Kustomize: attr.Kustomize, - } if err := r.ConsoleClient.UpdateService(existingService.ID, updater); err != nil { return ctrl.Result{}, err } @@ -250,8 +251,35 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 } attr.Helm.Chart = &val } + if service.Spec.SyncConfig != nil { + var annotations *string + var labels *string + if service.Spec.SyncConfig.Annotations != nil { + result, err := json.Marshal(service.Spec.SyncConfig.Annotations) + if err != nil { + return nil, err + } + rawAnnotations := string(result) + annotations = &rawAnnotations + } + if service.Spec.SyncConfig.Labels != nil { + result, err := json.Marshal(service.Spec.SyncConfig.Labels) + if err != nil { + return nil, err + } + rawLabels := string(result) + labels = &rawLabels + } + attr.SyncConfig = &console.SyncConfigAttributes{ + NamespaceMetadata: &console.MetadataAttributes{ + Labels: labels, + Annotations: annotations, + }, + } + } } + return attr, nil } From 227cb835ff7f50e757b9d6a1f1e1d1ab01070805 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 11:01:51 +0100 Subject: [PATCH 084/198] improve existing check --- controller/apis/deployments/v1alpha1/cluster.go | 4 ++-- controller/pkg/cluster_controller/reconciler.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index dc66eb7ca..3145a1395 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -392,6 +392,6 @@ func (cs *ClusterStatus) IsSHAChanged(sha *string) bool { return cs.HasSHA() && cs.SHA != sha } -func (cs *ClusterStatus) IsExisting() bool { - return cs.Existing != nil && *cs.Existing +func (cs *ClusterStatus) HasExisting() bool { + return cs.Existing != nil } diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index b2318ec19..6bd18ac8e 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -56,13 +56,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Handle existing resource. - existing, err := r.isExistingResource(cluster) + existing, err := r.isExisting(cluster) if err != nil { return ctrl.Result{}, fmt.Errorf("could not check if cluster is existing resource, got error: %+v", err) } if existing { logger.Info("Cluster already exists in the API, running in read-only mode") - return r.handleExistingResource(ctx, cluster) + return r.handleExisting(ctx, cluster) } // Handle resource deletion both in Kubernetes cluster and in Console API. @@ -106,9 +106,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return requeue, nil } -func (r *Reconciler) isExistingResource(cluster *v1alpha1.Cluster) (bool, error) { - if cluster.Status.IsExisting() { - return true, nil +func (r *Reconciler) isExisting(cluster *v1alpha1.Cluster) (bool, error) { + if cluster.Status.HasExisting() { + return *cluster.Status.Existing, nil } if !cluster.Spec.HasHandle() { @@ -126,7 +126,7 @@ func (r *Reconciler) isExistingResource(cluster *v1alpha1.Cluster) (bool, error) return !cluster.Status.HasID(), nil } -func (r *Reconciler) handleExistingResource(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { +func (r *Reconciler) handleExisting(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { apiCluster, err := r.ConsoleClient.GetClusterByHandle(cluster.Spec.Handle) if err != nil { return ctrl.Result{}, err From 6d4889e2185dd27dcd2b5af8611315e2098ba034 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 11:02:58 +0100 Subject: [PATCH 085/198] add sort --- controller/pkg/service_controller/controller.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 1da50a4fc..7621dc808 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -3,6 +3,7 @@ package servicecontroller import ( "context" "encoding/json" + "sort" "time" "github.com/go-logr/logr" @@ -100,7 +101,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - + sort.Slice(attr.Configuration, func(i, j int) bool { + return attr.Configuration[i].Name < attr.Configuration[i].Name + }) updater := console.ServiceUpdateAttributes{ Version: attr.Version, Protect: attr.Protect, From 1fc01eb8e136568e61f4a3ddbf0fc93a99f9d225 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 13 Dec 2023 11:15:54 +0100 Subject: [PATCH 086/198] optimize existing check and logs --- controller/apis/deployments/v1alpha1/provider.go | 4 ++-- controller/pkg/provider_reconciler/reconciler.go | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index 3338cc351..c5780b240 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -178,6 +178,6 @@ func (p *ProviderStatus) IsSHAEqual(sha string) bool { return p.GetSHA() == sha } -func (p *ProviderStatus) IsExisting() bool { - return p.Existing != nil && *p.Existing +func (p *ProviderStatus) HasExisting() bool { + return p.Existing != nil } diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index 7c8b0a038..018485d86 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -2,10 +2,12 @@ package providerreconciler import ( "context" + "fmt" "time" console "github.com/pluralsh/console-client-go" "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -47,7 +49,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco // Read Provider CRD from the K8S API var provider v1alpha1.Provider if err := r.Get(ctx, req.NamespacedName, &provider); err != nil { - log.Error(err, "unable to fetch provider") return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -119,8 +120,8 @@ func (r *Reconciler) handleExistingProvider(ctx context.Context, provider v1alph } func (r *Reconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Provider) (bool, error) { - if provider.Status.IsExisting() { - return true, nil + if provider.Status.HasExisting() { + return *provider.Status.Existing, nil } _, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) @@ -217,6 +218,10 @@ func (r *Reconciler) sync(ctx context.Context, provider v1alpha1.Provider, chang func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1.Provider) error { secretRef := r.getCloudProviderSettingsSecretRef(provider) + if secretRef == nil { + return fmt.Errorf("could not find secret ref configuration for cloud %q", provider.Spec.Cloud) + } + secret, err := kubernetes.GetSecret(ctx, r.Client, secretRef) if err != nil { return err @@ -230,5 +235,6 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { mgr.GetLogger().Info("Starting reconciler", "reconciler", "provider_reconciler") return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Provider{}). + Owns(&corev1.Secret{}). Complete(r) } From 1b98b329ac7a5f644bd6dec981cafb0dc636a1d2 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 11:19:35 +0100 Subject: [PATCH 087/198] fix service attr --- .../pkg/service_controller/controller.go | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 7621dc808..4353f21ea 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -254,33 +254,32 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 } attr.Helm.Chart = &val } - if service.Spec.SyncConfig != nil { - var annotations *string - var labels *string - if service.Spec.SyncConfig.Annotations != nil { - result, err := json.Marshal(service.Spec.SyncConfig.Annotations) - if err != nil { - return nil, err - } - rawAnnotations := string(result) - annotations = &rawAnnotations - } - if service.Spec.SyncConfig.Labels != nil { - result, err := json.Marshal(service.Spec.SyncConfig.Labels) - if err != nil { - return nil, err - } - rawLabels := string(result) - labels = &rawLabels + } + if service.Spec.SyncConfig != nil { + var annotations *string + var labels *string + if service.Spec.SyncConfig.Annotations != nil { + result, err := json.Marshal(service.Spec.SyncConfig.Annotations) + if err != nil { + return nil, err } - attr.SyncConfig = &console.SyncConfigAttributes{ - NamespaceMetadata: &console.MetadataAttributes{ - Labels: labels, - Annotations: annotations, - }, + rawAnnotations := string(result) + annotations = &rawAnnotations + } + if service.Spec.SyncConfig.Labels != nil { + result, err := json.Marshal(service.Spec.SyncConfig.Labels) + if err != nil { + return nil, err } + rawLabels := string(result) + labels = &rawLabels + } + attr.SyncConfig = &console.SyncConfigAttributes{ + NamespaceMetadata: &console.MetadataAttributes{ + Labels: labels, + Annotations: annotations, + }, } - } return attr, nil From 13423f3fbb47c0341bbb415615173e48fe1ac34a Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 11:20:29 +0100 Subject: [PATCH 088/198] fix sha comparison --- controller/apis/deployments/v1alpha1/cluster.go | 4 ++-- controller/pkg/cluster_controller/reconciler.go | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 3145a1395..5c0ebc475 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -388,8 +388,8 @@ func (cs *ClusterStatus) HasSHA() bool { return cs.SHA != nil && len(*cs.SHA) > 0 } -func (cs *ClusterStatus) IsSHAChanged(sha *string) bool { - return cs.HasSHA() && cs.SHA != sha +func (cs *ClusterStatus) IsSHAChanged(sha string) bool { + return cs.HasSHA() && *cs.SHA != sha } func (cs *ClusterStatus) HasExisting() bool { diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 6bd18ac8e..2d2f43196 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -22,7 +22,7 @@ import ( ) const ( - RequeueAfter = 30 * time.Second + RequeueAfter = 5 * time.Second FinalizerName = "deployments.plural.sh/cluster-protection" ) @@ -61,7 +61,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, fmt.Errorf("could not check if cluster is existing resource, got error: %+v", err) } if existing { - logger.Info("Cluster already exists in the API, running in read-only mode") + logger.V(0).Info("Cluster already exists in the API, running in read-only mode") return r.handleExisting(ctx, cluster) } @@ -84,7 +84,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } // Sync resource with Console API. - apiCluster, err := r.syncCluster(ctx, cluster, providerId, &sha) + apiCluster, err := r.sync(ctx, cluster, providerId, sha) if err != nil { return ctrl.Result{}, err } @@ -217,20 +217,20 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, nil, nil } -func (r *Reconciler) syncCluster(ctx context.Context, cluster *v1alpha1.Cluster, providerId *string, sha *string) (*console.ClusterFragment, error) { +func (r *Reconciler) sync(ctx context.Context, cluster *v1alpha1.Cluster, providerId *string, sha string) (*console.ClusterFragment, error) { exists := r.ConsoleClient.IsClusterExisting(cluster.Status.ID) logger := log.FromContext(ctx) if cluster.Status.IsSHAChanged(sha) && exists { - logger.Info("Detected changes, updating cluster") + logger.Info(fmt.Sprintf("Detected changes, updating %s cluster", cluster.Name)) return r.ConsoleClient.UpdateCluster(*cluster.Status.ID, cluster.UpdateAttributes()) } if exists { - logger.Info("No changes detected, updating cluster") + logger.V(0).Info(fmt.Sprintf("No changes detected for cluster %s", cluster.Name)) return r.ConsoleClient.GetCluster(cluster.Status.ID) } - logger.Info("Cluster does not exist, creating new one") + logger.Info(fmt.Sprintf("Cluster %s does not exist, creating new one", cluster.Name)) return r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) } From 58d76e77461c4bde82a774e1a75add473694f1ec Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 11:22:17 +0100 Subject: [PATCH 089/198] update logs --- controller/pkg/cluster_controller/reconciler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 2d2f43196..3d708e993 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -61,7 +61,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, fmt.Errorf("could not check if cluster is existing resource, got error: %+v", err) } if existing { - logger.V(0).Info("Cluster already exists in the API, running in read-only mode") + logger.V(9).Info("Cluster already exists in the API, running in read-only mode") return r.handleExisting(ctx, cluster) } @@ -227,7 +227,7 @@ func (r *Reconciler) sync(ctx context.Context, cluster *v1alpha1.Cluster, provid } if exists { - logger.V(0).Info(fmt.Sprintf("No changes detected for cluster %s", cluster.Name)) + logger.V(9).Info(fmt.Sprintf("No changes detected for cluster %s", cluster.Name)) return r.ConsoleClient.GetCluster(cluster.Status.ID) } From 6bc077da22f8bfc19c43cf99d384285e977ad5c7 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 11:32:53 +0100 Subject: [PATCH 090/198] update logs --- controller/config/crd/examples/cluster_aws.yaml | 2 +- controller/pkg/cluster_controller/reconciler.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/config/crd/examples/cluster_aws.yaml b/controller/config/crd/examples/cluster_aws.yaml index a8869b0ec..ae19b3436 100644 --- a/controller/config/crd/examples/cluster_aws.yaml +++ b/controller/config/crd/examples/cluster_aws.yaml @@ -14,7 +14,7 @@ metadata: spec: handle: aws cloud: aws - version: "1.25" + version: "1.26" protect: false providerRef: name: aws diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 3d708e993..65cfb48ed 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -227,10 +227,10 @@ func (r *Reconciler) sync(ctx context.Context, cluster *v1alpha1.Cluster, provid } if exists { - logger.V(9).Info(fmt.Sprintf("No changes detected for cluster %s", cluster.Name)) + logger.V(9).Info(fmt.Sprintf("No changes detected for %s cluster", cluster.Name)) return r.ConsoleClient.GetCluster(cluster.Status.ID) } - logger.Info(fmt.Sprintf("Cluster %s does not exist, creating new one", cluster.Name)) + logger.Info(fmt.Sprintf("%s cluster does not exist, creating it", cluster.Name)) return r.ConsoleClient.CreateCluster(cluster.Attributes(providerId)) } From acd1e59be38b100700dfe2aabf7608da7a5361f1 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 11:32:47 +0100 Subject: [PATCH 091/198] update git repo controller --- .../pkg/gitrepository_controller/controller.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 1bba6df9f..9aae076bb 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -62,13 +62,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } return ctrl.Result{}, err } - if !repo.GetDeletionTimestamp().IsZero() { - return r.handleDelete(ctx, repo) - } - cred, err := r.getRepositoryCredentials(ctx, repo) - if err != nil { - return ctrl.Result{}, err - } // Check if resource already exists in the API and only sync the ID exists, err := r.isAlreadyExists(repo) @@ -79,7 +72,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu logger.Info("repository already exists in the API, running in read-only mode") return r.handleExistingRepo(ctx, repo) } - + if !repo.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, repo) + } + cred, err := r.getRepositoryCredentials(ctx, repo) + if err != nil { + return ctrl.Result{}, err + } sha, err := utils.HashObject(cred) if err != nil { return ctrl.Result{}, err From edc436db85ff640e3ad797a646162db0e5082e7b Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 11:33:49 +0100 Subject: [PATCH 092/198] revert requeue time --- controller/pkg/cluster_controller/reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 65cfb48ed..efe06782b 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -22,7 +22,7 @@ import ( ) const ( - RequeueAfter = 5 * time.Second + RequeueAfter = 30 * time.Second FinalizerName = "deployments.plural.sh/cluster-protection" ) From bc5b23a70fb4e8a84713bbb8d0dc8433c3b43f31 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 13 Dec 2023 12:02:58 +0100 Subject: [PATCH 093/198] update provider logs --- .../config/crd/examples/all_in_one_gcp.yaml | 64 +++++++++++++++++++ .../pkg/provider_reconciler/reconciler.go | 14 +++- 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 controller/config/crd/examples/all_in_one_gcp.yaml diff --git a/controller/config/crd/examples/all_in_one_gcp.yaml b/controller/config/crd/examples/all_in_one_gcp.yaml new file mode 100644 index 000000000..51c84f374 --- /dev/null +++ b/controller/config/crd/examples/all_in_one_gcp.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: operator +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: gcp +spec: + name: gcp + namespace: operator + cloud: gcp + cloudSettings: + gcp: + name: credentials + namespace: operator +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: gcp + namespace: operator +spec: + handle: gcp + cloud: gcp + version: "1.25.14" + protect: false + providerRef: + name: gcp + cloudSettings: + gcp: + region: europe-central2 + network: operator + project: pluralsh-test-384515 + tags: + managed-by: plural-operator +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: k8shelm +spec: + url: https://github.com/zreigz/k8s-helm.git +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: ServiceDeployment +metadata: + name: k8saws + namespace: default +spec: + version: 0.0.1 + git: + folder: nginx + ref: master + repositoryRef: + kind: GitRepository + name: k8shelm + namespace: default + clusterRef: + kind: Cluster + name: aws + namespace: default diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index 018485d86..f597e7ef3 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -32,7 +32,11 @@ type Reconciler struct { } const ( - RequeueAfter = 30 * time.Second + // RequeueAfter is the time between scheduled reconciles if there are no + // changes to the CRD. + RequeueAfter = 30 * time.Second + // FinalizerName defines name for the main finalizer that synchronizes + // resource deletion from the Console API prior to removing the CRD. FinalizerName = "providers.deployments.plural.sh/finalizer" ) @@ -58,7 +62,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, err } if exists { - log.Info("Provider already exists in the API, running in read-only mode") return r.handleExistingProvider(ctx, provider) } @@ -133,7 +136,12 @@ func (r *Reconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Prov return false, err } - return !provider.Status.HasID(), nil + if !provider.Status.HasID() { + log.FromContext(ctx).Info("Provider already exists in the API, running in read-only mode") + return true, nil + } + + return false, nil } func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { From 8facec4053209db48185de56ca51e51d773addfc Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 12:40:13 +0100 Subject: [PATCH 094/198] update Makefile --- controller/Makefile | 95 ++++---- controller/go.mod | 177 ++++++++++++++- controller/go.sum | 523 ++++++++++++++++++++++++++++++++++++++++++-- controller/tools.go | 7 + 4 files changed, 735 insertions(+), 67 deletions(-) create mode 100644 controller/tools.go diff --git a/controller/Makefile b/controller/Makefile index fc622de2a..850e5b2aa 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -1,51 +1,25 @@ # Image URL to use all building/pushing image targets IMG ?= deployment-controller:latest -CRD_OPTIONS ?= "crd" -# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifndef GOPATH +$(error $$GOPATH environment variable not set) +endif + +ifeq (,$(findstring $(GOPATH)/bin,$(PATH))) +$(error $$GOPATH/bin directory is not in your $$PATH) +endif + ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif -OPENAPI_PATH=$(GOPATH)/src/k8s.io/kube-openapi - -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 - -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) - CONTROLLER_GEN = $(shell pwd)/bin/controller-gen controller-gen: ## Download controller-gen locally if necessary. $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3) ##@ Build -manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases - -generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - go generate ./pkg/... - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." - -fmt: ## Run go fmt against code. - go fmt ./... - -vet: ## Run go vet against code. - go vet ./... docker-build: ## Build docker image with the driver. docker build --no-cache -t ${IMG} . @@ -53,9 +27,6 @@ docker-build: ## Build docker image with the driver. docker-push: ## Push docker image with the driver. docker push ${IMG} -build: manifests generate fmt vet ## Build controller. - go build -o bin/deployment-controller main.go - genmock: ## generates mocks before running tests hack/gen-client-mocks.sh @@ -78,3 +49,53 @@ GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ rm -rf $$TMP_DIR ;\ } endef + +##@ General + +.PHONY: help +help: ## show 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) + +.PHONY: show-dependency-updates +show-dependency-updates: ## show possible dependency updates + go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}} {{.Version}} -> {{.Update.Version}}{{end}}' -m all + +.PHONY: update-dependencies +update-dependencies: ## update dependencies + go get -u ./... + go mod tidy + +.PHONY: install-tools +install-tools: ## install required tools + @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI {} go install {} + +##@ Build + +.PHONY: build +build: manifests generate ## build + go build -o bin/deployment-controller main.go + +##@ Codegen + +.PHONY: manifests +manifests: controller-gen ## generate Kubernetes manifests + $(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations + go generate ./pkg/... + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +##@ Tests + +.PHONY: test +test: ## run tests + go test ./... -v + +.PHONY: lint +lint: install-tools ## run linters + golangci-lint run ./... + +.PHONY: fix +fix: install-tools ## fix issues found by linters + golangci-lint run --fix ./... diff --git a/controller/go.mod b/controller/go.mod index b7d54f259..d181e7d61 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -6,7 +6,8 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 - github.com/go-logr/logr v1.2.3 + github.com/go-logr/logr v1.2.4 + github.com/golangci/golangci-lint v1.55.1 github.com/pluralsh/console-client-go v0.0.55 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.33.0 @@ -22,68 +23,222 @@ require ( ) require ( + 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect + 4d63.com/gochecknoglobals v0.2.1 // indirect + github.com/4meepo/tagalign v1.3.3 // indirect + github.com/Abirdcfly/dupword v0.0.13 // indirect + github.com/Antonboom/errname v0.1.12 // indirect + github.com/Antonboom/nilnil v0.1.7 // indirect + github.com/Antonboom/testifylint v0.2.3 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/OpenPeeDeeP/depguard/v2 v2.1.0 // indirect + github.com/alecthomas/go-check-sumtype v0.1.3 // indirect + github.com/alexkohler/nakedret/v2 v2.0.2 // indirect + github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alingse/asasalint v0.0.11 // indirect + github.com/ashanbrown/forbidigo v1.6.0 // indirect + github.com/ashanbrown/makezero v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bkielbasa/cyclop v1.2.1 // indirect + github.com/blizzy78/varnamelen v0.8.0 // indirect + github.com/bombsimon/wsl/v3 v3.4.0 // indirect + github.com/breml/bidichk v0.2.7 // indirect + github.com/breml/errchkjson v0.3.6 // indirect + github.com/butuzov/ireturn v0.2.2 // indirect + github.com/butuzov/mirror v1.1.0 // indirect + github.com/catenacyber/perfsprint v0.2.0 // indirect + github.com/ccojocar/zxcvbn-go v1.0.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/charithe/durationcheck v0.0.10 // indirect + github.com/chavacava/garif v0.1.0 // indirect + github.com/curioswitch/go-reassign v0.2.0 // indirect + github.com/daixiang0/gci v0.11.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denis-tingaikin/go-header v0.4.3 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/esimonov/ifshort v1.0.4 // indirect + github.com/ettle/strcase v0.1.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/firefart/nonamedreturns v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/ghostiam/protogetter v0.2.3 // indirect + github.com/go-critic/go-critic v0.9.0 // 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.22.3 // indirect + github.com/go-toolsmith/astcast v1.1.0 // indirect + github.com/go-toolsmith/astcopy v1.1.0 // indirect + github.com/go-toolsmith/astequal v1.1.0 // indirect + github.com/go-toolsmith/astfmt v1.1.0 // indirect + github.com/go-toolsmith/astp v1.1.0 // indirect + github.com/go-toolsmith/strparse v1.1.0 // indirect + github.com/go-toolsmith/typep v1.1.0 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect + github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect + github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect + github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect + github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect + github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect + github.com/golangci/misspell v0.4.1 // indirect + github.com/golangci/revgrep v0.5.2 // indirect + github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.1 // indirect + github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 // indirect + github.com/gostaticanalysis/analysisutil v0.7.1 // indirect + github.com/gostaticanalysis/comment v1.4.2 // indirect + github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect + github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jgautheron/goconst v1.6.0 // indirect + github.com/jingyugao/rowserrcheck v1.1.1 // indirect + github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/julz/importas v0.1.0 // indirect + github.com/kisielk/errcheck v1.6.3 // indirect + github.com/kisielk/gotool v1.0.0 // indirect + github.com/kkHAIKE/contextcheck v1.1.4 // indirect + github.com/kulti/thelper v0.6.3 // indirect + github.com/kunwardeep/paralleltest v1.0.8 // indirect + github.com/kyoh86/exportloopref v0.1.11 // indirect + github.com/ldez/gomoddirectives v0.2.3 // indirect + github.com/ldez/tagliatelle v0.5.0 // indirect + github.com/leonklingele/grouper v1.1.1 // indirect + github.com/lufeee/execinquery v1.2.1 // indirect + github.com/macabu/inamedparam v0.1.2 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/maratori/testableexamples v1.0.0 // indirect + github.com/maratori/testpackage v1.1.1 // indirect + github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mbilski/exhaustivestruct v1.2.0 // indirect + github.com/mgechev/revive v1.3.4 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/moricho/tparallel v0.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.7.0 // indirect - github.com/onsi/gomega v1.24.2 // indirect + github.com/nakabonne/nestif v0.3.1 // indirect + github.com/nishanths/exhaustive v0.11.0 // indirect + github.com/nishanths/predeclared v0.2.2 // indirect + github.com/nunnatsa/ginkgolinter v0.14.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polyfloyd/go-errorlint v1.4.5 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/quasilyte/go-ruleguard v0.4.0 // indirect + github.com/quasilyte/gogrep v0.5.0 // indirect + github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect + github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/ryancurrah/gomodguard v1.3.0 // indirect + github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect + github.com/sashamelentyev/interfacebloat v1.1.0 // indirect + github.com/sashamelentyev/usestdlibvars v1.24.0 // indirect github.com/schollz/progressbar/v3 v3.8.6 // indirect + github.com/securego/gosec/v2 v2.18.2 // indirect + github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sivchari/containedctx v1.0.3 // indirect + github.com/sivchari/nosnakecase v1.7.0 // indirect + github.com/sivchari/tenv v1.7.1 // indirect + github.com/sonatard/noctx v0.0.2 // indirect + github.com/sourcegraph/go-diff v0.7.0 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/viper v1.12.0 // indirect + github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect + github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect + github.com/tdakkota/asciicheck v0.2.0 // indirect + github.com/tetafro/godot v1.4.15 // indirect + github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect + github.com/timonwong/loggercheck v0.9.4 // indirect + github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect + github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect + github.com/ultraware/funlen v0.1.0 // indirect + github.com/ultraware/whitespace v0.0.5 // indirect + github.com/uudashr/gocognit v1.1.2 // indirect github.com/vektah/gqlparser/v2 v2.5.10 // indirect + github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/yagipy/maintidx v1.0.0 // indirect + github.com/yeya24/promlinter v0.2.0 // indirect + github.com/ykadowak/zerologlint v0.1.3 // indirect + gitlab.com/bosi/decorder v0.4.1 // indirect + go-simpler.org/sloglint v0.1.2 // indirect + go.tmz.dev/musttag v0.7.2 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.5.0 // indirect - golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.4.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect + golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + honnef.co/go/tools v0.4.6 // indirect k8s.io/apiextensions-apiserver v0.26.0 // indirect k8s.io/component-base v0.26.0 // indirect k8s.io/klog/v2 v2.80.1 // indirect k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect k8s.io/utils v0.0.0-20230115233650-391b47cb4029 // indirect + mvdan.cc/gofumpt v0.5.0 // indirect + mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect + mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect + mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // 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/controller/go.sum b/controller/go.sum index 6a86615d1..534587aa8 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -1,8 +1,13 @@ +4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= +4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= +4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= +4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= 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.44.3/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= @@ -13,6 +18,9 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV 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.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 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= @@ -30,52 +38,140 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo 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= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/4meepo/tagalign v1.3.3 h1:ZsOxcwGD/jP4U/aw7qeWu58i7dwYemfy5Y+IF1ACoNw= +github.com/4meepo/tagalign v1.3.3/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= +github.com/Abirdcfly/dupword v0.0.13 h1:SMS17YXypwP000fA7Lr+kfyBQyW14tTT+nRv9ASwUUo= +github.com/Abirdcfly/dupword v0.0.13/go.mod h1:Ut6Ue2KgF/kCOawpW4LnExT+xZLQviJPE4klBPMK/5Y= +github.com/Antonboom/errname v0.1.12 h1:oh9ak2zUtsLp5oaEd/erjB4GPu9w19NyoIskZClDcQY= +github.com/Antonboom/errname v0.1.12/go.mod h1:bK7todrzvlaZoQagP1orKzWXv59X/x0W0Io2XT1Ssro= +github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTow= +github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= +github.com/Antonboom/testifylint v0.2.3 h1:MFq9zyL+rIVpsvLX4vDPLojgN7qODzWsrnftNX2Qh60= +github.com/Antonboom/testifylint v0.2.3/go.mod h1:IYaXaOX9NbfAyO+Y04nfjGI8wDemC1rUyM/cYolz018= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 h1:3ZBs7LAezy8gh0uECsA6CGU43FF3zsx5f4eah5FxTMA= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0/go.mod h1:rZLTje5A9kFBe0pzhpe2TdhRniBF++PRHQuRpR8esVc= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= +github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/Yamashou/gqlgenc v0.16.0 h1:k1X/dvwnkiDImaeYw+C1j+GDX3MnzB4aONOTE6Mrku4= github.com/Yamashou/gqlgenc v0.16.0/go.mod h1:yKaNzczoGrIElG3mG8j2Bg3imv4WyIjLSTRBtvhfMtU= +github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= +github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/go-check-sumtype v0.1.3 h1:M+tqMxB68hcgccRXBMVCPI4UJ+QUfdSx0xdbypKCqA8= +github.com/alecthomas/go-check-sumtype v0.1.3/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/alexkohler/nakedret/v2 v2.0.2 h1:qnXuZNvv3/AxkAb22q/sEsEpcA99YxLFACDtEw9TPxE= +github.com/alexkohler/nakedret/v2 v2.0.2/go.mod h1:2b8Gkk0GsOrqQv/gPWjNLDSKwG8I5moSXG1K4VIBcTQ= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= +github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= 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/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= +github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= +github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= +github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= +github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= +github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= +github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= +github.com/butuzov/ireturn v0.2.2 h1:jWI36dxXwVrI+RnXDwux2IZOewpmfv930OuIRfaBUJ0= +github.com/butuzov/ireturn v0.2.2/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= +github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= +github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= +github.com/catenacyber/perfsprint v0.2.0 h1:azOocHLscPjqXVJ7Mf14Zjlkn4uNua0+Hcg1wTR6vUo= +github.com/catenacyber/perfsprint v0.2.0/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= +github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= +github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= 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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= +github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= +github.com/daixiang0/gci v0.11.2 h1:Oji+oPsp3bQ6bNNgX30NBAVT18P4uBH4sRZnlOlTj7Y= +github.com/daixiang0/gci v0.11.2/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= 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/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= +github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= +github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= +github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= +github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= +github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/ghostiam/protogetter v0.2.3 h1:qdv2pzo3BpLqezwqfGDLZ+nHEYmc5bUpIdsMbBVwMjw= +github.com/ghostiam/protogetter v0.2.3/go.mod h1:KmNLOsy1v04hKbvZs8EfGI1fk39AgTdRDxWNYPfXVc4= +github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= +github.com/go-critic/go-critic v0.9.0/go.mod h1:5P8tdXL7m/6qnyG6oRAlYLORvoXH0WDypYgAEmagT40= 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= @@ -90,8 +186,8 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE 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/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.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= @@ -103,6 +199,32 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= +github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= +github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 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= @@ -134,8 +256,29 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD 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/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6J5HIP8ZtyMdiDscjMLfRBSPuzVVeo= +github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= +github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= +github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= +github.com/golangci/golangci-lint v1.55.1 h1:DL2j9Eeapg1N3WEkKnQFX5L40SYtjZZJjGVdyEgNrDc= +github.com/golangci/golangci-lint v1.55.1/go.mod h1:z00biPRqjo5MISKV1+RWgONf2KvrPDmfqxHpHKB6bI4= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= +github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= +github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU= +github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 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/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= @@ -147,15 +290,19 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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.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-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/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/martian/v3 v3.1.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= @@ -163,17 +310,58 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf 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/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/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/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8= +github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= +github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= +github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= +github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jgautheron/goconst v1.6.0 h1:gbMLWKRMkzAc6kYsQL6/TxaoBUg3Jm9LSF/Ih1ADWGA= +github.com/jgautheron/goconst v1.6.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= 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= @@ -186,31 +374,80 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 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/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= +github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= +github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= +github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= 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/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.8 h1:Ul2KsqtzFxTlSU7IP0JusWlLiNqQaloB9vguyjbE558= +github.com/kunwardeep/paralleltest v1.0.8/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= +github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= +github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= +github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= +github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= +github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= +github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= +github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= +github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= +github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= +github.com/macabu/inamedparam v0.1.2 h1:RR5cnayM6Q7cDhQol32DE2BGAPGMnffJ31LFE+UklaU= +github.com/macabu/inamedparam v0.1.2/go.mod h1:Xg25QvY7IBRl1KLPV9Rbml8JOMZtF/iAkNkmV7eQgjw= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 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.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo= +github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= +github.com/mgechev/revive v1.3.4 h1:k/tO3XTaWY4DEHal9tWBkkUMJYO/dLDVyMmAQxmIMDc= +github.com/mgechev/revive v1.3.4/go.mod h1:W+pZCMu9qj8Uhfs1iJMQsEFLRozUfvwFwqVvRbSNLVw= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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= @@ -218,24 +455,50 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN 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/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= +github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= 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/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= -github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nishanths/exhaustive v0.11.0 h1:T3I8nUGhl/Cwu5Z2hfc92l0e04D2GEW6e0l8pzda2l0= +github.com/nishanths/exhaustive v0.11.0/go.mod h1:RqwDsZ1xY0dNdqHho2z6X+bgzizwbLYOWnZbbl2wLB4= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.14.1 h1:khx0CqR5U4ghsscjJ+lZVthp3zjIFytRXPTaQ/TMiyA= +github.com/nunnatsa/ginkgolinter v0.14.1/go.mod h1:nY0pafUSst7v7F637e7fymaMlQqI9c0Wka2fGsDkzWg= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= +github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc= +github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= 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/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pluralsh/console-client-go v0.0.55 h1:+j1Ur8ixNx4se4NEfTcul87/oVhUqFs+ZdsvCzvPYFM= github.com/pluralsh/console-client-go v0.0.55/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= 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/polyfloyd/go-errorlint v1.4.5 h1:70YWmMy4FgRHehGNOUask3HtSFSOLKgmDn7ryNe7LqI= +github.com/polyfloyd/go-errorlint v1.4.5/go.mod h1:sIZEbFoDOCnTYYZoVkjc4hTnM459tuWA9H/EkdXwsKk= 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= @@ -262,20 +525,73 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/quasilyte/go-ruleguard v0.4.0 h1:DyM6r+TKL+xbKB4Nm7Afd1IQh9kEUKQs2pboWGKtvQo= +github.com/quasilyte/go-ruleguard v0.4.0/go.mod h1:Eu76Z/R8IXtViWUIHkE3p8gdH3/PKk1eh3YGfaEof10= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= 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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= +github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk= github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= +github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= +github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.24.0 h1:MKNzmXtGh5N0y74Z/CIaJh4GlB364l0K1RUT08WSWAc= +github.com/sashamelentyev/usestdlibvars v1.24.0/go.mod h1:9cYkq+gYJ+a5W2RPdhfaSCnTVUC1OQP/bSiiBhq3OZE= github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= +github.com/securego/gosec/v2 v2.18.2 h1:DkDt3wCiOtAHf1XkiXZBhQ6m6mK/b9T/wD257R3/c+I= +github.com/securego/gosec/v2 v2.18.2/go.mod h1:xUuqSF6i0So56Y2wwohWAmB07EdBkUN6crbLlHwbyJs= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt95do8= +github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= +github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= +github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= +github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= +github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= +github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= 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= @@ -291,19 +607,65 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= +github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= +github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.4.15 h1:QzdIs+XB8q+U1WmQEWKHQbKmCw06QuQM7gLx/dky2RM= +github.com/tetafro/godot v1.4.15/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= +github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= +github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= +github.com/tomarrell/wrapcheck/v2 v2.8.1 h1:HxSqDSN0sAt0yJYsrcYVoEeyM4aI9yAm3KQpIXDJRhQ= +github.com/tomarrell/wrapcheck/v2 v2.8.1/go.mod h1:/n2Q3NZ4XFT50ho6Hbxg+RV1uyo2Uow/Vdm9NQcl5SE= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= +github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= +github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= +github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= +github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= +github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= +github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= +github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= +github.com/ykadowak/zerologlint v0.1.3 h1:TLy1dTW3Nuc+YE3bYRPToG1Q9Ej78b5UUN6bjbGdxPE= +github.com/ykadowak/zerologlint v0.1.3/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= 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= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/bosi/decorder v0.4.1 h1:VdsdfxhstabyhZovHafFw+9eJ6eU0d2CkFNJcZz/NU4= +gitlab.com/bosi/decorder v0.4.1/go.mod h1:jecSqWUew6Yle1pCr2eLWTensJMmsxHsBwt+PVbkAqA= +go-simpler.org/assert v0.6.0 h1:QxSrXa4oRuo/1eHMXSBFHKvJIpWABayzKldqZyugG7E= +go-simpler.org/assert v0.6.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= +go-simpler.org/sloglint v0.1.2 h1:IjdhF8NPxyn0Ckn2+fuIof7ntSnVUAqBFcQRrnG9AiM= +go-simpler.org/sloglint v0.1.2/go.mod h1:2LL+QImPfTslD5muNPydAEYmpXIj6o/WYcqnJjLi4o4= 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= +go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= 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= @@ -320,9 +682,13 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U 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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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= @@ -333,8 +699,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 h1:jWGQJV4niP+CCmFW9ekjA9Zx8vYORzOUH2/Nl5WPuLQ= +golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 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= @@ -347,6 +717,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl 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/lint v0.0.0-20201208152925-83fdc39ff7b5/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= @@ -355,6 +726,17 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB 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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -384,20 +766,36 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R 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-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 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= @@ -408,6 +806,11 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ 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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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= @@ -439,30 +842,57 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/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-20210423185535-09eb48e85fd7/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -476,6 +906,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 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-20190321232350-e250d351ecad/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= @@ -483,6 +914,7 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw 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-20190910044552-dd2b5c81c578/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= @@ -503,16 +935,43 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK 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-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 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-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 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-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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= @@ -535,6 +994,9 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M 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.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 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= @@ -572,7 +1034,14 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc 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-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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= @@ -585,6 +1054,10 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa 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.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 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= @@ -608,6 +1081,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV 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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -629,6 +1104,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh 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= +honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= +honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= @@ -647,6 +1124,14 @@ k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+O k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556WaVQylDXswKmX+dE= k8s.io/utils v0.0.0-20230115233650-391b47cb4029/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d h1:3rvTIIM22r9pvXk+q3swxUQAQOxksVMGK7sml4nG57w= +mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RFiZMMsLVL+A96Nvptar8Fj71is= 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= diff --git a/controller/tools.go b/controller/tools.go new file mode 100644 index 000000000..966059009 --- /dev/null +++ b/controller/tools.go @@ -0,0 +1,7 @@ +//go:build tools + +package main + +import ( + _ "github.com/golangci/golangci-lint/cmd/golangci-lint" +) From 5c11ac980184eeb92f1cfb742fb1828b3044ebbb Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 12:43:01 +0100 Subject: [PATCH 095/198] use new method of installing controller gen --- controller/Makefile | 31 ++---------- controller/go.mod | 45 ++++++++--------- controller/go.sum | 117 ++++++++++++++++++++++---------------------- controller/tools.go | 1 + 4 files changed, 86 insertions(+), 108 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 850e5b2aa..92a3e48c2 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -1,5 +1,4 @@ -# Image URL to use all building/pushing image targets -IMG ?= deployment-controller:latest +IMG ?= deployment-controller:latest # Image URL to use all building/pushing image targets ifndef GOPATH $(error $$GOPATH environment variable not set) @@ -9,16 +8,6 @@ ifeq (,$(findstring $(GOPATH)/bin,$(PATH))) $(error $$GOPATH/bin directory is not in your $$PATH) endif -ifeq (,$(shell go env GOBIN)) -GOBIN=$(shell go env GOPATH)/bin -else -GOBIN=$(shell go env GOBIN) -endif - -CONTROLLER_GEN = $(shell pwd)/bin/controller-gen -controller-gen: ## Download controller-gen locally if necessary. - $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3) - ##@ Build docker-build: ## Build docker image with the driver. @@ -36,20 +25,6 @@ apply-crds: ## applies CRDs apply-cluster-byok: apply-crds ## applies BYOK cluster kubectl apply -f config/crd/examples/cluster_byok.yaml -# go-get-tool will 'go get' any package $2 and install it to $1. -PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) -define go-get-tool -@[ -f $(1) ] || { \ -set -e ;\ -TMP_DIR=$$(mktemp -d) ;\ -cd $$TMP_DIR ;\ -go mod init tmp ;\ -echo "Downloading $(2)" ;\ -GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ -rm -rf $$TMP_DIR ;\ -} -endef - ##@ General .PHONY: help @@ -78,11 +53,11 @@ build: manifests generate ## build ##@ Codegen .PHONY: manifests -manifests: controller-gen ## generate Kubernetes manifests +manifests: install-tools ## generate Kubernetes manifests $(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate -generate: controller-gen ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations +generate: install-tools ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations go generate ./pkg/... $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." diff --git a/controller/go.mod b/controller/go.mod index d181e7d61..9a32f9d91 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -14,11 +14,12 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 - k8s.io/api v0.26.0 - k8s.io/apimachinery v0.26.0 - k8s.io/client-go v0.26.0 + k8s.io/api v0.28.0 + k8s.io/apimachinery v0.28.0 + k8s.io/client-go v0.28.0 k8s.io/klog v1.0.0 sigs.k8s.io/controller-runtime v0.14.1 + sigs.k8s.io/controller-tools v0.13.0 ) @@ -52,7 +53,7 @@ require ( github.com/catenacyber/perfsprint v0.2.0 // indirect github.com/ccojocar/zxcvbn-go v1.0.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect @@ -62,7 +63,6 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.1.1 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -72,8 +72,8 @@ require ( github.com/ghostiam/protogetter v0.2.3 // indirect github.com/go-critic/go-critic v0.9.0 // 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/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect @@ -83,6 +83,7 @@ require ( github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect + github.com/gobuffalo/flect v1.0.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -97,7 +98,7 @@ require ( github.com/golangci/misspell v0.4.1 // indirect github.com/golangci/revgrep v0.5.2 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.1 // indirect @@ -158,10 +159,10 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polyfloyd/go-errorlint v1.4.5 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/quasilyte/go-ruleguard v0.4.0 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect @@ -208,14 +209,14 @@ require ( gitlab.com/bosi/decorder v0.4.1 // indirect go-simpler.org/sloglint v0.1.2 // indirect go.tmz.dev/musttag v0.7.2 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect + golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect @@ -224,22 +225,22 @@ require ( golang.org/x/tools v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.4.6 // indirect - k8s.io/apiextensions-apiserver v0.26.0 // indirect - k8s.io/component-base v0.26.0 // indirect - k8s.io/klog/v2 v2.80.1 // indirect - k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect - k8s.io/utils v0.0.0-20230115233650-391b47cb4029 // indirect + k8s.io/apiextensions-apiserver v0.28.0 // indirect + k8s.io/component-base v0.28.0 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect mvdan.cc/gofumpt v0.5.0 // indirect mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/controller/go.sum b/controller/go.sum index 534587aa8..e90bf041e 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -115,8 +115,9 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= @@ -129,6 +130,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= github.com/daixiang0/gci v0.11.2 h1:Oji+oPsp3bQ6bNNgX30NBAVT18P4uBH4sRZnlOlTj7Y= @@ -138,7 +140,6 @@ 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/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -178,11 +179,9 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 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-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 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= @@ -190,12 +189,10 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.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/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -221,6 +218,8 @@ github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUN github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -281,8 +280,8 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSW github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 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/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -313,8 +312,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= @@ -389,7 +388,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -414,8 +413,6 @@ github.com/macabu/inamedparam v0.1.2 h1:RR5cnayM6Q7cDhQol32DE2BGAPGMnffJ31LFE+Uk github.com/macabu/inamedparam v0.1.2/go.mod h1:Xg25QvY7IBRl1KLPV9Rbml8JOMZtF/iAkNkmV7eQgjw= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -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.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= @@ -469,8 +466,12 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nunnatsa/ginkgolinter v0.14.1 h1:khx0CqR5U4ghsscjJ+lZVthp3zjIFytRXPTaQ/TMiyA= github.com/nunnatsa/ginkgolinter v0.14.1/go.mod h1:nY0pafUSst7v7F637e7fymaMlQqI9c0Wka2fGsDkzWg= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= @@ -504,27 +505,27 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn 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.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.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/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 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/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/quasilyte/go-ruleguard v0.4.0 h1:DyM6r+TKL+xbKB4Nm7Afd1IQh9kEUKQs2pboWGKtvQo= github.com/quasilyte/go-ruleguard v0.4.0/go.mod h1:Eu76Z/R8IXtViWUIHkE3p8gdH3/PKk1eh3YGfaEof10= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= @@ -592,7 +593,6 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= -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= @@ -605,6 +605,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= @@ -666,13 +667,15 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= @@ -774,8 +777,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= @@ -793,9 +794,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 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= @@ -859,7 +859,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1035,7 +1034,6 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D 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-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1070,8 +1068,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/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= @@ -1083,6 +1081,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1092,7 +1092,6 @@ 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.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -1106,24 +1105,24 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= -k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= -k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= -k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= -k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= -k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= -k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= -k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= -k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= +k8s.io/api v0.28.0 h1:3j3VPWmN9tTDI68NETBWlDiA9qOiGJ7sdKeufehBYsM= +k8s.io/api v0.28.0/go.mod h1:0l8NZJzB0i/etuWnIXcwfIv+xnDOhL3lLW919AWYDuY= +k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= +k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= +k8s.io/apimachinery v0.28.0 h1:ScHS2AG16UlYWk63r46oU3D5y54T53cVI5mMJwwqFNA= +k8s.io/apimachinery v0.28.0/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw= +k8s.io/client-go v0.28.0 h1:ebcPRDZsCjpj62+cMk1eGNX1QkMdRmQ6lmz5BLoFWeM= +k8s.io/client-go v0.28.0/go.mod h1:0Asy9Xt3U98RypWJmU1ZrRAGKhP6NqDPmptlAzK2kMc= +k8s.io/component-base v0.28.0 h1:HQKy1enJrOeJlTlN4a6dU09wtmXaUvThC0irImfqyxI= +k8s.io/component-base v0.28.0/go.mod h1:Yyf3+ZypLfMydVzuLBqJ5V7Kx6WwDr/5cN+dFjw1FNk= 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-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= -k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556WaVQylDXswKmX+dE= -k8s.io/utils v0.0.0-20230115233650-391b47cb4029/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= @@ -1137,8 +1136,10 @@ 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/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM= sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= -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/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= +sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/controller/tools.go b/controller/tools.go index 966059009..655a352e1 100644 --- a/controller/tools.go +++ b/controller/tools.go @@ -4,4 +4,5 @@ package main import ( _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" ) From c9696976af6d32cbbf62baf3673252c3b51938a7 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 12:54:15 +0100 Subject: [PATCH 096/198] fix dependencies --- controller/go.mod | 21 +++++++++-------- controller/go.sum | 58 ++++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/controller/go.mod b/controller/go.mod index 9a32f9d91..55322585b 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -10,17 +10,16 @@ require ( github.com/golangci/golangci-lint v1.55.1 github.com/pluralsh/console-client-go v0.0.55 github.com/pluralsh/polly v0.1.4 - github.com/samber/lo v1.33.0 + github.com/samber/lo v1.39.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 - k8s.io/api v0.28.0 - k8s.io/apimachinery v0.28.0 - k8s.io/client-go v0.28.0 + k8s.io/api v0.26.0 + k8s.io/apimachinery v0.26.0 + k8s.io/client-go v0.26.0 k8s.io/klog v1.0.0 sigs.k8s.io/controller-runtime v0.14.1 - sigs.k8s.io/controller-tools v0.13.0 - + sigs.k8s.io/controller-tools v0.11.1 ) require ( @@ -98,7 +97,7 @@ require ( github.com/golangci/misspell v0.4.1 // indirect github.com/golangci/revgrep v0.5.2 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.1 // indirect @@ -209,7 +208,7 @@ require ( gitlab.com/bosi/decorder v0.4.1 // indirect go-simpler.org/sloglint v0.1.2 // indirect go.tmz.dev/musttag v0.7.2 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect @@ -231,8 +230,8 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.4.6 // indirect - k8s.io/apiextensions-apiserver v0.28.0 // indirect - k8s.io/component-base v0.28.0 // indirect + k8s.io/apiextensions-apiserver v0.26.0 // indirect + k8s.io/component-base v0.26.0 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect @@ -244,3 +243,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 diff --git a/controller/go.sum b/controller/go.sum index e90bf041e..d92d5f597 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -140,6 +140,7 @@ 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/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -153,8 +154,8 @@ github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcH github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -280,8 +281,8 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSW github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 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/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -312,8 +313,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= @@ -388,6 +389,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -544,8 +546,8 @@ github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJ github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= -github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk= -github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= @@ -593,6 +595,7 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= +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= @@ -620,8 +623,6 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.4.15 h1:QzdIs+XB8q+U1WmQEWKHQbKmCw06QuQM7gLx/dky2RM= github.com/tetafro/godot v1.4.15/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= -github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= @@ -667,12 +668,11 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -1034,6 +1034,7 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D 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-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1092,6 +1093,7 @@ 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.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -1105,22 +1107,22 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -k8s.io/api v0.28.0 h1:3j3VPWmN9tTDI68NETBWlDiA9qOiGJ7sdKeufehBYsM= -k8s.io/api v0.28.0/go.mod h1:0l8NZJzB0i/etuWnIXcwfIv+xnDOhL3lLW919AWYDuY= -k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= -k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= -k8s.io/apimachinery v0.28.0 h1:ScHS2AG16UlYWk63r46oU3D5y54T53cVI5mMJwwqFNA= -k8s.io/apimachinery v0.28.0/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw= -k8s.io/client-go v0.28.0 h1:ebcPRDZsCjpj62+cMk1eGNX1QkMdRmQ6lmz5BLoFWeM= -k8s.io/client-go v0.28.0/go.mod h1:0Asy9Xt3U98RypWJmU1ZrRAGKhP6NqDPmptlAzK2kMc= -k8s.io/component-base v0.28.0 h1:HQKy1enJrOeJlTlN4a6dU09wtmXaUvThC0irImfqyxI= -k8s.io/component-base v0.28.0/go.mod h1:Yyf3+ZypLfMydVzuLBqJ5V7Kx6WwDr/5cN+dFjw1FNk= +k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= +k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= +k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= +k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= +k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= +k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= +k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= +k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= 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.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 h1:8cNCQs+WqqnSpZ7y0LMQPKD+RZUHU17VqLPMW3qxnxc= +k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596/go.mod h1:/BYxry62FuDzmI+i9B+X2pqfySRmSOW2ARmj5Zbqhj0= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= @@ -1136,8 +1138,8 @@ 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/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM= sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= -sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= -sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= +sigs.k8s.io/controller-tools v0.11.1 h1:blfU7DbmXuACWHfpZR645KCq8cLOc6nfkipGSGnH+Wk= +sigs.k8s.io/controller-tools v0.11.1/go.mod h1:dm4bN3Yp1ZP+hbbeSLF8zOEHsI1/bf15u3JNcgRv2TM= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= From 60e8a730c7d3aeb42d37ecd8aa8ed3c97d91d578 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 12:57:39 +0100 Subject: [PATCH 097/198] update Makefile --- controller/Makefile | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 92a3e48c2..be9f6a9aa 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -8,23 +8,6 @@ ifeq (,$(findstring $(GOPATH)/bin,$(PATH))) $(error $$GOPATH/bin directory is not in your $$PATH) endif -##@ Build - -docker-build: ## Build docker image with the driver. - docker build --no-cache -t ${IMG} . - -docker-push: ## Push docker image with the driver. - docker push ${IMG} - -genmock: ## generates mocks before running tests - hack/gen-client-mocks.sh - -apply-crds: ## applies CRDs - kubectl apply -f config/crd/bases - -apply-cluster-byok: apply-crds ## applies BYOK cluster - kubectl apply -f config/crd/examples/cluster_byok.yaml - ##@ General .PHONY: help @@ -50,6 +33,13 @@ install-tools: ## install required tools build: manifests generate ## build go build -o bin/deployment-controller main.go +docker-build: ## build Docker image with the driver + docker build --no-cache -t ${IMG} . + +docker-push: ## push Docker image with the driver + docker push ${IMG} + + ##@ Codegen .PHONY: manifests @@ -61,10 +51,14 @@ generate: install-tools ## generate code containing DeepCopy, DeepCopyInto, and go generate ./pkg/... $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." +.PHONY: genmock +genmock: ## generates mocks before running tests + hack/gen-client-mocks.sh + ##@ Tests .PHONY: test -test: ## run tests +test: genmock ## run tests go test ./... -v .PHONY: lint @@ -74,3 +68,8 @@ lint: install-tools ## run linters .PHONY: fix fix: install-tools ## fix issues found by linters golangci-lint run --fix ./... + +##@ Utils + +apply-crds: ## applies CRDs + kubectl apply -f config/crd/bases \ No newline at end of file From db6d1610d033759687d60bfe803ad49be9ce43f0 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:01:42 +0100 Subject: [PATCH 098/198] add workflow --- .github/workflows/controller.yaml | 62 +++++++++++++++++++++++++++++++ controller/.golangci.yml | 24 ++++++++++++ controller/Makefile | 1 - 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/controller.yaml create mode 100644 controller/.golangci.yml diff --git a/.github/workflows/controller.yaml b/.github/workflows/controller.yaml new file mode 100644 index 000000000..f6c312a0c --- /dev/null +++ b/.github/workflows/controller.yaml @@ -0,0 +1,62 @@ +name: CI / Controller +on: + push: + branches: + - "master" + paths: + - ".github/workflows/controller.yaml" + - "controller/**" + pull_request: + branches: + - "**" + paths: + - ".github/workflows/controller.yaml" + - "controller/**" +permissions: + contents: read +env: + GOPATH: /home/runner/go/ + GOPROXY: "https://proxy.golang.org" +jobs: + build: + name: Build + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: controller + timeout-minutes: 5 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - run: go build -v . + unit-test: + name: Unit tests + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: controller + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: go.mod + check-latest: true + - run: PATH=$PATH:$GOPATH/bin make test + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: controller/go.mod + check-latest: true + - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 + with: + version: v1.55.2 \ No newline at end of file diff --git a/controller/.golangci.yml b/controller/.golangci.yml new file mode 100644 index 000000000..49df66d6d --- /dev/null +++ b/controller/.golangci.yml @@ -0,0 +1,24 @@ +issues: + max-per-linter: 0 + max-same-issues: 0 +linters: + disable-all: true + enable: + - durationcheck + - errcheck + - exportloopref + - forcetypeassert + - godot + - gofmt + - gosimple + - ineffassign + - makezero + - misspell + - nilerr + - predeclared + - staticcheck + - tenv + - unconvert + - unparam + - unused + - vet diff --git a/controller/Makefile b/controller/Makefile index be9f6a9aa..c17a1f236 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -39,7 +39,6 @@ docker-build: ## build Docker image with the driver docker-push: ## push Docker image with the driver docker push ${IMG} - ##@ Codegen .PHONY: manifests From 8f88cf8e76028a20087f037ac7df366f8321e9d8 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:02:57 +0100 Subject: [PATCH 099/198] update golangci-lint version --- .github/workflows/controller.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/controller.yaml b/.github/workflows/controller.yaml index f6c312a0c..a5e013575 100644 --- a/.github/workflows/controller.yaml +++ b/.github/workflows/controller.yaml @@ -59,4 +59,4 @@ jobs: check-latest: true - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 with: - version: v1.55.2 \ No newline at end of file + version: v1.55.1 \ No newline at end of file From 59152bb66adddbbcb786df903849de0ce0f2d765 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:04:07 +0100 Subject: [PATCH 100/198] update workflow --- .github/workflows/controller.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/controller.yaml b/.github/workflows/controller.yaml index a5e013575..78b96a886 100644 --- a/.github/workflows/controller.yaml +++ b/.github/workflows/controller.yaml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version-file: 'go.mod' + go-version-file: controller/go.mod cache: true - run: go mod download - run: go build -v . @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version-file: go.mod + go-version-file: controller/go.mod check-latest: true - run: PATH=$PATH:$GOPATH/bin make test lint: From 9a70580f471183873f4e1890fb33dfcf37f1d5b9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:06:50 +0100 Subject: [PATCH 101/198] update lint working dir --- .github/workflows/controller.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/controller.yaml b/.github/workflows/controller.yaml index 78b96a886..badfc6957 100644 --- a/.github/workflows/controller.yaml +++ b/.github/workflows/controller.yaml @@ -59,4 +59,5 @@ jobs: check-latest: true - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 with: - version: v1.55.1 \ No newline at end of file + version: v1.55.1 + working-directory: controller From 31c619766cfe96f4c1a9b2ebb14944011c792504 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:12:45 +0100 Subject: [PATCH 102/198] update mocks --- controller/pkg/test/mocks/ConsoleClient.go | 171 +++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/controller/pkg/test/mocks/ConsoleClient.go b/controller/pkg/test/mocks/ConsoleClient.go index c65fa2131..f673a0faf 100644 --- a/controller/pkg/test/mocks/ConsoleClient.go +++ b/controller/pkg/test/mocks/ConsoleClient.go @@ -9,6 +9,8 @@ import ( gqlclient "github.com/pluralsh/console-client-go" mock "github.com/stretchr/testify/mock" + + v1alpha1 "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" ) // ConsoleClient is an autogenerated mock type for the ConsoleClient type @@ -216,6 +218,24 @@ func (_m *ConsoleClient) DeleteRepository(id string) error { return r0 } +// DeleteService provides a mock function with given fields: serviceId +func (_m *ConsoleClient) DeleteService(serviceId string) error { + ret := _m.Called(serviceId) + + if len(ret) == 0 { + panic("no return value specified for DeleteService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(serviceId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetCluster provides a mock function with given fields: id func (_m *ConsoleClient) GetCluster(id *string) (*gqlclient.ClusterFragment, error) { ret := _m.Called(id) @@ -246,6 +266,36 @@ func (_m *ConsoleClient) GetCluster(id *string) (*gqlclient.ClusterFragment, err return r0, r1 } +// GetClusterByHandle provides a mock function with given fields: handle +func (_m *ConsoleClient) GetClusterByHandle(handle *string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(handle) + + if len(ret) == 0 { + panic("no return value specified for GetClusterByHandle") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string) (*gqlclient.ClusterFragment, error)); ok { + return rf(handle) + } + if rf, ok := ret.Get(0).(func(*string) *gqlclient.ClusterFragment); ok { + r0 = rf(handle) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string) error); ok { + r1 = rf(handle) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetProvider provides a mock function with given fields: ctx, id, options func (_m *ConsoleClient) GetProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { _va := make([]interface{}, len(options)) @@ -283,6 +333,43 @@ func (_m *ConsoleClient) GetProvider(ctx context.Context, id string, options ... return r0, r1 } +// GetProviderByCloud provides a mock function with given fields: ctx, cloud, options +func (_m *ConsoleClient) GetProviderByCloud(ctx context.Context, cloud v1alpha1.CloudProvider, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, cloud) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetProviderByCloud") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, cloud, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, cloud, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, cloud, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRepository provides a mock function with given fields: url func (_m *ConsoleClient) GetRepository(url *string) (*gqlclient.GetGitRepository, error) { ret := _m.Called(url) @@ -373,6 +460,60 @@ func (_m *ConsoleClient) GetServices() ([]*gqlclient.ServiceDeploymentBaseFragme return r0, r1 } +// IsClusterDeleting provides a mock function with given fields: id +func (_m *ConsoleClient) IsClusterDeleting(id *string) bool { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for IsClusterDeleting") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(*string) bool); ok { + r0 = rf(id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsClusterExisting provides a mock function with given fields: id +func (_m *ConsoleClient) IsClusterExisting(id *string) bool { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for IsClusterExisting") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(*string) bool); ok { + r0 = rf(id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsProviderDeleting provides a mock function with given fields: ctx, id +func (_m *ConsoleClient) IsProviderDeleting(ctx context.Context, id string) bool { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for IsProviderDeleting") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // IsProviderExists provides a mock function with given fields: ctx, id func (_m *ConsoleClient) IsProviderExists(ctx context.Context, id string) bool { ret := _m.Called(ctx, id) @@ -451,6 +592,36 @@ func (_m *ConsoleClient) ListRepositories() (*gqlclient.ListGitRepositories, err return r0, r1 } +// UpdateCluster provides a mock function with given fields: id, attrs +func (_m *ConsoleClient) UpdateCluster(id string, attrs gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(id, attrs) + + if len(ret) == 0 { + panic("no return value specified for UpdateCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error)); ok { + return rf(id, attrs) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) *gqlclient.ClusterFragment); ok { + r0 = rf(id, attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.ClusterUpdateAttributes) error); ok { + r1 = rf(id, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateComponents provides a mock function with given fields: id, components, errs func (_m *ConsoleClient) UpdateComponents(id string, components []*gqlclient.ComponentAttributes, errs []*gqlclient.ServiceErrorAttributes) error { ret := _m.Called(id, components, errs) From b9ac11d4a244c39e1390756387959a599bf2ec93 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:15:20 +0100 Subject: [PATCH 103/198] fix mockgen --- controller/hack/gen-client-mocks.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/hack/gen-client-mocks.sh b/controller/hack/gen-client-mocks.sh index 7c979b2a4..76965cc77 100755 --- a/controller/hack/gen-client-mocks.sh +++ b/controller/hack/gen-client-mocks.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -set -euo pipefail +# Exit on error +set -e cd $(dirname $0)/.. From b2357744bddf9e30273b9e9e161730d1777823b2 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 13 Dec 2023 13:17:09 +0100 Subject: [PATCH 104/198] properly wait for cluster deletion --- .../config/crd/examples/all_in_one_gcp.yaml | 52 +++++++++---------- .../pkg/cluster_controller/reconciler.go | 21 +++++--- .../pkg/provider_reconciler/reconciler.go | 7 ++- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/controller/config/crd/examples/all_in_one_gcp.yaml b/controller/config/crd/examples/all_in_one_gcp.yaml index 51c84f374..5f926002b 100644 --- a/controller/config/crd/examples/all_in_one_gcp.yaml +++ b/controller/config/crd/examples/all_in_one_gcp.yaml @@ -36,29 +36,29 @@ spec: project: pluralsh-test-384515 tags: managed-by: plural-operator ---- -apiVersion: deployments.plural.sh/v1alpha1 -kind: GitRepository -metadata: - name: k8shelm -spec: - url: https://github.com/zreigz/k8s-helm.git ---- -apiVersion: deployments.plural.sh/v1alpha1 -kind: ServiceDeployment -metadata: - name: k8saws - namespace: default -spec: - version: 0.0.1 - git: - folder: nginx - ref: master - repositoryRef: - kind: GitRepository - name: k8shelm - namespace: default - clusterRef: - kind: Cluster - name: aws - namespace: default +#--- +#apiVersion: deployments.plural.sh/v1alpha1 +#kind: GitRepository +#metadata: +# name: k8s-helm +#spec: +# url: https://github.com/zreigz/k8s-helm.git +#--- +#apiVersion: deployments.plural.sh/v1alpha1 +#kind: ServiceDeployment +#metadata: +# name: zreigz-test +# namespace: operator +#spec: +# version: 0.0.1 +# git: +# folder: nginx +# ref: master +# repositoryRef: +# kind: GitRepository +# name: k8s-helm +# namespace: operator +# clusterRef: +# kind: Cluster +# name: gcp +# namespace: operator diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index efe06782b..0ae66dbb0 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -7,10 +7,6 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/utils" "github.com/samber/lo" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -19,6 +15,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/errors" + "github.com/pluralsh/console/controller/pkg/utils" ) const ( @@ -71,8 +72,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return *result, err } - // Get Provider ID from the reference if it is set and ensure that owner reference is set properly. - providerId, result, err := r.getProviderIdAndSetOwnerRef(ctx, cluster) + // Get Provider ID from the reference if it is set and ensure that controller reference is set properly. + providerId, result, err := r.getProviderIdAndSetControllerRef(ctx, cluster) if result != nil { return *result, err } @@ -171,6 +172,10 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 // so that it can be retried. return &ctrl.Result{}, err } + + // If deletion process started requeue so that we can make sure provider + // has been deleted from Console API before removing the finalizer. + return &requeue, nil } // If our finalizer is present, remove it. @@ -188,7 +193,7 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 return nil, nil } -func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { +func (r *Reconciler) getProviderIdAndSetControllerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { logger := log.FromContext(ctx) if cluster.Spec.IsProviderRefRequired() { @@ -206,7 +211,7 @@ func (r *Reconciler) getProviderIdAndSetOwnerRef(ctx context.Context, cluster *v return nil, &requeue, nil } - err = utils.TryAddOwnerRef(ctx, r.Client, provider, cluster, r.Scheme) + err = utils.TryAddControllerRef(ctx, r.Client, provider, cluster, r.Scheme) if err != nil { return nil, &ctrl.Result{}, fmt.Errorf("could not set cluster owner reference, got error: %+v", err) } diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index f597e7ef3..a6e092dfa 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -37,7 +37,8 @@ const ( RequeueAfter = 30 * time.Second // FinalizerName defines name for the main finalizer that synchronizes // resource deletion from the Console API prior to removing the CRD. - FinalizerName = "providers.deployments.plural.sh/finalizer" + FinalizerName = "providers.deployments.plural.sh/finalizer" + ForegroundDeletionFinalizerName = "foregroundDeletion" ) var ( @@ -56,6 +57,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, client.IgnoreNotFound(err) } + if err := kubernetes.TryAddFinalizer(ctx, r.Client, &provider, ForegroundDeletionFinalizerName); err != nil { + return ctrl.Result{}, err + } + // Check if resource already exists in the API and only sync the ID exists, err := r.isAlreadyExists(ctx, provider) if err != nil { From c14bff22684c61ffffdc375d62bdfcbffffb0c50 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:40:13 +0100 Subject: [PATCH 105/198] move kubernetes utils --- .../gitrepository_controller/controller.go | 5 +- controller/pkg/kubernetes/helper.go | 152 ------------------ .../pkg/provider_reconciler/reconciler.go | 5 +- .../reconciler_attributes.go | 8 +- .../pkg/service_controller/controller.go | 13 +- controller/pkg/utils/kubernetes.go | 119 ++++++++++++++ 6 files changed, 133 insertions(+), 169 deletions(-) delete mode 100644 controller/pkg/kubernetes/helper.go diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 9aae076bb..9b39b79ad 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -13,7 +13,6 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/kubernetes" "github.com/pluralsh/console/controller/pkg/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -88,7 +87,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if existingRepo == nil { - if err := kubernetes.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { + if err := utils.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { return ctrl.Result{}, err } resp, err := r.ConsoleClient.CreateRepository(repo.Spec.Url, cred.PrivateKey, cred.Passphrase, cred.Username, cred.Password) @@ -163,7 +162,7 @@ func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitReposit } } } - if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { + if err := utils.TryRemoveFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { return ctrl.Result{}, err } } diff --git a/controller/pkg/kubernetes/helper.go b/controller/pkg/kubernetes/helper.go deleted file mode 100644 index 78f6c9c09..000000000 --- a/controller/pkg/kubernetes/helper.go +++ /dev/null @@ -1,152 +0,0 @@ -package kubernetes - -import ( - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/util/retry" - "sigs.k8s.io/controller-runtime/pkg/client" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -// RemoveFinalizer removes the given finalizers from the object. -func RemoveFinalizer(obj metav1.Object, toRemove ...string) { - set := sets.NewString(obj.GetFinalizers()...) - set.Delete(toRemove...) - obj.SetFinalizers(set.List()) -} - -func TryRemoveFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj ctrlruntimeclient.Object, finalizers ...string) error { - key := ctrlruntimeclient.ObjectKeyFromObject(obj) - - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - // fetch the current state of the object - if err := client.Get(ctx, key, obj); err != nil { - // finalizer removal normally happens during object cleanup, so if - // the object is gone already, that is absolutely fine - if apierrors.IsNotFound(err) { - return nil - } - return err - } - - original := obj.DeepCopyObject().(ctrlruntimeclient.Object) - - // modify it - previous := sets.NewString(obj.GetFinalizers()...) - RemoveFinalizer(obj, finalizers...) - current := sets.NewString(obj.GetFinalizers()...) - - // save some work - if previous.Equal(current) { - return nil - } - - // update the object - return client.Patch(ctx, obj, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) - }) - - if err != nil { - kind := obj.GetObjectKind().GroupVersionKind().Kind - return fmt.Errorf("failed to remove finalizers %v from %s %s: %w", finalizers, kind, key, err) - } - - return nil -} - -// AddFinalizer will add the given finalizer to the object. It uses a StringSet to avoid duplicates. -func AddFinalizer(obj metav1.Object, finalizers ...string) { - set := sets.NewString(obj.GetFinalizers()...) - set.Insert(finalizers...) - obj.SetFinalizers(set.List()) -} - -func TryAddFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj ctrlruntimeclient.Object, finalizers ...string) error { - key := ctrlruntimeclient.ObjectKeyFromObject(obj) - - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - // fetch the current state of the object - if err := client.Get(ctx, key, obj); err != nil { - return err - } - - // cannot add new finalizers to deleted objects - if obj.GetDeletionTimestamp() != nil { - return nil - } - - original := obj.DeepCopyObject().(ctrlruntimeclient.Object) - - // modify it - previous := sets.NewString(obj.GetFinalizers()...) - AddFinalizer(obj, finalizers...) - current := sets.NewString(obj.GetFinalizers()...) - - // save some work - if previous.Equal(current) { - return nil - } - - // update the object - return client.Patch(ctx, obj, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) - }) - - if err != nil { - kind := obj.GetObjectKind().GroupVersionKind().Kind - return fmt.Errorf("failed to add finalizers %v to %s %s: %w", finalizers, kind, key, err) - } - - return nil -} - -func DeleteSecret(ctx context.Context, client client.Client, secretNamespace, secretName string) error { - if secretName == "" { - return nil - } - - secret := &corev1.Secret{} - name := types.NamespacedName{Name: secretName, Namespace: secretNamespace} - err := client.Get(ctx, name, secret) - if apierrors.IsNotFound(err) { - return nil - } - if err != nil { - return fmt.Errorf("failed to get Secret %q: %w", name.String(), err) - } - - if err := client.Delete(ctx, secret); err != nil { - return fmt.Errorf("failed to delete Secret %q: %w", name.String(), err) - } - - // We successfully deleted the secret - return nil -} - -func GetSecret(ctx context.Context, client client.Client, ref *corev1.SecretReference) (*corev1.Secret, error) { - secret := &corev1.Secret{} - name := types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace} - err := client.Get(ctx, name, secret) - if err != nil { - return nil, err - } - return secret, err -} - -func GetConfigMapData(ctx context.Context, client client.Client, namespace string, ref *corev1.ConfigMapKeySelector) (string, error) { - configMap := &corev1.ConfigMap{} - name := types.NamespacedName{Name: ref.Name, Namespace: namespace} - err := client.Get(ctx, name, configMap) - if err != nil { - return "", err - } - if configMap.Data != nil { - return configMap.Data[ref.Key], nil - } - - return "", nil -} diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index a6e092dfa..c2310383d 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -18,7 +18,6 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/kubernetes" "github.com/pluralsh/console/controller/pkg/utils" ) @@ -57,7 +56,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, client.IgnoreNotFound(err) } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, &provider, ForegroundDeletionFinalizerName); err != nil { + if err := utils.TryAddFinalizer(ctx, r.Client, &provider, ForegroundDeletionFinalizerName); err != nil { return ctrl.Result{}, err } @@ -235,7 +234,7 @@ func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1. return fmt.Errorf("could not find secret ref configuration for cloud %q", provider.Spec.Cloud) } - secret, err := kubernetes.GetSecret(ctx, r.Client, secretRef) + secret, err := utils.GetSecret(ctx, r.Client, secretRef) if err != nil { return err } diff --git a/controller/pkg/provider_reconciler/reconciler_attributes.go b/controller/pkg/provider_reconciler/reconciler_attributes.go index f186abb55..da728ae37 100644 --- a/controller/pkg/provider_reconciler/reconciler_attributes.go +++ b/controller/pkg/provider_reconciler/reconciler_attributes.go @@ -5,10 +5,10 @@ import ( "fmt" console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/pkg/utils" corev1 "k8s.io/api/core/v1" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - "github.com/pluralsh/console/controller/pkg/kubernetes" ) func (r *Reconciler) missingCredentialKeyError(key string) error { @@ -45,7 +45,7 @@ func (r *Reconciler) toCloudProviderAWSSettingsAttributes(ctx context.Context, r const accessKeyIDKeyName = "accessKeyId" const secretAccessKeyName = "secretAccessKey" - secret, err := kubernetes.GetSecret(ctx, r.Client, ref) + secret, err := utils.GetSecret(ctx, r.Client, ref) if err != nil { return nil, err } @@ -74,7 +74,7 @@ func (r *Reconciler) toCloudProviderAzureSettingsAttributes(ctx context.Context, const clientIDKeyName = "clientId" const clientSecretKeyName = "clientSecret" - secret, err := kubernetes.GetSecret(ctx, r.Client, ref) + secret, err := utils.GetSecret(ctx, r.Client, ref) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func (r *Reconciler) toCloudProviderAzureSettingsAttributes(ctx context.Context, func (r *Reconciler) toCloudProviderGCPSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { const applicationCredentialsKeyName = "applicationCredentials" - secret, err := kubernetes.GetSecret(ctx, r.Client, ref) + secret, err := utils.GetSecret(ctx, r.Client, ref) if err != nil { return nil, err } diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 4353f21ea..0f3d8a1b2 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -11,7 +11,6 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/kubernetes" "github.com/pluralsh/console/controller/pkg/utils" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" @@ -82,7 +81,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } if existingService == nil { - if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + if err := utils.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { return ctrl.Result{}, err } _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) @@ -93,7 +92,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { return ctrl.Result{}, err } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + if err := utils.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { return ctrl.Result{}, err } } @@ -241,14 +240,14 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 } } if service.Spec.Helm.ValuesRef != nil { - val, err := kubernetes.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ValuesRef) + val, err := utils.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ValuesRef) if err != nil { return nil, err } attr.Helm.Values = &val } if service.Spec.Helm.ChartRef != nil { - val, err := kubernetes.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ChartRef) + val, err := utils.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ChartRef) if err != nil { return nil, err } @@ -287,7 +286,7 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.ServiceDeployment) error { if service.Spec.ConfigurationRef != nil { - configurationSecret, err := kubernetes.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) + configurationSecret, err := utils.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) if err != nil { return err } @@ -349,7 +348,7 @@ func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster } return requeue, nil } - if err := kubernetes.TryRemoveFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { + if err := utils.TryRemoveFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { return ctrl.Result{}, err } } diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index 6ace48ee6..146e3746a 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -5,7 +5,12 @@ import ( "fmt" "reflect" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/util/retry" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -80,3 +85,117 @@ func TryUpdateStatus[PatchObject ctrlruntimeclient.Object](ctx context.Context, return client.Status().Patch(ctx, object, ctrlruntimeclient.MergeFrom(original)) }) } + +// RemoveFinalizer removes the given finalizers from the object. +func RemoveFinalizer(obj metav1.Object, toRemove ...string) { + set := sets.NewString(obj.GetFinalizers()...) + set.Delete(toRemove...) + obj.SetFinalizers(set.List()) +} + +func TryRemoveFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj ctrlruntimeclient.Object, finalizers ...string) error { + key := ctrlruntimeclient.ObjectKeyFromObject(obj) + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // fetch the current state of the object + if err := client.Get(ctx, key, obj); err != nil { + // finalizer removal normally happens during object cleanup, so if + // the object is gone already, that is absolutely fine + if apierrors.IsNotFound(err) { + return nil + } + return err + } + + original := obj.DeepCopyObject().(ctrlruntimeclient.Object) + + // modify it + previous := sets.NewString(obj.GetFinalizers()...) + RemoveFinalizer(obj, finalizers...) + current := sets.NewString(obj.GetFinalizers()...) + + // save some work + if previous.Equal(current) { + return nil + } + + // update the object + return client.Patch(ctx, obj, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) + }) + + if err != nil { + kind := obj.GetObjectKind().GroupVersionKind().Kind + return fmt.Errorf("failed to remove finalizers %v from %s %s: %w", finalizers, kind, key, err) + } + + return nil +} + +// AddFinalizer will add the given finalizer to the object. It uses a StringSet to avoid duplicates. +func AddFinalizer(obj metav1.Object, finalizers ...string) { + set := sets.NewString(obj.GetFinalizers()...) + set.Insert(finalizers...) + obj.SetFinalizers(set.List()) +} + +func TryAddFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj ctrlruntimeclient.Object, finalizers ...string) error { + key := ctrlruntimeclient.ObjectKeyFromObject(obj) + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // fetch the current state of the object + if err := client.Get(ctx, key, obj); err != nil { + return err + } + + // cannot add new finalizers to deleted objects + if obj.GetDeletionTimestamp() != nil { + return nil + } + + original := obj.DeepCopyObject().(ctrlruntimeclient.Object) + + // modify it + previous := sets.NewString(obj.GetFinalizers()...) + AddFinalizer(obj, finalizers...) + current := sets.NewString(obj.GetFinalizers()...) + + // save some work + if previous.Equal(current) { + return nil + } + + // update the object + return client.Patch(ctx, obj, ctrlruntimeclient.MergeFromWithOptions(original, ctrlruntimeclient.MergeFromWithOptimisticLock{})) + }) + + if err != nil { + kind := obj.GetObjectKind().GroupVersionKind().Kind + return fmt.Errorf("failed to add finalizers %v to %s %s: %w", finalizers, kind, key, err) + } + + return nil +} + +func GetSecret(ctx context.Context, client ctrlruntimeclient.Client, ref *corev1.SecretReference) (*corev1.Secret, error) { + secret := &corev1.Secret{} + name := types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace} + err := client.Get(ctx, name, secret) + if err != nil { + return nil, err + } + return secret, err +} + +func GetConfigMapData(ctx context.Context, client ctrlruntimeclient.Client, namespace string, ref *corev1.ConfigMapKeySelector) (string, error) { + configMap := &corev1.ConfigMap{} + name := types.NamespacedName{Name: ref.Name, Namespace: namespace} + err := client.Get(ctx, name, configMap) + if err != nil { + return "", err + } + if configMap.Data != nil { + return configMap.Data[ref.Key], nil + } + + return "", nil +} From 7273c2994518fbf6fc9ad7fb41c54c4a3dd91c54 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:44:45 +0100 Subject: [PATCH 106/198] refactor kubernetes utils --- controller/pkg/utils/kubernetes.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index 146e3746a..911a64836 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -17,8 +17,10 @@ import ( ) func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, object ctrlruntimeclient.Object, scheme *runtime.Scheme) error { + key := ctrlruntimeclient.ObjectKeyFromObject(object) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(object), object); err != nil { + if err := client.Get(ctx, key, object); err != nil { return err } @@ -42,8 +44,10 @@ func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner } func TryAddControllerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, controlled ctrlruntimeclient.Object, scheme *runtime.Scheme) error { + key := ctrlruntimeclient.ObjectKeyFromObject(controlled) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(controlled), controlled); err != nil { + if err := client.Get(ctx, key, controlled); err != nil { return err } @@ -71,8 +75,10 @@ type Patcher[PatchObject ctrlruntimeclient.Object] func(object PatchObject, orig // TryUpdateStatus TODO ... func TryUpdateStatus[PatchObject ctrlruntimeclient.Object](ctx context.Context, client ctrlruntimeclient.Client, object PatchObject, patch Patcher[PatchObject]) error { + key := ctrlruntimeclient.ObjectKeyFromObject(object) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(object), object); err != nil { + if err := client.Get(ctx, key, object); err != nil { return fmt.Errorf("could not fetch current %s/%s state, got error: %+v", object.GetName(), object.GetNamespace(), err) } @@ -99,8 +105,7 @@ func TryRemoveFinalizer(ctx context.Context, client ctrlruntimeclient.Client, ob err := retry.RetryOnConflict(retry.DefaultRetry, func() error { // fetch the current state of the object if err := client.Get(ctx, key, obj); err != nil { - // finalizer removal normally happens during object cleanup, so if - // the object is gone already, that is absolutely fine + // finalizer removal normally happens during object cleanup, so if the object is gone already, that is absolutely fine if apierrors.IsNotFound(err) { return nil } @@ -178,21 +183,19 @@ func TryAddFinalizer(ctx context.Context, client ctrlruntimeclient.Client, obj c func GetSecret(ctx context.Context, client ctrlruntimeclient.Client, ref *corev1.SecretReference) (*corev1.Secret, error) { secret := &corev1.Secret{} - name := types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace} - err := client.Get(ctx, name, secret) - if err != nil { + if err := client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace}, secret); err != nil { return nil, err } - return secret, err + + return secret, nil } func GetConfigMapData(ctx context.Context, client ctrlruntimeclient.Client, namespace string, ref *corev1.ConfigMapKeySelector) (string, error) { configMap := &corev1.ConfigMap{} - name := types.NamespacedName{Name: ref.Name, Namespace: namespace} - err := client.Get(ctx, name, configMap) - if err != nil { + if err := client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: namespace}, configMap); err != nil { return "", err } + if configMap.Data != nil { return configMap.Data[ref.Key], nil } From 8726281df41dce5afd7b0722cd7821baf430a18f Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:53:04 +0100 Subject: [PATCH 107/198] handle cluster cascading deletion --- controller/pkg/cluster_controller/reconciler.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 0ae66dbb0..89c5ea27c 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -206,6 +206,17 @@ func (r *Reconciler) getProviderIdAndSetControllerRef(ctx context.Context, clust return nil, &ctrl.Result{}, fmt.Errorf("could not get provider, got error: %+v", err) } + // Once provider is marked with deletion timestamp we should delete cluster as well. + // Provider cannot be deleted until cluster exists so that ensures cascading deletion. + if provider.DeletionTimestamp != nil { + err := r.Delete(ctx, cluster) + if err != nil { + return nil, &ctrl.Result{}, err + } + + return nil, &requeue, nil + } + if !provider.Status.HasID() { logger.Info("Provider does not have ID set yet") return nil, &requeue, nil From 7d8bef198dc7a0e30015b06c13e3e035db0b2bc9 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 13 Dec 2023 13:53:57 +0100 Subject: [PATCH 108/198] fix create provider nil pointer --- .../config/crd/examples/all_in_one_gcp.yaml | 2 +- controller/pkg/client/provider.go | 4 ++++ .../pkg/provider_reconciler/reconciler.go | 17 ++++++----------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/controller/config/crd/examples/all_in_one_gcp.yaml b/controller/config/crd/examples/all_in_one_gcp.yaml index 5f926002b..31ee282d9 100644 --- a/controller/config/crd/examples/all_in_one_gcp.yaml +++ b/controller/config/crd/examples/all_in_one_gcp.yaml @@ -25,7 +25,7 @@ metadata: spec: handle: gcp cloud: gcp - version: "1.25.14" + version: "1.25.13" protect: false providerRef: name: gcp diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go index bf1d89621..036db9628 100644 --- a/controller/pkg/client/provider.go +++ b/controller/pkg/client/provider.go @@ -13,6 +13,10 @@ import ( func (c *client) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { response, err := c.consoleClient.CreateClusterProvider(ctx, attributes, options...) + if err != nil { + return nil, err + } + return response.CreateClusterProvider, err } diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/provider_reconciler/reconciler.go index a6e092dfa..05ba24aa0 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/provider_reconciler/reconciler.go @@ -35,10 +35,9 @@ const ( // RequeueAfter is the time between scheduled reconciles if there are no // changes to the CRD. RequeueAfter = 30 * time.Second - // FinalizerName defines name for the main finalizer that synchronizes + // ProviderProtectionFinalizerName defines name for the main finalizer that synchronizes // resource deletion from the Console API prior to removing the CRD. - FinalizerName = "providers.deployments.plural.sh/finalizer" - ForegroundDeletionFinalizerName = "foregroundDeletion" + ProviderProtectionFinalizerName = "providers.deployments.plural.sh/provider-protection" ) var ( @@ -57,10 +56,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, client.IgnoreNotFound(err) } - if err := kubernetes.TryAddFinalizer(ctx, r.Client, &provider, ForegroundDeletionFinalizerName); err != nil { - return ctrl.Result{}, err - } - // Check if resource already exists in the API and only sync the ID exists, err := r.isAlreadyExists(ctx, provider) if err != nil { @@ -155,8 +150,8 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1 // If 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 // to registering our finalizer. - if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&provider, FinalizerName) { - controllerutil.AddFinalizer(&provider, FinalizerName) + if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&provider, ProviderProtectionFinalizerName) { + controllerutil.AddFinalizer(&provider, ProviderProtectionFinalizerName) if err := r.Update(ctx, &provider); err != nil { return &ctrl.Result{}, err } @@ -185,8 +180,8 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1 } // If our finalizer is present, remove it - if controllerutil.ContainsFinalizer(&provider, FinalizerName) { - controllerutil.RemoveFinalizer(&provider, FinalizerName) + if controllerutil.ContainsFinalizer(&provider, ProviderProtectionFinalizerName) { + controllerutil.RemoveFinalizer(&provider, ProviderProtectionFinalizerName) if err := r.Update(ctx, &provider); err != nil { return &ctrl.Result{}, err } From 7768fa4752c71a1d33700279b5c91e8f8a93a4c2 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:54:26 +0100 Subject: [PATCH 109/198] update check --- controller/pkg/cluster_controller/reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 89c5ea27c..99837d0d2 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -208,7 +208,7 @@ func (r *Reconciler) getProviderIdAndSetControllerRef(ctx context.Context, clust // Once provider is marked with deletion timestamp we should delete cluster as well. // Provider cannot be deleted until cluster exists so that ensures cascading deletion. - if provider.DeletionTimestamp != nil { + if !provider.DeletionTimestamp.IsZero() { err := r.Delete(ctx, cluster) if err != nil { return nil, &ctrl.Result{}, err From 2c231707ff57f7484c8807feb237d815e5931fe2 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 13 Dec 2023 13:59:16 +0100 Subject: [PATCH 110/198] add logs --- controller/pkg/cluster_controller/reconciler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/cluster_controller/reconciler.go index 99837d0d2..eba7159b3 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/cluster_controller/reconciler.go @@ -209,9 +209,10 @@ func (r *Reconciler) getProviderIdAndSetControllerRef(ctx context.Context, clust // Once provider is marked with deletion timestamp we should delete cluster as well. // Provider cannot be deleted until cluster exists so that ensures cascading deletion. if !provider.DeletionTimestamp.IsZero() { + logger.Info(fmt.Sprintf("Provider is being deleted, deleting %s cluster as well", cluster.Name)) err := r.Delete(ctx, cluster) if err != nil { - return nil, &ctrl.Result{}, err + return nil, &ctrl.Result{}, fmt.Errorf("could not delete %s cluster, got error: %+v", cluster.Name, err) } return nil, &requeue, nil From 392837fdda1df1277807b1b25754f18b335d1c60 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 13 Dec 2023 14:21:21 +0100 Subject: [PATCH 111/198] fix makefile, add initial gorelease config --- controller/.goreleaser.yml | 60 +++++++++++++++++++ controller/Makefile | 3 +- .../bases/deployments.plural.sh_clusters.yaml | 9 +-- ...deployments.plural.sh_gitrepositories.yaml | 2 +- .../deployments.plural.sh_providers.yaml | 5 +- ...loyments.plural.sh_servicedeployments.yaml | 2 +- controller/main.go | 23 +++++-- 7 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 controller/.goreleaser.yml diff --git a/controller/.goreleaser.yml b/controller/.goreleaser.yml new file mode 100644 index 000000000..23f50b302 --- /dev/null +++ b/controller/.goreleaser.yml @@ -0,0 +1,60 @@ +# Visit https://goreleaser.com for documentation on how to customize this +# behavior. +before: + hooks: + # this is just an example and not a requirement for provider building/publishing + - go mod tidy +builds: + - env: + # goreleaser does not work with CGO, it could also complicate + # usage by users in CI/CD systems like Terraform Cloud where + # they are unable to install libraries. + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm + - arm64 + ignore: + - goos: darwin + goarch: '386' + binary: '{{ .ProjectName }}_v{{ .Version }}' +archives: + - format: zip + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' +checksum: + extra_files: + - glob: 'terraform-registry-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' + algorithm: sha256 +signs: + - artifacts: checksum + args: + # if you are using this in a GitHub action or some other automated pipeline, you + # need to pass the batch flag to indicate its not interactive. + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" +release: + extra_files: + - glob: 'terraform-registry-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + # If you want to manually examine the release before its live, uncomment this line: + # draft: true +changelog: + skip: true diff --git a/controller/Makefile b/controller/Makefile index c17a1f236..e0f73fb0f 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -1,4 +1,5 @@ IMG ?= deployment-controller:latest # Image URL to use all building/pushing image targets +CONTROLLER_GEN = $(shell which controller-gen) ifndef GOPATH $(error $$GOPATH environment variable not set) @@ -71,4 +72,4 @@ fix: install-tools ## fix issues found by linters ##@ Utils apply-crds: ## applies CRDs - kubectl apply -f config/crd/bases \ No newline at end of file + kubectl apply -f config/crd/bases diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 421b2116d..aef7b9d7d 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.11.1 creationTimestamp: null name: clusters.deployments.plural.sh spec: @@ -80,7 +80,6 @@ spec: - azure - gcp - byok - example: azure type: string x-kubernetes-validations: - message: Cloud is immutable @@ -103,17 +102,14 @@ spec: properties: location: description: Location in Azure to deploy this cluster to. - example: eastus type: string network: description: Network is a name for the Azure virtual network for this cluster. - example: mynetwork type: string resourceGroup: description: ResourceGroup is a name for the Azure resource group for this cluster. - example: myresourcegroup type: string subscriptionId: description: SubscriptionId is GUID of the Azure subscription @@ -148,7 +144,6 @@ spec: description: Handle is a short, unique human-readable name used to identify this cluster. Does not necessarily map to the cloud resource name. This has to be specified in order to adopt existing cluster. - example: myclusterhandle type: string nodePools: description: NodePools contains specs of node pools managed by this @@ -228,7 +223,6 @@ spec: type: array protect: description: Protect cluster from being deleted. - example: false type: boolean providerRef: description: ProviderRef references provider to use for this cluster. @@ -276,7 +270,6 @@ spec: version: description: Version of Kubernetes to use for this cluster. Can be skipped only for BYOK. - example: 1.25.11 type: string type: object status: diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index ad2349035..0e7a8d229 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.11.1 creationTimestamp: null name: gitrepositories.deployments.plural.sh spec: diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index 8ed657bca..671973d5b 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.11.1 creationTimestamp: null name: providers.deployments.plural.sh spec: @@ -55,7 +55,6 @@ spec: - gcp - aws - azure - example: aws type: string x-kubernetes-validations: - message: Cloud is immutable @@ -111,7 +110,6 @@ spec: x-kubernetes-map-type: atomic name: description: Name is a human-readable name of the Provider. - example: gcp-provider type: string x-kubernetes-validations: - message: Name is immutable @@ -119,7 +117,6 @@ spec: namespace: description: Namespace is the namespace ClusterAPI resources are deployed into. - example: capi-gcp type: string x-kubernetes-validations: - message: Namespace is immutable diff --git a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index bfaebda4b..c82c47ad6 100644 --- a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.11.1 creationTimestamp: null name: servicedeployments.deployments.plural.sh spec: diff --git a/controller/main.go b/controller/main.go index 048ec9a47..4ae6e1ef4 100644 --- a/controller/main.go +++ b/controller/main.go @@ -6,9 +6,6 @@ import ( "os" "strings" - deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -16,11 +13,19 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" + + deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/types" ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("Setup") + // version is managed by GoReleaser, see: https://goreleaser.com/cookbooks/using-main.version/ + version = "dev" + // commit is managed by GoReleaser, see: https://goreleaser.com/cookbooks/using-main.version/ + commit = "none" ) func init() { @@ -44,7 +49,7 @@ func main() { reconcilers: types.Reconcilers(), } opts := ctrlruntimezap.Options{ - Development: true, + Development: version == "dev", } opts.BindFlags(flag.CommandLine) flag.StringVar(&opt.metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") @@ -60,6 +65,10 @@ func main() { }) flag.Parse() + if flag.Arg(0) == "version" { + versionInfo() + return + } ctrl.SetLogger(ctrlruntimezap.New(ctrlruntimezap.UseFlagOptions(&opts))) @@ -126,3 +135,9 @@ func runOrDie(controllers []types.Controller, mgr ctrl.Manager) { os.Exit(1) } } + +func versionInfo() { + fmt.Println("PLURAL CONTROLLER:") + fmt.Printf(" version\t%s\n", version) + fmt.Printf(" git commit\t%s\n", commit) +} From 2996a728eed7bb3c4df632e9a6cd9e27a70ea49d Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 13 Dec 2023 14:21:22 +0100 Subject: [PATCH 112/198] delete repo --- .../deployments/v1alpha1/git_repository.go | 1 + ...deployments.plural.sh_gitrepositories.yaml | 1 + controller/pkg/errors/base.go | 19 +++++++++++++++++-- .../gitrepository_controller/controller.go | 4 +++- .../pkg/service_controller/controller.go | 10 +++++++++- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 8217328e7..9151cd828 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -42,6 +42,7 @@ type GitRepositoryStatus struct { // CRD is then set to read-only mode and does not update Console API from CRD. // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=boolean + // +kubebuilder:default:=false Existing *bool `json:"existing,omitempty"` } diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index 0e7a8d229..c6461425a 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -67,6 +67,7 @@ spec: status: properties: existing: + default: false description: Existing flag is set to true when Console API object already exists when CRD is created. CRD is then set to read-only mode and does not update Console API from CRD. diff --git a/controller/pkg/errors/base.go b/controller/pkg/errors/base.go index c96ead6fd..5397241c0 100644 --- a/controller/pkg/errors/base.go +++ b/controller/pkg/errors/base.go @@ -12,8 +12,9 @@ func (k KnownError) String() string { } const ( - ErrorNotFound KnownError = "could not find resource" - ErrExpected KnownError = "this is a transient, expected error" + ErrorNotFound KnownError = "could not find resource" + ErrExpected KnownError = "this is a transient, expected error" + ErrDeleteRepository = "could not delete repository" ) type wrappedErrorResponse struct { @@ -53,3 +54,17 @@ func IsNotFound(err error) bool { return newAPIError(errorResponse).Has(ErrorNotFound) } + +func IsDeleteRepository(err error) bool { + if err == nil { + return false + } + + errorResponse := new(client.ErrorResponse) + ok := errors.As(err, &errorResponse) + if !ok { + return false + } + + return newAPIError(errorResponse).Has(ErrDeleteRepository) +} diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/gitrepository_controller/controller.go index 9b39b79ad..82ad80fcb 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/gitrepository_controller/controller.go @@ -157,9 +157,11 @@ func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitReposit } if existingRepos != nil { if err := r.ConsoleClient.DeleteRepository(*repo.Status.Id); err != nil { - if !errors.IsNotFound(err) { + if !errors.IsDeleteRepository(err) { return ctrl.Result{}, err } + logger.Info("waiting for the services") + return requeue, nil } } if err := utils.TryRemoveFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/service_controller/controller.go index 0f3d8a1b2..7eb1bc6db 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/service_controller/controller.go @@ -62,6 +62,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.RepositoryRef.Name, Namespace: service.Spec.RepositoryRef.Namespace}, repository); err != nil { return ctrl.Result{}, err } + if !repository.DeletionTimestamp.IsZero() { + log.Info("deleting service after repository deletion") + if err := r.Delete(ctx, service); err != nil { + return ctrl.Result{}, err + } + return requeue, nil + } + if repository.Status.Id == nil { log.Info("Repository is not ready") return requeue, nil @@ -101,7 +109,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } sort.Slice(attr.Configuration, func(i, j int) bool { - return attr.Configuration[i].Name < attr.Configuration[i].Name + return attr.Configuration[i].Name < attr.Configuration[j].Name }) updater := console.ServiceUpdateAttributes{ Version: attr.Version, From ca73cc30f5dbc07ee1e16ca176ac74a53a5c88df Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 13 Dec 2023 14:37:41 +0100 Subject: [PATCH 113/198] add make release target and monorepo config for goreleaser --- .gitignore | 2 -- controller/.gitignore | 2 ++ controller/.goreleaser.yml | 59 +++++++++++++++++++------------------- controller/Makefile | 4 +++ 4 files changed, 36 insertions(+), 31 deletions(-) create mode 100644 controller/.gitignore diff --git a/.gitignore b/.gitignore index feb75a6c5..786d3beff 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,6 @@ watchman-*.tar /priv/data -/controller/bin/ - # IDE /.vscode/**/* !/.vscode/settings.json diff --git a/controller/.gitignore b/controller/.gitignore new file mode 100644 index 000000000..a5d8f7237 --- /dev/null +++ b/controller/.gitignore @@ -0,0 +1,2 @@ +bin/ +dist/ diff --git a/controller/.goreleaser.yml b/controller/.goreleaser.yml index 23f50b302..5eb3f30db 100644 --- a/controller/.goreleaser.yml +++ b/controller/.goreleaser.yml @@ -1,14 +1,20 @@ -# Visit https://goreleaser.com for documentation on how to customize this -# behavior. +# Visit https://goreleaser.com for documentation on how to customize this behavior. + +# Requires a GoReleaser Pro to run +partial: + by: goos + +project_name: plural-controller + +monorepo: + tag_prefix: controller/ + before: hooks: - # this is just an example and not a requirement for provider building/publishing - go mod tidy + builds: - env: - # goreleaser does not work with CGO, it could also complicate - # usage by users in CI/CD systems like Terraform Cloud where - # they are unable to install libraries. - CGO_ENABLED=0 mod_timestamp: '{{ .CommitTimestamp }}' flags: @@ -29,32 +35,27 @@ builds: - goos: darwin goarch: '386' binary: '{{ .ProjectName }}_v{{ .Version }}' + archives: - format: zip name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' + checksum: - extra_files: - - glob: 'terraform-registry-manifest.json' - name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' - algorithm: sha256 -signs: - - artifacts: checksum - args: - # if you are using this in a GitHub action or some other automated pipeline, you - # need to pass the batch flag to indicate its not interactive. - - "--batch" - - "--local-user" - - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key - - "--output" - - "${signature}" - - "--detach-sign" - - "${artifact}" -release: - extra_files: - - glob: 'terraform-registry-manifest.json' - name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' - # If you want to manually examine the release before its live, uncomment this line: - # draft: true + +snapshot: + name_template: "{{ incpatch .Version }}-next" + changelog: - skip: true + sort: asc + use: github-native + filters: + exclude: + - '^docs:' + - '^test:' + +release: + name_template: "{{.ProjectName}}-v{{.Version}}" + header: | + ## Plural Controller release ({{ .Date }}) + Welcome to this new release of the Plural Controller! diff --git a/controller/Makefile b/controller/Makefile index e0f73fb0f..ff0e9c32a 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -34,6 +34,10 @@ install-tools: ## install required tools build: manifests generate ## build go build -o bin/deployment-controller main.go +.PHONY: release +release: manifests generate ## builds release version of the app. Requires GoReleaser to work. + goreleaser build --clean --single-target --snapshot + docker-build: ## build Docker image with the driver docker build --no-cache -t ${IMG} . From 16e58bc1c9b03baf5cdd0997dca6456a229db83d Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 14 Dec 2023 10:42:24 +0100 Subject: [PATCH 114/198] unit test create a new repository --- controller/go.mod | 1 + .../controller_test.go | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 controller/pkg/gitrepository_controller/controller_test.go diff --git a/controller/go.mod b/controller/go.mod index 55322585b..942ba5473 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -62,6 +62,7 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.1.1 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fatih/structtag v1.2.0 // indirect diff --git a/controller/pkg/gitrepository_controller/controller_test.go b/controller/pkg/gitrepository_controller/controller_test.go new file mode 100644 index 000000000..748d86fd3 --- /dev/null +++ b/controller/pkg/gitrepository_controller/controller_test.go @@ -0,0 +1,110 @@ +package gitrepositorycontroller_test + +import ( + "context" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + "testing" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" + "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func init() { + utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +} + +func TestCreateNewRepository(t *testing.T) { + tests := []struct { + name string + repository string + returnGetRepository *gqlclient.GetGitRepository + returnErrorGetRepository error + returnCreateRepository *gqlclient.CreateGitRepository + returnErrorCreateRepository error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.GitRepositoryStatus + }{ + { + name: "scenario 1: create a new repository", + repository: "test", + expectedStatus: v1alpha1.GitRepositoryStatus{ + Health: "", + Message: nil, + Id: lo.ToPtr("123"), + Sha: "TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA====", + Existing: lo.ToPtr(false), + }, + returnGetRepository: &gqlclient.GetGitRepository{ + GitRepository: nil, + }, + returnCreateRepository: &gqlclient.CreateGitRepository{ + CreateGitRepository: &gqlclient.GitRepositoryFragment{ + ID: "123", + }, + }, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1alpha1.GitRepositorySpec{ + Url: "https://test", + CredentialsRef: &corev1.SecretReference{ + Name: "testsecret", + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testsecret", + }, + Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + target := &gitrepositorycontroller.Reconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) + fakeConsoleClient.On("CreateRepository", mock.AnythingOfType("string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.returnCreateRepository, test.returnErrorCreateRepository) + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) + assert.NoError(t, err) + existingRepo := &v1alpha1.GitRepository{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) + assert.NoError(t, err) + assert.Equal(t, existingRepo.Status, test.expectedStatus) + }) + } +} From c1157fd06f6e7e4f479d223eb569058d7e785bc4 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 14 Dec 2023 10:55:44 +0100 Subject: [PATCH 115/198] move reconcilers to the same package --- .../cluster_reconciler.go} | 26 +++++++---------- controller/pkg/reconciler/common.go | 16 +++++++++++ .../git_repository_reconciler.go} | 26 +++++++---------- .../git_repository_reconciler_test.go} | 9 +++--- .../provider_reconciler.go} | 28 +++++++------------ .../provider_reconciler_attributes.go} | 14 +++++----- .../service_reconciler.go} | 22 ++++++--------- controller/pkg/types/reconciler.go | 13 ++++----- 8 files changed, 71 insertions(+), 83 deletions(-) rename controller/pkg/{cluster_controller/reconciler.go => reconciler/cluster_reconciler.go} (88%) create mode 100644 controller/pkg/reconciler/common.go rename controller/pkg/{gitrepository_controller/controller.go => reconciler/git_repository_reconciler.go} (88%) rename controller/pkg/{gitrepository_controller/controller_test.go => reconciler/git_repository_reconciler_test.go} (95%) rename controller/pkg/{provider_reconciler/reconciler.go => reconciler/provider_reconciler.go} (86%) rename controller/pkg/{provider_reconciler/reconciler_attributes.go => reconciler/provider_reconciler_attributes.go} (78%) rename controller/pkg/{service_controller/controller.go => reconciler/service_reconciler.go} (93%) diff --git a/controller/pkg/cluster_controller/reconciler.go b/controller/pkg/reconciler/cluster_reconciler.go similarity index 88% rename from controller/pkg/cluster_controller/reconciler.go rename to controller/pkg/reconciler/cluster_reconciler.go index eba7159b3..283c45d4b 100644 --- a/controller/pkg/cluster_controller/reconciler.go +++ b/controller/pkg/reconciler/cluster_reconciler.go @@ -1,9 +1,8 @@ -package cluster_controller +package reconciler import ( "context" "fmt" - "time" "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" @@ -23,16 +22,11 @@ import ( ) const ( - RequeueAfter = 30 * time.Second FinalizerName = "deployments.plural.sh/cluster-protection" ) -var ( - requeue = ctrl.Result{RequeueAfter: RequeueAfter} -) - -// Reconciler reconciles a Cluster object. -type Reconciler struct { +// ClusterReconciler reconciles a Cluster object. +type ClusterReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient Log logr.Logger @@ -40,13 +34,13 @@ type Reconciler struct { } // SetupWithManager sets up the controller with the Manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Cluster{}). Complete(r) } -func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { logger := log.FromContext(ctx) // Read resource from Kubernetes cluster. @@ -107,7 +101,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return requeue, nil } -func (r *Reconciler) isExisting(cluster *v1alpha1.Cluster) (bool, error) { +func (r *ClusterReconciler) isExisting(cluster *v1alpha1.Cluster) (bool, error) { if cluster.Status.HasExisting() { return *cluster.Status.Existing, nil } @@ -127,7 +121,7 @@ func (r *Reconciler) isExisting(cluster *v1alpha1.Cluster) (bool, error) { return !cluster.Status.HasID(), nil } -func (r *Reconciler) handleExisting(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { +func (r *ClusterReconciler) handleExisting(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { apiCluster, err := r.ConsoleClient.GetClusterByHandle(cluster.Spec.Handle) if err != nil { return ctrl.Result{}, err @@ -148,7 +142,7 @@ func (r *Reconciler) handleExisting(ctx context.Context, cluster *v1alpha1.Clust return requeue, nil } -func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (*ctrl.Result, error) { +func (r *ClusterReconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (*ctrl.Result, error) { // If 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 to registering our finalizer. if cluster.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(cluster, FinalizerName) { @@ -193,7 +187,7 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1 return nil, nil } -func (r *Reconciler) getProviderIdAndSetControllerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { +func (r *ClusterReconciler) getProviderIdAndSetControllerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { logger := log.FromContext(ctx) if cluster.Spec.IsProviderRefRequired() { @@ -234,7 +228,7 @@ func (r *Reconciler) getProviderIdAndSetControllerRef(ctx context.Context, clust return nil, nil, nil } -func (r *Reconciler) sync(ctx context.Context, cluster *v1alpha1.Cluster, providerId *string, sha string) (*console.ClusterFragment, error) { +func (r *ClusterReconciler) sync(ctx context.Context, cluster *v1alpha1.Cluster, providerId *string, sha string) (*console.ClusterFragment, error) { exists := r.ConsoleClient.IsClusterExisting(cluster.Status.ID) logger := log.FromContext(ctx) diff --git a/controller/pkg/reconciler/common.go b/controller/pkg/reconciler/common.go new file mode 100644 index 000000000..468b07bbe --- /dev/null +++ b/controller/pkg/reconciler/common.go @@ -0,0 +1,16 @@ +package reconciler + +import ( + "time" + + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + // requeueAfter is the time between scheduled reconciles if there are no changes to the CRD. + requeueAfter = 30 * time.Second +) + +var ( + requeue = ctrl.Result{RequeueAfter: requeueAfter} +) diff --git a/controller/pkg/gitrepository_controller/controller.go b/controller/pkg/reconciler/git_repository_reconciler.go similarity index 88% rename from controller/pkg/gitrepository_controller/controller.go rename to controller/pkg/reconciler/git_repository_reconciler.go index 82ad80fcb..bc9d359ba 100644 --- a/controller/pkg/gitrepository_controller/controller.go +++ b/controller/pkg/reconciler/git_repository_reconciler.go @@ -1,9 +1,8 @@ -package gitrepositorycontroller +package reconciler import ( "context" "fmt" - "time" "github.com/samber/lo" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -26,17 +25,12 @@ import ( const ( RepoFinalizer = "deployments.plural.sh/gitrepo-protection" - RequeueAfter = 30 * time.Second privateKey = "privateKey" passphrase = "passphrase" username = "username" password = "password" ) -var ( - requeue = ctrl.Result{RequeueAfter: RequeueAfter} -) - type GitRepoCred struct { PrivateKey *string Passphrase *string @@ -44,15 +38,15 @@ type GitRepoCred struct { Password *string } -// Reconciler reconciles a GitRepository object -type Reconciler struct { +// GitRepositoryReconciler reconciles a GitRepository object +type GitRepositoryReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient Log logr.Logger Scheme *runtime.Scheme } -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { @@ -136,13 +130,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } // SetupWithManager sets up the controller with the Manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.GitRepository{}). Complete(r) } -func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { +func (r *GitRepositoryReconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { logger := log.FromContext(ctx) if controllerutil.ContainsFinalizer(repo, RepoFinalizer) { logger.Info("delete git repository") @@ -171,7 +165,7 @@ func (r *Reconciler) handleDelete(ctx context.Context, repo *v1alpha1.GitReposit return ctrl.Result{}, nil } -func (r *Reconciler) getRepositoryCredentials(ctx context.Context, repo *v1alpha1.GitRepository) (*GitRepoCred, error) { +func (r *GitRepositoryReconciler) getRepositoryCredentials(ctx context.Context, repo *v1alpha1.GitRepository) (*GitRepoCred, error) { cred := &GitRepoCred{} if repo.Spec.CredentialsRef != nil { secret := &corev1.Secret{} @@ -206,7 +200,7 @@ func (r *Reconciler) getRepositoryCredentials(ctx context.Context, repo *v1alpha return cred, nil } -func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, error) { +func (r *GitRepositoryReconciler) getRepository(url string) (*console.GitRepositoryFragment, error) { existingRepos, err := r.ConsoleClient.GetRepository(&url) if err != nil { return nil, err @@ -215,7 +209,7 @@ func (r *Reconciler) getRepository(url string) (*console.GitRepositoryFragment, return existingRepos.GitRepository, nil } -func (r *Reconciler) isAlreadyExists(repository *v1alpha1.GitRepository) (bool, error) { +func (r *GitRepositoryReconciler) isAlreadyExists(repository *v1alpha1.GitRepository) (bool, error) { if repository.Status.IsExisting() { return true, nil } @@ -232,7 +226,7 @@ func (r *Reconciler) isAlreadyExists(repository *v1alpha1.GitRepository) (bool, return !repository.Status.HasID(), nil } -func (r *Reconciler) handleExistingRepo(ctx context.Context, repo *v1alpha1.GitRepository) (reconcile.Result, error) { +func (r *GitRepositoryReconciler) handleExistingRepo(ctx context.Context, repo *v1alpha1.GitRepository) (reconcile.Result, error) { logger := log.FromContext(ctx) existingRepo, err := r.getRepository(repo.Spec.Url) if err != nil && !errors.IsNotFound(err) { diff --git a/controller/pkg/gitrepository_controller/controller_test.go b/controller/pkg/reconciler/git_repository_reconciler_test.go similarity index 95% rename from controller/pkg/gitrepository_controller/controller_test.go rename to controller/pkg/reconciler/git_repository_reconciler_test.go index 748d86fd3..7e062d50c 100644 --- a/controller/pkg/gitrepository_controller/controller_test.go +++ b/controller/pkg/reconciler/git_repository_reconciler_test.go @@ -1,14 +1,15 @@ -package gitrepositorycontroller_test +package reconciler_test import ( "context" + "testing" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/pkg/reconciler" "github.com/samber/lo" "github.com/stretchr/testify/mock" - "testing" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -90,7 +91,7 @@ func TestCreateNewRepository(t *testing.T) { // act ctx := context.Background() - target := &gitrepositorycontroller.Reconciler{ + target := &reconciler.GitRepositoryReconciler{ Client: fakeClient, Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), Scheme: scheme.Scheme, diff --git a/controller/pkg/provider_reconciler/reconciler.go b/controller/pkg/reconciler/provider_reconciler.go similarity index 86% rename from controller/pkg/provider_reconciler/reconciler.go rename to controller/pkg/reconciler/provider_reconciler.go index b51e1764a..fb547c1b8 100644 --- a/controller/pkg/provider_reconciler/reconciler.go +++ b/controller/pkg/reconciler/provider_reconciler.go @@ -1,9 +1,8 @@ -package providerreconciler +package reconciler import ( "context" "fmt" - "time" console "github.com/pluralsh/console-client-go" "github.com/samber/lo" @@ -21,9 +20,9 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" ) -// Reconciler reconciles a v1alpha1.Provider object. +// ProviderReconciler reconciles a v1alpha1.Provider object. // Implements reconcile.Reconciler and types.Controller -type Reconciler struct { +type ProviderReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient @@ -31,21 +30,14 @@ type Reconciler struct { } const ( - // RequeueAfter is the time between scheduled reconciles if there are no - // changes to the CRD. - RequeueAfter = 30 * time.Second // ProviderProtectionFinalizerName defines name for the main finalizer that synchronizes // resource deletion from the Console API prior to removing the CRD. ProviderProtectionFinalizerName = "providers.deployments.plural.sh/provider-protection" ) -var ( - requeue = ctrl.Result{RequeueAfter: RequeueAfter} -) - // Reconcile ... // TODO: Add kubebuilder rbac annotation -func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := log.FromContext(ctx) log.Info("Reconciling") @@ -103,7 +95,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return requeue, nil } -func (r *Reconciler) handleExistingProvider(ctx context.Context, provider v1alpha1.Provider) (reconcile.Result, error) { +func (r *ProviderReconciler) handleExistingProvider(ctx context.Context, provider v1alpha1.Provider) (reconcile.Result, error) { apiProvider, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) if err != nil { return ctrl.Result{}, err @@ -121,7 +113,7 @@ func (r *Reconciler) handleExistingProvider(ctx context.Context, provider v1alph return requeue, nil } -func (r *Reconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Provider) (bool, error) { +func (r *ProviderReconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Provider) (bool, error) { if provider.Status.HasExisting() { return *provider.Status.Existing, nil } @@ -143,7 +135,7 @@ func (r *Reconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Prov return false, nil } -func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { +func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { log := log.FromContext(ctx) // If object is not being deleted, so if it does not have our finalizer, @@ -193,7 +185,7 @@ func (r *Reconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1 return nil, nil } -func (r *Reconciler) sync(ctx context.Context, provider v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { +func (r *ProviderReconciler) sync(ctx context.Context, provider v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { log := log.FromContext(ctx) exists := r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) @@ -223,7 +215,7 @@ func (r *Reconciler) sync(ctx context.Context, provider v1alpha1.Provider, chang return r.ConsoleClient.CreateProvider(ctx, attributes) } -func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1.Provider) error { +func (r *ProviderReconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1.Provider) error { secretRef := r.getCloudProviderSettingsSecretRef(provider) if secretRef == nil { return fmt.Errorf("could not find secret ref configuration for cloud %q", provider.Spec.Cloud) @@ -238,7 +230,7 @@ func (r *Reconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1. } // SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *ProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { mgr.GetLogger().Info("Starting reconciler", "reconciler", "provider_reconciler") return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Provider{}). diff --git a/controller/pkg/provider_reconciler/reconciler_attributes.go b/controller/pkg/reconciler/provider_reconciler_attributes.go similarity index 78% rename from controller/pkg/provider_reconciler/reconciler_attributes.go rename to controller/pkg/reconciler/provider_reconciler_attributes.go index da728ae37..97b12003e 100644 --- a/controller/pkg/provider_reconciler/reconciler_attributes.go +++ b/controller/pkg/reconciler/provider_reconciler_attributes.go @@ -1,4 +1,4 @@ -package providerreconciler +package reconciler import ( "context" @@ -11,11 +11,11 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" ) -func (r *Reconciler) missingCredentialKeyError(key string) error { +func (r *ProviderReconciler) missingCredentialKeyError(key string) error { return fmt.Errorf("%q key does not exist in referenced credential secret", key) } -func (r *Reconciler) getCloudProviderSettingsSecretRef(provider v1alpha1.Provider) *corev1.SecretReference { +func (r *ProviderReconciler) getCloudProviderSettingsSecretRef(provider v1alpha1.Provider) *corev1.SecretReference { switch provider.Spec.Cloud { case v1alpha1.AWS: return provider.Spec.CloudSettings.AWS @@ -28,7 +28,7 @@ func (r *Reconciler) getCloudProviderSettingsSecretRef(provider v1alpha1.Provide return nil } -func (r *Reconciler) toCloudProviderSettingsAttributes(ctx context.Context, provider v1alpha1.Provider) (*console.CloudProviderSettingsAttributes, error) { +func (r *ProviderReconciler) toCloudProviderSettingsAttributes(ctx context.Context, provider v1alpha1.Provider) (*console.CloudProviderSettingsAttributes, error) { switch provider.Spec.Cloud { case v1alpha1.AWS: return r.toCloudProviderAWSSettingsAttributes(ctx, provider.Spec.CloudSettings.AWS) @@ -41,7 +41,7 @@ func (r *Reconciler) toCloudProviderSettingsAttributes(ctx context.Context, prov return nil, fmt.Errorf("unsupported cloud: %q", provider.Spec.Cloud) } -func (r *Reconciler) toCloudProviderAWSSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { +func (r *ProviderReconciler) toCloudProviderAWSSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { const accessKeyIDKeyName = "accessKeyId" const secretAccessKeyName = "secretAccessKey" @@ -68,7 +68,7 @@ func (r *Reconciler) toCloudProviderAWSSettingsAttributes(ctx context.Context, r }, nil } -func (r *Reconciler) toCloudProviderAzureSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { +func (r *ProviderReconciler) toCloudProviderAzureSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { const tenantIDKeyName = "tenantId" const subscriptionIDKeyName = "subscriptionId" const clientIDKeyName = "clientId" @@ -109,7 +109,7 @@ func (r *Reconciler) toCloudProviderAzureSettingsAttributes(ctx context.Context, }, nil } -func (r *Reconciler) toCloudProviderGCPSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { +func (r *ProviderReconciler) toCloudProviderGCPSettingsAttributes(ctx context.Context, ref *corev1.SecretReference) (*console.CloudProviderSettingsAttributes, error) { const applicationCredentialsKeyName = "applicationCredentials" secret, err := utils.GetSecret(ctx, r.Client, ref) diff --git a/controller/pkg/service_controller/controller.go b/controller/pkg/reconciler/service_reconciler.go similarity index 93% rename from controller/pkg/service_controller/controller.go rename to controller/pkg/reconciler/service_reconciler.go index 7eb1bc6db..e41b80270 100644 --- a/controller/pkg/service_controller/controller.go +++ b/controller/pkg/reconciler/service_reconciler.go @@ -1,10 +1,9 @@ -package servicecontroller +package reconciler import ( "context" "encoding/json" "sort" - "time" "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" @@ -24,22 +23,17 @@ import ( const ( ServiceFinalizer = "deployments.plural.sh/service-protection" - RequeueAfter = 30 * time.Second ) -var ( - requeue = ctrl.Result{RequeueAfter: RequeueAfter} -) - -// Reconciler reconciles a Service object -type Reconciler struct { +// ServiceReconciler reconciles a Service object +type ServiceReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient Log logr.Logger Scheme *runtime.Scheme } -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) service := &v1alpha1.ServiceDeployment{} if err := r.Get(ctx, req.NamespacedName, service); err != nil { @@ -183,7 +177,7 @@ func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.Servic } // SetupWithManager sets up the controller with the Manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.ServiceDeployment{}). Owns(&corev1.Secret{}). @@ -191,7 +185,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.ServiceDeployment, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { +func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.ServiceDeployment, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { attr := &console.ServiceDeploymentAttributes{ Name: service.Name, Namespace: service.Namespace, @@ -292,7 +286,7 @@ func (r *Reconciler) genServiceAttributes(ctx context.Context, service *v1alpha1 return attr, nil } -func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.ServiceDeployment) error { +func (r *ServiceReconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.ServiceDeployment) error { if service.Spec.ConfigurationRef != nil { configurationSecret, err := utils.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) if err != nil { @@ -332,7 +326,7 @@ func (r *Reconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.S return nil } -func (r *Reconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { +func (r *ServiceReconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { log := log.FromContext(ctx) if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { log.Info("try to delete service") diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index ce802bfc7..6cc3345ad 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -4,10 +4,7 @@ import ( "fmt" "github.com/pluralsh/console/controller/pkg/client" - clustercontroller "github.com/pluralsh/console/controller/pkg/cluster_controller" - gitrepositorycontroller "github.com/pluralsh/console/controller/pkg/gitrepository_controller" - providerreconciler "github.com/pluralsh/console/controller/pkg/provider_reconciler" - servicecontroller "github.com/pluralsh/console/controller/pkg/service_controller" + "github.com/pluralsh/console/controller/pkg/reconciler" ctrl "sigs.k8s.io/controller-runtime" ) @@ -41,28 +38,28 @@ func ToReconciler(reconciler string) (Reconciler, error) { func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.ConsoleClient) (Controller, error) { switch sc { case GitRepositoryReconciler: - return &gitrepositorycontroller.Reconciler{ + return &reconciler.GitRepositoryReconciler{ Client: mgr.GetClient(), Log: mgr.GetLogger(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ServiceDeploymentReconciler: - return &servicecontroller.Reconciler{ + return &reconciler.ServiceReconciler{ Client: mgr.GetClient(), Log: mgr.GetLogger(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ClusterReconciler: - return &clustercontroller.Reconciler{ + return &reconciler.ClusterReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Log: mgr.GetLogger(), Scheme: mgr.GetScheme(), }, nil case ProviderReconciler: - return &providerreconciler.Reconciler{ + return &reconciler.ProviderReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), From fc6ce18be274f41165cfce67c7ad2a6cec38178d Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Thu, 14 Dec 2023 11:57:33 +0100 Subject: [PATCH 116/198] update controller goreleaser to follow console versioning --- controller/.goreleaser.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/.goreleaser.yml b/controller/.goreleaser.yml index 5eb3f30db..53c8e490e 100644 --- a/controller/.goreleaser.yml +++ b/controller/.goreleaser.yml @@ -7,7 +7,7 @@ partial: project_name: plural-controller monorepo: - tag_prefix: controller/ + tag_prefix: v before: hooks: @@ -55,7 +55,7 @@ changelog: - '^test:' release: - name_template: "{{.ProjectName}}-v{{.Version}}" + name_template: "{{ .ProjectName }}-v{{ .Version }}" header: | ## Plural Controller release ({{ .Date }}) Welcome to this new release of the Plural Controller! From bc277701c5ee68960d62b130906b69c7291b6223 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 14 Dec 2023 12:33:17 +0100 Subject: [PATCH 117/198] add cluster create test --- .../pkg/reconciler/cluster_reconciler.go | 2 +- .../pkg/reconciler/cluster_reconciler_test.go | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 controller/pkg/reconciler/cluster_reconciler_test.go diff --git a/controller/pkg/reconciler/cluster_reconciler.go b/controller/pkg/reconciler/cluster_reconciler.go index 283c45d4b..d44571f96 100644 --- a/controller/pkg/reconciler/cluster_reconciler.go +++ b/controller/pkg/reconciler/cluster_reconciler.go @@ -7,6 +7,7 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -17,7 +18,6 @@ import ( "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/utils" ) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go new file mode 100644 index 000000000..5768dd022 --- /dev/null +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -0,0 +1,128 @@ +package reconciler_test + +import ( + "context" + "testing" + + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/reconciler" + "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func init() { + utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +} + +func TestCreateNewCluster(t *testing.T) { + const ( + cloud = "aws" + clusterName = "test-cluster" + clusterConsoleID = "12345-67890" + providerName = "test-provider" + providerNamespace = "test-provider" + providerConsoleID = "12345-67890" + ) + + tests := []struct { + name string + cluster string + returnGetClusterByHandle *gqlclient.ClusterFragment + returnErrorGetClusterByHandle error + returnIsClusterExisting bool + returnCreateCluster *gqlclient.ClusterFragment + returnErrorCreateCluster error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ClusterStatus + }{ + { + name: "scenario 1: create a new cluster", + cluster: clusterName, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Existing: lo.ToPtr(false), + }, + returnGetClusterByHandle: nil, + returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), + returnIsClusterExisting: false, + returnCreateCluster: &gqlclient.ClusterFragment{ + ID: clusterConsoleID, + }, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Version: lo.ToPtr("1.24"), + Cloud: cloud, + ProviderRef: &corev1.ObjectReference{ + Name: providerName, + }, + }, + }, + &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerName, + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: cloud, + Name: providerName, + Namespace: providerNamespace, + }, + Status: v1alpha1.ProviderStatus{ + ID: lo.ToPtr(providerConsoleID), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + ctx := context.Background() + target := &reconciler.ClusterReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) + + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) + assert.NoError(t, err) + + existingCluster := &v1alpha1.Cluster{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingCluster.Status) + }) + } +} From a3e34d38ab56aaaebf383cbd45482c1ad0312be0 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 14 Dec 2023 12:48:41 +0100 Subject: [PATCH 118/198] add BYOK cluster create test --- .../pkg/reconciler/cluster_reconciler_test.go | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 5768dd022..87db848b7 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -30,7 +30,6 @@ func init() { func TestCreateNewCluster(t *testing.T) { const ( - cloud = "aws" clusterName = "test-cluster" clusterConsoleID = "12345-67890" providerName = "test-provider" @@ -50,7 +49,7 @@ func TestCreateNewCluster(t *testing.T) { expectedStatus v1alpha1.ClusterStatus }{ { - name: "scenario 1: create a new cluster", + name: "scenario 1: create a new AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ ID: lo.ToPtr(clusterConsoleID), @@ -60,9 +59,7 @@ func TestCreateNewCluster(t *testing.T) { returnGetClusterByHandle: nil, returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), returnIsClusterExisting: false, - returnCreateCluster: &gqlclient.ClusterFragment{ - ID: clusterConsoleID, - }, + returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, existingObjects: []ctrlruntimeclient.Object{ &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ @@ -71,7 +68,7 @@ func TestCreateNewCluster(t *testing.T) { Spec: v1alpha1.ClusterSpec{ Handle: lo.ToPtr(clusterName), Version: lo.ToPtr("1.24"), - Cloud: cloud, + Cloud: "aws", ProviderRef: &corev1.ObjectReference{ Name: providerName, }, @@ -82,7 +79,7 @@ func TestCreateNewCluster(t *testing.T) { Name: providerName, }, Spec: v1alpha1.ProviderSpec{ - Cloud: cloud, + Cloud: "aws", Name: providerName, Namespace: providerNamespace, }, @@ -92,6 +89,30 @@ func TestCreateNewCluster(t *testing.T) { }, }, }, + { + name: "scenario 2: create a new BYOK cluster", + cluster: clusterName, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), + Existing: lo.ToPtr(false), + }, + returnGetClusterByHandle: nil, + returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), + returnIsClusterExisting: false, + returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Cloud: "byok", + }, + }, + }, + }, } for _, test := range tests { From d17df10af7dbc6a2808618e5a63b5500f646778d Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 14 Dec 2023 12:52:39 +0100 Subject: [PATCH 119/198] refactor tests --- .../pkg/reconciler/cluster_reconciler_test.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 87db848b7..9f8dbd7f9 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -117,15 +117,15 @@ func TestCreateNewCluster(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() fakeConsoleClient := mocks.NewConsoleClient(t) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) ctx := context.Background() + target := &reconciler.ClusterReconciler{ Client: fakeClient, Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), @@ -133,10 +133,6 @@ func TestCreateNewCluster(t *testing.T) { ConsoleClient: fakeConsoleClient, } - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) - fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) assert.NoError(t, err) From a1b9e985ad4a054da3731c7be3c10fa2fbc56b15 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Thu, 14 Dec 2023 12:55:25 +0100 Subject: [PATCH 120/198] add initial unit test for provider --- .../provider_reconciler_attributes.go | 7 +- .../reconciler/provider_reconciler_test.go | 122 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 controller/pkg/reconciler/provider_reconciler_test.go diff --git a/controller/pkg/reconciler/provider_reconciler_attributes.go b/controller/pkg/reconciler/provider_reconciler_attributes.go index 97b12003e..720db487c 100644 --- a/controller/pkg/reconciler/provider_reconciler_attributes.go +++ b/controller/pkg/reconciler/provider_reconciler_attributes.go @@ -5,9 +5,10 @@ import ( "fmt" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/pkg/utils" corev1 "k8s.io/api/core/v1" + "github.com/pluralsh/console/controller/pkg/utils" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" ) @@ -16,6 +17,10 @@ func (r *ProviderReconciler) missingCredentialKeyError(key string) error { } func (r *ProviderReconciler) getCloudProviderSettingsSecretRef(provider v1alpha1.Provider) *corev1.SecretReference { + if provider.Spec.CloudSettings == nil { + return nil + } + switch provider.Spec.Cloud { case v1alpha1.AWS: return provider.Spec.CloudSettings.AWS diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/pkg/reconciler/provider_reconciler_test.go new file mode 100644 index 000000000..d6aecb030 --- /dev/null +++ b/controller/pkg/reconciler/provider_reconciler_test.go @@ -0,0 +1,122 @@ +package reconciler_test + +import ( + "context" + "encoding/json" + "testing" + + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/pluralsh/console/controller/pkg/reconciler" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/test/mocks" +) + +func init() { + utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +} + +func TestCreateNewProvider(t *testing.T) { + tests := []struct { + name string + providerName string + returnCreateProvider *gqlclient.ClusterProviderFragment + returnGetProviderByCloudError error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ProviderStatus + }{ + { + name: "scenario 1: create a new provider", + providerName: "gcp-provider", + returnCreateProvider: &gqlclient.ClusterProviderFragment{ + ID: "1234", + Name: "gcp-provider", + Namespace: "gcp", + Cloud: "gcp", + }, + returnGetProviderByCloudError: errors.NewNotFound(schema.GroupResource{}, "gcp-provider"), + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gcp-provider", + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: "gcp", + CloudSettings: &v1alpha1.CloudProviderSettings{ + GCP: &corev1.SecretReference{ + Name: "credentials", + }, + }, + Name: "gcp-provider", + Namespace: "gcp", + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "credentials", + }, + Data: map[string][]byte{ + "applicationCredentials": []byte("mock"), + }, + }, + }, + expectedStatus: v1alpha1.ProviderStatus{ + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Existing: lo.ToPtr(false), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // set up the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + providerReconciler := &reconciler.ProviderReconciler{ + Client: fakeClient, + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, test.returnGetProviderByCloudError) + fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(false) + fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(test.returnCreateProvider, nil) + + _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) + assert.NoError(t, err) + + existingProvider := &v1alpha1.Provider{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) + + existingProviderStatusJson, err := json.Marshal(existingProvider.Status) + expectedStatusJson, err := json.Marshal(test.expectedStatus) + + assert.NoError(t, err) + assert.EqualValues(t, string(existingProviderStatusJson), string(expectedStatusJson)) + }) + } +} From 81645d4917ca832bfd6ab6d8026a7b9b3dea01b0 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 14 Dec 2023 13:21:14 +0100 Subject: [PATCH 121/198] add tests for adopting existing clusters --- .../pkg/reconciler/cluster_reconciler_test.go | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 9f8dbd7f9..52a4d0663 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -143,3 +143,85 @@ func TestCreateNewCluster(t *testing.T) { }) } } + +func TestAdoptExistingCluster(t *testing.T) { + const ( + clusterName = "test-cluster" + clusterConsoleID = "12345-67890" + ) + + tests := []struct { + name string + cluster string + returnGetClusterByHandle *gqlclient.ClusterFragment + returnErrorGetClusterByHandle error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ClusterStatus + }{ + { + name: "scenario 1: adopt existing AWS cluster", + cluster: clusterName, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + Existing: lo.ToPtr(true), + }, + returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, + returnErrorGetClusterByHandle: nil, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, + Spec: v1alpha1.ClusterSpec{Handle: lo.ToPtr(clusterName)}, + }, + }, + }, + { + name: "scenario 2: adopt existing BYOK cluster", + cluster: clusterName, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + Existing: lo.ToPtr(true), + CurrentVersion: lo.ToPtr("1.24.11"), + }, + returnGetClusterByHandle: &gqlclient.ClusterFragment{ + ID: clusterConsoleID, + CurrentVersion: lo.ToPtr("1.24.11"), + }, + returnErrorGetClusterByHandle: nil, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Cloud: "byok", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) + + ctx := context.Background() + + target := &reconciler.ClusterReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) + assert.NoError(t, err) + + existingCluster := &v1alpha1.Cluster{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingCluster.Status) + }) + } +} From c4750ddc9e7d30434a4259977a23fc004fedd4b9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Thu, 14 Dec 2023 14:14:20 +0100 Subject: [PATCH 122/198] add tests for updating clusters --- .../pkg/reconciler/cluster_reconciler_test.go | 135 +++++++++++++++--- 1 file changed, 117 insertions(+), 18 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 52a4d0663..f8e42b424 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -62,30 +62,22 @@ func TestCreateNewCluster(t *testing.T) { returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, existingObjects: []ctrlruntimeclient.Object{ &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - }, + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Version: lo.ToPtr("1.24"), - Cloud: "aws", - ProviderRef: &corev1.ObjectReference{ - Name: providerName, - }, + Handle: lo.ToPtr(clusterName), + Version: lo.ToPtr("1.24"), + Cloud: "aws", + ProviderRef: &corev1.ObjectReference{Name: providerName}, }, }, &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: providerName, - }, + ObjectMeta: metav1.ObjectMeta{Name: providerName}, Spec: v1alpha1.ProviderSpec{ Cloud: "aws", Name: providerName, Namespace: providerNamespace, }, - Status: v1alpha1.ProviderStatus{ - ID: lo.ToPtr(providerConsoleID), - }, + Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, }, }, }, @@ -103,9 +95,7 @@ func TestCreateNewCluster(t *testing.T) { returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, existingObjects: []ctrlruntimeclient.Object{ &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - }, + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, Spec: v1alpha1.ClusterSpec{ Handle: lo.ToPtr(clusterName), Cloud: "byok", @@ -144,6 +134,115 @@ func TestCreateNewCluster(t *testing.T) { } } +func TestUpdateCluster(t *testing.T) { + const ( + clusterName = "test-cluster" + clusterConsoleID = "12345-67890" + providerName = "test-provider" + providerNamespace = "test-provider" + providerConsoleID = "12345-67890" + ) + + tests := []struct { + name string + cluster string + returnIsClusterExisting bool + returnUpdateCluster *gqlclient.ClusterFragment + returnErrorUpdateCluster error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ClusterStatus + }{ + { + name: "scenario 1: update AWS cluster", + cluster: clusterName, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Existing: lo.ToPtr(false), + }, + returnIsClusterExisting: true, + returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Version: lo.ToPtr("1.24"), + Cloud: "aws", + ProviderRef: &corev1.ObjectReference{Name: providerName}, + }, + Status: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), + Existing: lo.ToPtr(false), + }, + }, + &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{Name: providerName}, + Spec: v1alpha1.ProviderSpec{ + Cloud: "aws", + Name: providerName, + Namespace: providerNamespace, + }, + Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, + }, + }, + }, + { + name: "scenario 2: update BYOK cluster", + cluster: clusterName, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), + Existing: lo.ToPtr(false), + }, + returnIsClusterExisting: true, + returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Cloud: "byok", + }, + Status: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Existing: lo.ToPtr(false), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) + fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return(test.returnUpdateCluster, test.returnErrorUpdateCluster) + + ctx := context.Background() + + target := &reconciler.ClusterReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) + assert.NoError(t, err) + + existingCluster := &v1alpha1.Cluster{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingCluster.Status) + }) + } +} + func TestAdoptExistingCluster(t *testing.T) { const ( clusterName = "test-cluster" From 99b3c0f98e822eb5a360de0bef869956ea66f2ab Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 14 Dec 2023 14:19:03 +0100 Subject: [PATCH 123/198] git repo unit tests --- .../git_repository_reconciler_test.go | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/controller/pkg/reconciler/git_repository_reconciler_test.go b/controller/pkg/reconciler/git_repository_reconciler_test.go index 7e062d50c..727cf784b 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_test.go +++ b/controller/pkg/reconciler/git_repository_reconciler_test.go @@ -105,7 +105,161 @@ func TestCreateNewRepository(t *testing.T) { existingRepo := &v1alpha1.GitRepository{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) assert.NoError(t, err) - assert.Equal(t, existingRepo.Status, test.expectedStatus) + assert.EqualValues(t, test.expectedStatus, existingRepo.Status) + }) + } +} + +func TestUpdateRepository(t *testing.T) { + tests := []struct { + name string + repository string + returnGetRepository *gqlclient.GetGitRepository + returnErrorGetRepository error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.GitRepositoryStatus + }{ + { + name: "scenario 1: update credentials", + repository: "test", + expectedStatus: v1alpha1.GitRepositoryStatus{ + Health: "", + Message: nil, + Id: lo.ToPtr("123"), + Sha: "TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA====", + Existing: lo.ToPtr(false), + }, + returnGetRepository: &gqlclient.GetGitRepository{ + GitRepository: &gqlclient.GitRepositoryFragment{ + ID: "123", + }, + }, + + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1alpha1.GitRepositorySpec{ + Url: "https://test", + CredentialsRef: &corev1.SecretReference{ + Name: "testsecret", + }, + }, + Status: v1alpha1.GitRepositoryStatus{ + Health: "", + Message: nil, + Id: lo.ToPtr("123"), + Sha: "ABC", + Existing: lo.ToPtr(false), + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testsecret", + }, + Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + target := &reconciler.GitRepositoryReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) + fakeConsoleClient.On("UpdateRepository", mock.Anything, mock.Anything).Return(&gqlclient.UpdateGitRepository{}, nil) + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) + assert.NoError(t, err) + existingRepo := &v1alpha1.GitRepository{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingRepo.Status) + }) + } +} + +func TestImportRepository(t *testing.T) { + tests := []struct { + name string + repository string + returnGetRepository *gqlclient.GetGitRepository + returnErrorGetRepository error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.GitRepositoryStatus + }{ + { + name: "scenario 1: update credentials", + repository: "test", + expectedStatus: v1alpha1.GitRepositoryStatus{ + Health: "", + Message: nil, + Id: lo.ToPtr("123"), + Existing: lo.ToPtr(true), + }, + returnGetRepository: &gqlclient.GetGitRepository{ + GitRepository: &gqlclient.GitRepositoryFragment{ + ID: "123", + }, + }, + + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1alpha1.GitRepositorySpec{ + Url: "https://test", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + target := &reconciler.GitRepositoryReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) + assert.NoError(t, err) + existingRepo := &v1alpha1.GitRepository{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingRepo.Status) }) } } From 614e0f3f014aabc9a6a1a6502eac76dea1295aa6 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Thu, 14 Dec 2023 15:26:00 +0100 Subject: [PATCH 124/198] add more unit tests for provider --- .../reconciler/provider_reconciler_test.go | 304 ++++++++++++++---- 1 file changed, 235 insertions(+), 69 deletions(-) diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/pkg/reconciler/provider_reconciler_test.go index d6aecb030..39d939be8 100644 --- a/controller/pkg/reconciler/provider_reconciler_test.go +++ b/controller/pkg/reconciler/provider_reconciler_test.go @@ -32,7 +32,7 @@ func init() { } func TestCreateNewProvider(t *testing.T) { - tests := []struct { + test := struct { name string providerName string returnCreateProvider *gqlclient.ClusterProviderFragment @@ -40,83 +40,249 @@ func TestCreateNewProvider(t *testing.T) { existingObjects []ctrlruntimeclient.Object expectedStatus v1alpha1.ProviderStatus }{ - { - name: "scenario 1: create a new provider", - providerName: "gcp-provider", - returnCreateProvider: &gqlclient.ClusterProviderFragment{ - ID: "1234", - Name: "gcp-provider", - Namespace: "gcp", - Cloud: "gcp", - }, - returnGetProviderByCloudError: errors.NewNotFound(schema.GroupResource{}, "gcp-provider"), - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gcp-provider", - }, - Spec: v1alpha1.ProviderSpec{ - Cloud: "gcp", - CloudSettings: &v1alpha1.CloudProviderSettings{ - GCP: &corev1.SecretReference{ - Name: "credentials", - }, + + name: "create a new provider", + providerName: "gcp-provider", + returnCreateProvider: &gqlclient.ClusterProviderFragment{ + ID: "1234", + Name: "gcp-provider", + Namespace: "gcp", + Cloud: "gcp", + }, + returnGetProviderByCloudError: errors.NewNotFound(schema.GroupResource{}, "gcp-provider"), + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gcp-provider", + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: "gcp", + CloudSettings: &v1alpha1.CloudProviderSettings{ + GCP: &corev1.SecretReference{ + Name: "credentials", }, - Name: "gcp-provider", - Namespace: "gcp", }, + Name: "gcp-provider", + Namespace: "gcp", }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "credentials", - }, - Data: map[string][]byte{ - "applicationCredentials": []byte("mock"), + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "credentials", + }, + Data: map[string][]byte{ + "applicationCredentials": []byte("mock"), + }, + }, + }, + expectedStatus: v1alpha1.ProviderStatus{ + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Existing: lo.ToPtr(false), + }, + } + + t.Run(test.name, func(t *testing.T) { + // set up the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + providerReconciler := &reconciler.ProviderReconciler{ + Client: fakeClient, + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, test.returnGetProviderByCloudError) + fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(false) + fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(test.returnCreateProvider, nil) + + _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) + assert.NoError(t, err) + + existingProvider := &v1alpha1.Provider{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) + + existingProviderStatusJson, err := json.Marshal(existingProvider.Status) + expectedStatusJson, err := json.Marshal(test.expectedStatus) + + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) + }) +} + +func TestAdoptProvider(t *testing.T) { + test := struct { + name string + providerName string + returnGetProviderByCloud *gqlclient.ClusterProviderFragment + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ProviderStatus + }{ + name: "adopt existing provider", + providerName: "gcp-provider", + returnGetProviderByCloud: &gqlclient.ClusterProviderFragment{ + ID: "1234", + Name: "gcp-provider", + Namespace: "gcp", + Cloud: "gcp", + }, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gcp-provider", + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: "gcp", + CloudSettings: &v1alpha1.CloudProviderSettings{ + GCP: &corev1.SecretReference{ + Name: "credentials", + }, }, + Name: "gcp-provider", + Namespace: "gcp", }, }, - expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Existing: lo.ToPtr(false), + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "credentials", + }, + Data: map[string][]byte{ + "applicationCredentials": []byte("mock"), + }, }, }, + expectedStatus: v1alpha1.ProviderStatus{ + ID: lo.ToPtr("1234"), + Existing: lo.ToPtr(true), + }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // set up the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - providerReconciler := &reconciler.ProviderReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, test.returnGetProviderByCloudError) - fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(false) - fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(test.returnCreateProvider, nil) - - _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) - assert.NoError(t, err) - - existingProvider := &v1alpha1.Provider{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - - existingProviderStatusJson, err := json.Marshal(existingProvider.Status) - expectedStatusJson, err := json.Marshal(test.expectedStatus) - - assert.NoError(t, err) - assert.EqualValues(t, string(existingProviderStatusJson), string(expectedStatusJson)) - }) + t.Run(test.name, func(t *testing.T) { + // set up the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + providerReconciler := &reconciler.ProviderReconciler{ + Client: fakeClient, + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(test.returnGetProviderByCloud, nil) + + _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) + assert.NoError(t, err) + + existingProvider := &v1alpha1.Provider{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) + + existingProviderStatusJson, err := json.Marshal(existingProvider.Status) + expectedStatusJson, err := json.Marshal(test.expectedStatus) + + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) + }) +} + +func TestUpdateProvider(t *testing.T) { + test := struct { + name string + providerName string + returnUpdateProvider *gqlclient.ClusterProviderFragment + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ProviderStatus + }{ + name: "update existing provider", + providerName: "gcp-provider", + returnUpdateProvider: &gqlclient.ClusterProviderFragment{ + ID: "1234", + Name: "gcp-provider", + Namespace: "gcp", + Cloud: "gcp", + }, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gcp-provider", + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: "gcp", + CloudSettings: &v1alpha1.CloudProviderSettings{ + GCP: &corev1.SecretReference{ + Name: "credentials", + }, + }, + Name: "gcp-provider", + Namespace: "gcp", + }, + Status: v1alpha1.ProviderStatus{ + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr(""), + Existing: lo.ToPtr(false), + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "credentials", + }, + Data: map[string][]byte{ + "applicationCredentials": []byte("mock"), + }, + }, + }, + expectedStatus: v1alpha1.ProviderStatus{ + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Existing: lo.ToPtr(false), + }, } + + t.Run(test.name, func(t *testing.T) { + // set up the test scenario + fakeClient := fake. + NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(test.existingObjects...). + Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + + // act + ctx := context.Background() + providerReconciler := &reconciler.ProviderReconciler{ + Client: fakeClient, + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(true, nil) + fakeConsoleClient.On("UpdateProvider", mock.Anything, mock.Anything, mock.Anything).Return(test.returnUpdateProvider, nil) + + _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) + assert.NoError(t, err) + + existingProvider := &v1alpha1.Provider{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) + + existingProviderStatusJson, err := json.Marshal(existingProvider.Status) + expectedStatusJson, err := json.Marshal(test.expectedStatus) + + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) + }) } From 0f92e281089d3ef93c0b3ec59f978903f3edd085 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 15 Dec 2023 11:13:23 +0100 Subject: [PATCH 125/198] add common condition types --- .../apis/deployments/v1alpha1/common.go | 22 +++++++++++++++++++ .../apis/deployments/v1alpha1/provider.go | 7 ++++++ 2 files changed, 29 insertions(+) diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index d9fb509fb..6b073e1de 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -65,3 +65,25 @@ func (t *Taint) Attributes() *console.TaintAttributes { // TaintEffect is the effect for a Kubernetes taint. type TaintEffect string + +type ConditionType string + +func (c ConditionType) String() string { + return string(c) +} + +const ( + ReadonlyConditionType ConditionType = "Readonly" + ReadyConditionType ConditionType = "Ready" +) + +type ConditionReason string + +func (c ConditionReason) String() string { + return string(c) +} + +const ( + ReadonlyConditionReason ConditionReason = "Readonly" + ReadyConditionReason ConditionReason = "Ready" +) diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index c5780b240..d6cd783cc 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -144,6 +144,13 @@ type ProviderStatus struct { // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=boolean Existing *bool `json:"existing,omitempty"` + // Represents the observations of a Provider'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"` } func (p *ProviderStatus) GetID() string { From 47a027c4fff4c4e49afce3dfe9518ae529aa8d99 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 15 Dec 2023 12:35:38 +0100 Subject: [PATCH 126/198] update provider reconciler logic --- .../apis/deployments/v1alpha1/provider.go | 15 +- .../v1alpha1/zz_generated.deepcopy.go | 11 +- .../bases/deployments.plural.sh_clusters.yaml | 2 +- ...deployments.plural.sh_gitrepositories.yaml | 2 +- .../deployments.plural.sh_providers.yaml | 79 ++++++++- ...loyments.plural.sh_servicedeployments.yaml | 2 +- controller/go.mod | 48 ++--- controller/go.sum | 167 ++++++++++++------ .../pkg/reconciler/provider_reconciler.go | 85 ++++----- .../provider_reconciler_attributes.go | 2 +- .../reconciler/provider_reconciler_scope.go | 42 +++++ controller/pkg/utils/kubernetes.go | 6 + 12 files changed, 324 insertions(+), 137 deletions(-) create mode 100644 controller/pkg/reconciler/provider_reconciler_scope.go diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index d6cd783cc..1f34a5405 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -6,6 +6,7 @@ import ( console "github.com/pluralsh/console-client-go" "github.com/samber/lo" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -139,13 +140,7 @@ type ProviderStatus struct { // +kubebuilder:validation:Optional // +kubebuilder:validation:Type:=string SHA *string `json:"sha,omitempty"` - // Existing flag is set to true when Console API object already exists when CRD is created. - // CRD is then set to read-only mode and does not update Console API from CRD. - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Type:=boolean - Existing *bool `json:"existing,omitempty"` // Represents the observations of a Provider's current state. - // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" // +patchMergeKey=type // +patchStrategy=merge // +listType=map @@ -185,6 +180,10 @@ func (p *ProviderStatus) IsSHAEqual(sha string) bool { return p.GetSHA() == sha } -func (p *ProviderStatus) HasExisting() bool { - return p.Existing != nil +func (p *ProviderStatus) HasReadonlyCondition() bool { + return meta.FindStatusCondition(p.Conditions, ReadonlyConditionType.String()) != nil +} + +func (p *ProviderStatus) IsReadonly() bool { + return meta.IsStatusConditionTrue(p.Conditions, ReadonlyConditionType.String()) } diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index a0e54e997..593fb014f 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -641,10 +642,12 @@ func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { *out = new(string) **out = **in } - if in.Existing != nil { - in, out := &in.Existing, &out.Existing - *out = new(bool) - **out = **in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index aef7b9d7d..e21a04dc9 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: clusters.deployments.plural.sh spec: diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index c6461425a..cc4ccb3d1 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: gitrepositories.deployments.plural.sh spec: diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index 671973d5b..e973fa2d3 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: providers.deployments.plural.sh spec: @@ -127,11 +127,78 @@ spec: status: description: ProviderStatus ... properties: - existing: - description: Existing flag is set to true when Console API object - already exists when CRD is created. CRD is then set to read-only - mode and does not update Console API from CRD. - type: boolean + conditions: + description: Represents the observations of a Provider's current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map id: description: ID of the provider in the Console API. type: string diff --git a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index c82c47ad6..373c3c805 100644 --- a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: servicedeployments.deployments.plural.sh spec: diff --git a/controller/go.mod b/controller/go.mod index 942ba5473..6e6ebb2b6 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -14,12 +14,13 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 - k8s.io/api v0.26.0 - k8s.io/apimachinery v0.26.0 - k8s.io/client-go v0.26.0 + k8s.io/api v0.26.10 + k8s.io/apimachinery v0.26.10 + k8s.io/client-go v0.26.10 k8s.io/klog v1.0.0 - sigs.k8s.io/controller-runtime v0.14.1 - sigs.k8s.io/controller-tools v0.11.1 + sigs.k8s.io/cluster-api v1.4.9 + sigs.k8s.io/controller-runtime v0.14.7 + sigs.k8s.io/controller-tools v0.10.0 ) require ( @@ -43,6 +44,7 @@ require ( github.com/ashanbrown/makezero v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v3 v3.4.0 // indirect github.com/breml/bidichk v0.2.7 // indirect @@ -62,7 +64,7 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.1.1 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -71,7 +73,7 @@ require ( github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.2.3 // indirect github.com/go-critic/go-critic v0.9.0 // indirect - github.com/go-logr/zapr v1.2.3 // indirect + github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect @@ -98,7 +100,7 @@ require ( github.com/golangci/misspell v0.4.1 // indirect github.com/golangci/revgrep v0.5.2 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.1 // indirect @@ -131,14 +133,14 @@ require ( github.com/leonklingele/grouper v1.1.1 // indirect github.com/lufeee/execinquery v1.2.1 // indirect github.com/macabu/inamedparam v0.1.2 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/maratori/testableexamples v1.0.0 // indirect github.com/maratori/testpackage v1.1.1 // indirect github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mbilski/exhaustivestruct v1.2.0 // indirect github.com/mgechev/revive v1.3.4 // indirect @@ -154,8 +156,8 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.14.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/onsi/gomega v1.28.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polyfloyd/go-errorlint v1.4.5 // indirect @@ -167,7 +169,7 @@ require ( github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.2 // indirect github.com/ryancurrah/gomodguard v1.3.0 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect @@ -182,15 +184,15 @@ require ( github.com/sivchari/tenv v1.7.1 // indirect github.com/sonatard/noctx v0.0.2 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.12.0 // indirect + github.com/spf13/viper v1.16.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect + github.com/subosito/gotenv v1.4.2 // indirect github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.15 // indirect @@ -209,30 +211,30 @@ require ( gitlab.com/bosi/decorder v0.4.1 // indirect go-simpler.org/sloglint v0.1.2 // indirect go.tmz.dev/musttag v0.7.2 // indirect - go.uber.org/atomic v1.7.0 // indirect + go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.14.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.4.6 // indirect - k8s.io/apiextensions-apiserver v0.26.0 // indirect - k8s.io/component-base v0.26.0 // indirect + k8s.io/apiextensions-apiserver v0.26.10 // indirect + k8s.io/component-base v0.26.10 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect diff --git a/controller/go.sum b/controller/go.sum index d92d5f597..ff1db4f8e 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -58,8 +58,17 @@ github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rW github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 h1:3ZBs7LAezy8gh0uECsA6CGU43FF3zsx5f4eah5FxTMA= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0/go.mod h1:rZLTje5A9kFBe0pzhpe2TdhRniBF++PRHQuRpR8esVc= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/Yamashou/gqlgenc v0.16.0 h1:k1X/dvwnkiDImaeYw+C1j+GDX3MnzB4aONOTE6Mrku4= @@ -83,6 +92,11 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +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/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= @@ -95,6 +109,10 @@ 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/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= @@ -103,6 +121,7 @@ github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/butuzov/ireturn v0.2.2 h1:jWI36dxXwVrI+RnXDwux2IZOewpmfv930OuIRfaBUJ0= github.com/butuzov/ireturn v0.2.2/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= @@ -114,6 +133,7 @@ github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQd github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -129,6 +149,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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/coredns/caddy v1.1.0 h1:ezvsPrT/tA/7pYDBZxu0cT0VmWk75AfIaf6GSYCNMf0= +github.com/coredns/caddy v1.1.0/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/corefile-migration v1.0.21 h1:W/DCETrHDiFo0Wj03EyMkaQ9fwsmSgqTCQDHpceaSsE= +github.com/coredns/corefile-migration v1.0.21/go.mod h1:XnhgULOEouimnzgn0t4WPuFDN2/PJQcTxdWKC5eXNGE= 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/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= @@ -140,6 +165,8 @@ 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/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -148,14 +175,14 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 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/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= -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 v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -164,12 +191,14 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghostiam/protogetter v0.2.3 h1:qdv2pzo3BpLqezwqfGDLZ+nHEYmc5bUpIdsMbBVwMjw= github.com/ghostiam/protogetter v0.2.3/go.mod h1:KmNLOsy1v04hKbvZs8EfGI1fk39AgTdRDxWNYPfXVc4= github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= @@ -185,11 +214,10 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V 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.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -281,8 +309,10 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSW github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 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/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/cel-go v0.12.7 h1:jM6p55R0MKBg79hZjn1zs2OlrywZ1Vk00rxVvad1/O0= +github.com/google/cel-go v0.12.7/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw= +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= @@ -336,6 +366,7 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -349,6 +380,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= @@ -413,8 +446,8 @@ github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCE github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/macabu/inamedparam v0.1.2 h1:RR5cnayM6Q7cDhQol32DE2BGAPGMnffJ31LFE+UklaU= github.com/macabu/inamedparam v0.1.2/go.mod h1:Xg25QvY7IBRl1KLPV9Rbml8JOMZtF/iAkNkmV7eQgjw= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= @@ -432,8 +465,9 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -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/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/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.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -443,10 +477,14 @@ github.com/mgechev/revive v1.3.4 h1:k/tO3XTaWY4DEHal9tWBkkUMJYO/dLDVyMmAQxmIMDc= github.com/mgechev/revive v1.3.4/go.mod h1:W+pZCMu9qj8Uhfs1iJMQsEFLRozUfvwFwqVvRbSNLVw= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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= @@ -478,6 +516,8 @@ github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4 github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc= github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= @@ -485,10 +525,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 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= @@ -536,8 +574,10 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -562,6 +602,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -579,22 +621,24 @@ github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= +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= @@ -609,10 +653,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= @@ -639,6 +684,9 @@ github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvni github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +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/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -666,17 +714,18 @@ 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= -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/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +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/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -687,8 +736,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= @@ -775,6 +824,7 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -794,8 +844,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= 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= @@ -887,6 +937,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 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.4/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -975,8 +1026,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= +gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/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= @@ -1027,6 +1078,7 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG 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= @@ -1034,13 +1086,18 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D 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-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/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-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1054,9 +1111,12 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji 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.31.1/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.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +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/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= @@ -1069,8 +1129,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/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= @@ -1086,6 +1147,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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= @@ -1094,7 +1156,6 @@ 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.0/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= @@ -1107,16 +1168,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= -k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= -k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= -k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= -k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= -k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= -k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= -k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= -k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= +k8s.io/api v0.26.10 h1:skTnrDR0r8dg4MMLf6YZIzugxNM0BjFsWKPkNc5kOvk= +k8s.io/api v0.26.10/go.mod h1:ou/H3yviqrHtP/DSPVTfsc7qNfmU06OhajytJfYXkXw= +k8s.io/apiextensions-apiserver v0.26.10 h1:wAriTUc6l7gUqJKOxhmXnYo/VNJzk4oh4QLCUR4Uq+k= +k8s.io/apiextensions-apiserver v0.26.10/go.mod h1:N2qhlxkhJLSoC4f0M1/1lNG627b45SYqnOPEVFoQXw4= +k8s.io/apimachinery v0.26.10 h1:aE+J2KIbjctFqPp3Y0q4Wh2PD+l1p2g3Zp4UYjSvtGU= +k8s.io/apimachinery v0.26.10/go.mod h1:iT1ZP4JBP34wwM+ZQ8ByPEQ81u043iqAcsJYftX9amM= +k8s.io/apiserver v0.26.10 h1:gradpIHygzZN87yK+o6V3gpbCSF78HZ0hejLZQQwdDs= +k8s.io/apiserver v0.26.10/go.mod h1:TGrQKQWUfQcotK3P4TtoVZxXOWklFF36QZlA5wufLs4= +k8s.io/client-go v0.26.10 h1:4mDzl+1IrfRxh4Ro0s65JRGJp14w77gSMUTjACYWVRo= +k8s.io/client-go v0.26.10/go.mod h1:sh74ig838gCckU4ElYclWb24lTesPdEDPnlyg5vcbkA= +k8s.io/cluster-bootstrap v0.25.0 h1:KJ2/r0dV+bLfTK5EBobAVKvjGel3N4Qqh3bvnzh9qPk= +k8s.io/cluster-bootstrap v0.25.0/go.mod h1:x/TCtY3EiuR/rODkA3SvVQT3uSssQLf9cXcmSjdDTe0= +k8s.io/component-base v0.26.10 h1:vl3Gfe5aC09mNxfnQtTng7u3rnBVrShOK3MAkqEleb0= +k8s.io/component-base v0.26.10/go.mod h1:/IDdENUHG5uGxqcofZajovYXE9KSPzJ4yQbkYQt7oN0= 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.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= @@ -1136,10 +1201,12 @@ mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RF 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/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM= -sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= -sigs.k8s.io/controller-tools v0.11.1 h1:blfU7DbmXuACWHfpZR645KCq8cLOc6nfkipGSGnH+Wk= -sigs.k8s.io/controller-tools v0.11.1/go.mod h1:dm4bN3Yp1ZP+hbbeSLF8zOEHsI1/bf15u3JNcgRv2TM= +sigs.k8s.io/cluster-api v1.4.9 h1:3cS3r09k/iCEKbV3OJvPOVwF/bZtD3BhTRx2oGa+fO8= +sigs.k8s.io/cluster-api v1.4.9/go.mod h1:pf1yqnCM26vXEVEjHhVAbnMtSJs+EKdpzyFiWwUAoZY= +sigs.k8s.io/controller-runtime v0.14.7 h1:Vrnm2vk9ZFlRkXATHz0W0wXcqNl7kPat8q2JyxVy0Q8= +sigs.k8s.io/controller-runtime v0.14.7/go.mod h1:ErTs3SJCOujNUnTz4AS+uh8hp6DHMo1gj6fFndJT1X8= +sigs.k8s.io/controller-tools v0.10.0 h1:0L5DTDTFB67jm9DkfrONgTGmfc/zYow0ZaHyppizU2U= +sigs.k8s.io/controller-tools v0.10.0/go.mod h1:uvr0EW6IsprfB0jpQq6evtKy+hHyHCXNfdWI5ONPx94= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/controller/pkg/reconciler/provider_reconciler.go b/controller/pkg/reconciler/provider_reconciler.go index fb547c1b8..c3f527990 100644 --- a/controller/pkg/reconciler/provider_reconciler.go +++ b/controller/pkg/reconciler/provider_reconciler.go @@ -5,7 +5,6 @@ import ( "fmt" console "github.com/pluralsh/console-client-go" - "github.com/samber/lo" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -37,16 +36,29 @@ const ( // Reconcile ... // TODO: Add kubebuilder rbac annotation -func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { log := log.FromContext(ctx) log.Info("Reconciling") // Read Provider CRD from the K8S API - var provider v1alpha1.Provider - if err := r.Get(ctx, req.NamespacedName, &provider); err != nil { + var provider *v1alpha1.Provider + if err := r.Get(ctx, req.NamespacedName, provider); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } + scope, err := NewProviderScope(ctx, r.Client, provider) + if err != nil { + log.Error(err, "failed to create scope") + return ctrl.Result{}, err + } + + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + // Check if resource already exists in the API and only sync the ID exists, err := r.isAlreadyExists(ctx, provider) if err != nil { @@ -81,41 +93,28 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques return ctrl.Result{}, err } - // Sync back Provider to crd status - if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) (any, any) { - p.Status.ID = &apiProvider.ID - p.Status.SHA = &sha - p.Status.Existing = lo.ToPtr(false) - - return original.Status, p.Status - }); err != nil { - return ctrl.Result{}, err - } + provider.Status.ID = &apiProvider.ID + provider.Status.SHA = &sha + //provider.Status.Existing = lo.ToPtr(false) return requeue, nil } -func (r *ProviderReconciler) handleExistingProvider(ctx context.Context, provider v1alpha1.Provider) (reconcile.Result, error) { +func (r *ProviderReconciler) handleExistingProvider(ctx context.Context, provider *v1alpha1.Provider) (reconcile.Result, error) { apiProvider, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) if err != nil { return ctrl.Result{}, err } - if err = utils.TryUpdateStatus[*v1alpha1.Provider](ctx, r.Client, &provider, func(p *v1alpha1.Provider, original *v1alpha1.Provider) (any, any) { - p.Status.ID = &apiProvider.ID - p.Status.Existing = lo.ToPtr(true) - - return original.Status, p.Status - }); err != nil { - return ctrl.Result{}, err - } + provider.Status.ID = &apiProvider.ID + //provider.Status.Existing = lo.ToPtr(true) return requeue, nil } -func (r *ProviderReconciler) isAlreadyExists(ctx context.Context, provider v1alpha1.Provider) (bool, error) { - if provider.Status.HasExisting() { - return *provider.Status.Existing, nil +func (r *ProviderReconciler) isAlreadyExists(ctx context.Context, provider *v1alpha1.Provider) (bool, error) { + if provider.Status.HasReadonlyCondition() { + return provider.Status.IsReadonly(), nil } _, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) @@ -129,23 +128,24 @@ func (r *ProviderReconciler) isAlreadyExists(ctx context.Context, provider v1alp if !provider.Status.HasID() { log.FromContext(ctx).Info("Provider already exists in the API, running in read-only mode") + //utils.MarkCondition(setter, conditionType, conditionStatus, conditionReason, message) return true, nil } return false, nil } -func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider v1alpha1.Provider) (*ctrl.Result, error) { +func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider *v1alpha1.Provider) (*ctrl.Result, error) { log := log.FromContext(ctx) // If 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 // to registering our finalizer. - if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&provider, ProviderProtectionFinalizerName) { - controllerutil.AddFinalizer(&provider, ProviderProtectionFinalizerName) - if err := r.Update(ctx, &provider); err != nil { - return &ctrl.Result{}, err - } + if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(provider, ProviderProtectionFinalizerName) { + controllerutil.AddFinalizer(provider, ProviderProtectionFinalizerName) + //if err := r.Update(ctx, provider); err != nil { + // return &ctrl.Result{}, err + //} } // If object is being deleted @@ -170,22 +170,23 @@ func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider return &requeue, nil } - // If our finalizer is present, remove it - if controllerutil.ContainsFinalizer(&provider, ProviderProtectionFinalizerName) { - controllerutil.RemoveFinalizer(&provider, ProviderProtectionFinalizerName) - if err := r.Update(ctx, &provider); err != nil { - return &ctrl.Result{}, err - } - } + //// If our finalizer is present, remove it + //if controllerutil.ContainsFinalizer(provider, ProviderProtectionFinalizerName) { + // controllerutil.RemoveFinalizer(provider, ProviderProtectionFinalizerName) + // //if err := r.Update(ctx, provider); err != nil { + // // return &ctrl.Result{}, err + // //} + //} // Stop reconciliation as the item is being deleted + controllerutil.RemoveFinalizer(provider, ProviderProtectionFinalizerName) return &ctrl.Result{}, nil } return nil, nil } -func (r *ProviderReconciler) sync(ctx context.Context, provider v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { +func (r *ProviderReconciler) sync(ctx context.Context, provider *v1alpha1.Provider, changed bool) (*console.ClusterProviderFragment, error) { log := log.FromContext(ctx) exists := r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) @@ -215,7 +216,7 @@ func (r *ProviderReconciler) sync(ctx context.Context, provider v1alpha1.Provide return r.ConsoleClient.CreateProvider(ctx, attributes) } -func (r *ProviderReconciler) tryAddControllerRef(ctx context.Context, provider v1alpha1.Provider) error { +func (r *ProviderReconciler) tryAddControllerRef(ctx context.Context, provider *v1alpha1.Provider) error { secretRef := r.getCloudProviderSettingsSecretRef(provider) if secretRef == nil { return fmt.Errorf("could not find secret ref configuration for cloud %q", provider.Spec.Cloud) @@ -226,7 +227,7 @@ func (r *ProviderReconciler) tryAddControllerRef(ctx context.Context, provider v return err } - return utils.TryAddControllerRef(ctx, r.Client, &provider, secret, r.Scheme) + return utils.TryAddControllerRef(ctx, r.Client, provider, secret, r.Scheme) } // SetupWithManager is responsible for initializing new reconciler within provided ctrl.Manager. diff --git a/controller/pkg/reconciler/provider_reconciler_attributes.go b/controller/pkg/reconciler/provider_reconciler_attributes.go index 720db487c..052033815 100644 --- a/controller/pkg/reconciler/provider_reconciler_attributes.go +++ b/controller/pkg/reconciler/provider_reconciler_attributes.go @@ -16,7 +16,7 @@ func (r *ProviderReconciler) missingCredentialKeyError(key string) error { return fmt.Errorf("%q key does not exist in referenced credential secret", key) } -func (r *ProviderReconciler) getCloudProviderSettingsSecretRef(provider v1alpha1.Provider) *corev1.SecretReference { +func (r *ProviderReconciler) getCloudProviderSettingsSecretRef(provider *v1alpha1.Provider) *corev1.SecretReference { if provider.Spec.CloudSettings == nil { return nil } diff --git a/controller/pkg/reconciler/provider_reconciler_scope.go b/controller/pkg/reconciler/provider_reconciler_scope.go new file mode 100644 index 000000000..85b155b3c --- /dev/null +++ b/controller/pkg/reconciler/provider_reconciler_scope.go @@ -0,0 +1,42 @@ +package reconciler + +import ( + "context" + "errors" + "fmt" + + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" +) + +type ProviderScope struct { + Client client.Client + Provider *v1alpha1.Provider + + ctx context.Context + patchHelper *patch.Helper +} + +func (p *ProviderScope) PatchObject() error { + return p.patchHelper.Patch(p.ctx, p.Provider) +} + +func NewProviderScope(ctx context.Context, client client.Client, provider *v1alpha1.Provider) (*ProviderScope, error) { + if provider == nil { + return nil, errors.New("failed to create new ProviderScope from nil Provider") + } + + helper, err := patch.NewHelper(provider, client) + if err != nil { + return nil, fmt.Errorf("failed to init patch helper: %s", err) + } + + return &ProviderScope{ + Client: client, + Provider: provider, + ctx: ctx, + patchHelper: helper, + }, nil +} diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index 911a64836..def7b4bf8 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -14,6 +14,8 @@ import ( "k8s.io/client-go/util/retry" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" ) func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, object ctrlruntimeclient.Object, scheme *runtime.Scheme) error { @@ -202,3 +204,7 @@ func GetConfigMapData(ctx context.Context, client ctrlruntimeclient.Client, name return "", nil } + +func MarkCondition(setter func(), conditionType v1alpha1.ConditionType, conditionStatus corev1.ConditionStatus, conditionReason v1alpha1.ConditionReason, message string) { + +} From 49c680ade293f4b01007e31885d1acfa5ec4e5cb Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 15 Dec 2023 13:19:44 +0100 Subject: [PATCH 127/198] start adding cluster conditions --- .../apis/deployments/v1alpha1/cluster.go | 19 ++-- .../v1alpha1/zz_generated.deepcopy.go | 10 ++- .../bases/deployments.plural.sh_clusters.yaml | 77 ++++++++++++++-- .../pkg/reconciler/cluster_reconciler.go | 89 ++++++++++--------- .../reconciler/cluster_reconciler_scope.go | 42 +++++++++ 5 files changed, 178 insertions(+), 59 deletions(-) create mode 100644 controller/pkg/reconciler/cluster_reconciler_scope.go diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 5c0ebc475..900ed5caf 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -8,6 +8,7 @@ import ( console "github.com/pluralsh/console-client-go" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/json" ) @@ -359,10 +360,12 @@ type ClusterStatus struct { // +kubebuilder:validation:Type:=string SHA *string `json:"sha,omitempty"` - // Existing if set to true, then Console will not be synced with the data from this resource. - // It can be used to read already existing resources. - // +kubebuilder:validation:Optional - Existing *bool `json:"existing,omitempty"` + // Represents the observations of a Cluster current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // CurrentVersion contains current Kubernetes version this cluster is using. // +kubebuilder:validation:Optional @@ -392,6 +395,10 @@ func (cs *ClusterStatus) IsSHAChanged(sha string) bool { return cs.HasSHA() && *cs.SHA != sha } -func (cs *ClusterStatus) HasExisting() bool { - return cs.Existing != nil +func (cs *ClusterStatus) HasReadonlyCondition() bool { + return meta.FindStatusCondition(cs.Conditions, ReadonlyConditionType.String()) != nil +} + +func (cs *ClusterStatus) IsReadonly() bool { + return meta.IsStatusConditionTrue(cs.Conditions, ReadonlyConditionType.String()) } diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 593fb014f..003edec32 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -394,10 +394,12 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = new(string) **out = **in } - if in.Existing != nil { - in, out := &in.Existing, &out.Existing - *out = new(bool) - **out = **in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.CurrentVersion != nil { in, out := &in.CurrentVersion, &out.CurrentVersion diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index e21a04dc9..6c20b46a7 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -274,15 +274,82 @@ spec: type: object status: properties: + conditions: + description: Represents the observations of a Cluster current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map currentVersion: description: CurrentVersion contains current Kubernetes version this cluster is using. type: string - existing: - description: Existing if set to true, then Console will not be synced - with the data from this resource. It can be used to read already - existing resources. - type: boolean id: description: ID from Console. type: string diff --git a/controller/pkg/reconciler/cluster_reconciler.go b/controller/pkg/reconciler/cluster_reconciler.go index d44571f96..1222a4539 100644 --- a/controller/pkg/reconciler/cluster_reconciler.go +++ b/controller/pkg/reconciler/cluster_reconciler.go @@ -6,7 +6,6 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" - "github.com/samber/lo" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -40,7 +39,7 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { logger := log.FromContext(ctx) // Read resource from Kubernetes cluster. @@ -50,6 +49,18 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request return ctrl.Result{}, client.IgnoreNotFound(err) } + // Ensure that status updates will always be persisted when exiting this function. + scope, err := NewClusterScope(ctx, r.Client, cluster) + if err != nil { + logger.Error(err, "Failed to create cluster scope") + return ctrl.Result{}, err + } + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + // Handle existing resource. existing, err := r.isExisting(cluster) if err != nil { @@ -61,9 +72,8 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request } // Handle resource deletion both in Kubernetes cluster and in Console API. - result, err := r.addOrRemoveFinalizer(ctx, cluster) - if result != nil { - return *result, err + if result := r.addOrRemoveFinalizer(cluster); result != nil { + return *result, nil } // Get Provider ID from the reference if it is set and ensure that controller reference is set properly. @@ -85,25 +95,19 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request } // Update resource status. - if err = utils.TryUpdateStatus[*v1alpha1.Cluster](ctx, r.Client, cluster, func(c *v1alpha1.Cluster, original *v1alpha1.Cluster) (any, any) { - c.Status.ID = &apiCluster.ID - c.Status.KasURL = apiCluster.KasURL - c.Status.CurrentVersion = apiCluster.CurrentVersion - c.Status.PingedAt = apiCluster.PingedAt - c.Status.SHA = &sha - c.Status.Existing = lo.ToPtr(false) - - return original.Status, c.Status - }); err != nil { - return ctrl.Result{}, err - } + cluster.Status.ID = &apiCluster.ID + cluster.Status.KasURL = apiCluster.KasURL + cluster.Status.CurrentVersion = apiCluster.CurrentVersion + cluster.Status.PingedAt = apiCluster.PingedAt + cluster.Status.SHA = &sha + // TODO cluster.Status.Existing = lo.ToPtr(false) return requeue, nil } func (r *ClusterReconciler) isExisting(cluster *v1alpha1.Cluster) (bool, error) { - if cluster.Status.HasExisting() { - return *cluster.Status.Existing, nil + if cluster.Status.HasReadonlyCondition() { + return cluster.Status.IsReadonly(), nil } if !cluster.Spec.HasHandle() { @@ -127,36 +131,38 @@ func (r *ClusterReconciler) handleExisting(ctx context.Context, cluster *v1alpha return ctrl.Result{}, err } - if err = utils.TryUpdateStatus[*v1alpha1.Cluster](ctx, r.Client, cluster, func(c *v1alpha1.Cluster, original *v1alpha1.Cluster) (any, any) { - c.Status.ID = &apiCluster.ID - c.Status.KasURL = apiCluster.KasURL - c.Status.CurrentVersion = apiCluster.CurrentVersion - c.Status.PingedAt = apiCluster.PingedAt - c.Status.Existing = lo.ToPtr(true) + cluster.Status.ID = &apiCluster.ID + cluster.Status.KasURL = apiCluster.KasURL + cluster.Status.CurrentVersion = apiCluster.CurrentVersion + cluster.Status.PingedAt = apiCluster.PingedAt + // TODO c.Status.Existing = lo.ToPtr(true) - return original.Status, c.Status - }); err != nil { - return ctrl.Result{}, err - } + //meta.SetStatusCondition(&cluster.Status.Conditions, v1.Condition{ + // Type: v1alpha1.ReadonlyConditionType.String(), + // Status: v1.ConditionTrue, + // Reason: v1alpha1.ReadonlyConditionReason.String(), + // Message: "Cluster already exists, running in read-only mode where only status will be updated and no changes in Console will be made", + //}) + // // Existing if set to true, then Console will not be synced with the data from this resource. + // // It can be used to read already existing resources. + // // +kubebuilder:validation:Optional + // Existing *bool `json:"existing,omitempty"` return requeue, nil } -func (r *ClusterReconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v1alpha1.Cluster) (*ctrl.Result, error) { +func (r *ClusterReconciler) addOrRemoveFinalizer(cluster *v1alpha1.Cluster) *ctrl.Result { // If 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 to registering our finalizer. if cluster.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(cluster, FinalizerName) { controllerutil.AddFinalizer(cluster, FinalizerName) - if err := r.Update(ctx, cluster); err != nil { - return &ctrl.Result{}, err - } } // If object is being deleted. if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { // If object is already being deleted from Console API requeue. if r.ConsoleClient.IsClusterDeleting(cluster.Status.ID) { - return &requeue, nil + return &requeue } // Remove Cluster from Console API if it exists. @@ -164,27 +170,22 @@ func (r *ClusterReconciler) addOrRemoveFinalizer(ctx context.Context, cluster *v if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil { // if fail to delete the external dependency here, return with error // so that it can be retried. - return &ctrl.Result{}, err + return &ctrl.Result{} } // If deletion process started requeue so that we can make sure provider // has been deleted from Console API before removing the finalizer. - return &requeue, nil + return &requeue } // If our finalizer is present, remove it. - if controllerutil.ContainsFinalizer(cluster, FinalizerName) { - controllerutil.RemoveFinalizer(cluster, FinalizerName) - if err := r.Update(ctx, cluster); err != nil { - return &ctrl.Result{}, err - } - } + controllerutil.RemoveFinalizer(cluster, FinalizerName) // Stop reconciliation as the item is being deleted. - return &ctrl.Result{}, nil + return &ctrl.Result{} } - return nil, nil + return nil } func (r *ClusterReconciler) getProviderIdAndSetControllerRef(ctx context.Context, cluster *v1alpha1.Cluster) (providerId *string, result *ctrl.Result, err error) { diff --git a/controller/pkg/reconciler/cluster_reconciler_scope.go b/controller/pkg/reconciler/cluster_reconciler_scope.go new file mode 100644 index 000000000..8a6963b15 --- /dev/null +++ b/controller/pkg/reconciler/cluster_reconciler_scope.go @@ -0,0 +1,42 @@ +package reconciler + +import ( + "context" + "errors" + "fmt" + + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" +) + +type ClusterScope struct { + Client client.Client + Cluster *v1alpha1.Cluster + + ctx context.Context + patchHelper *patch.Helper +} + +func (p *ClusterScope) PatchObject() error { + return p.patchHelper.Patch(p.ctx, p.Cluster) +} + +func NewClusterScope(ctx context.Context, client client.Client, cluster *v1alpha1.Cluster) (*ClusterScope, error) { + if cluster == nil { + return nil, errors.New("failed to create new cluster scope, got nil cluster") + } + + helper, err := patch.NewHelper(cluster, client) + if err != nil { + return nil, fmt.Errorf("failed to create new cluster scope, go error: %s", err) + } + + return &ClusterScope{ + Client: client, + Cluster: cluster, + ctx: ctx, + patchHelper: helper, + }, nil +} From 2744ef9c1c103f3bf1cb40e85985d4f9e2e7b8f8 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 15 Dec 2023 13:44:26 +0100 Subject: [PATCH 128/198] set up provider conditions --- .../apis/deployments/v1alpha1/common.go | 10 ++++ .../apis/deployments/v1alpha1/provider.go | 4 ++ .../config/crd/examples/all_in_one_gcp.yaml | 52 +++++++++---------- .../config/crd/examples/provider_gcp.yaml | 4 +- .../pkg/reconciler/provider_reconciler.go | 37 ++++++------- controller/pkg/utils/kubernetes.go | 9 +++- 6 files changed, 66 insertions(+), 50 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/apis/deployments/v1alpha1/common.go index 6b073e1de..737ddbbe9 100644 --- a/controller/apis/deployments/v1alpha1/common.go +++ b/controller/apis/deployments/v1alpha1/common.go @@ -87,3 +87,13 @@ const ( ReadonlyConditionReason ConditionReason = "Readonly" ReadyConditionReason ConditionReason = "Ready" ) + +type ConditionMessage string + +func (c ConditionMessage) String() string { + return string(c) +} + +const ( + ReadonlyTrueConditionMessage ConditionMessage = "Running in read-only mode. Resource already exists upstream and will not be synced." +) diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/apis/deployments/v1alpha1/provider.go index 1f34a5405..141424955 100644 --- a/controller/apis/deployments/v1alpha1/provider.go +++ b/controller/apis/deployments/v1alpha1/provider.go @@ -93,6 +93,10 @@ func (p *Provider) Diff(ctx context.Context, getter CloudSettingsGetter, hasher return !p.Status.IsSHAEqual(currentSha), currentSha, nil } +func (p *Provider) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&p.Status.Conditions, condition) +} + // ProviderList ... // +kubebuilder:object:root=true type ProviderList struct { diff --git a/controller/config/crd/examples/all_in_one_gcp.yaml b/controller/config/crd/examples/all_in_one_gcp.yaml index 31ee282d9..dc658fdca 100644 --- a/controller/config/crd/examples/all_in_one_gcp.yaml +++ b/controller/config/crd/examples/all_in_one_gcp.yaml @@ -36,29 +36,29 @@ spec: project: pluralsh-test-384515 tags: managed-by: plural-operator -#--- -#apiVersion: deployments.plural.sh/v1alpha1 -#kind: GitRepository -#metadata: -# name: k8s-helm -#spec: -# url: https://github.com/zreigz/k8s-helm.git -#--- -#apiVersion: deployments.plural.sh/v1alpha1 -#kind: ServiceDeployment -#metadata: -# name: zreigz-test -# namespace: operator -#spec: -# version: 0.0.1 -# git: -# folder: nginx -# ref: master -# repositoryRef: -# kind: GitRepository -# name: k8s-helm -# namespace: operator -# clusterRef: -# kind: Cluster -# name: gcp -# namespace: operator +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: k8s-helm +spec: + url: https://github.com/zreigz/k8s-helm.git +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: ServiceDeployment +metadata: + name: zreigz-test + namespace: operator +spec: + version: 0.0.1 + git: + folder: nginx + ref: master + repositoryRef: + kind: GitRepository + name: k8s-helm + namespace: operator + clusterRef: + kind: Cluster + name: gcp + namespace: operator diff --git a/controller/config/crd/examples/provider_gcp.yaml b/controller/config/crd/examples/provider_gcp.yaml index c5fe7af83..86c5d4e26 100644 --- a/controller/config/crd/examples/provider_gcp.yaml +++ b/controller/config/crd/examples/provider_gcp.yaml @@ -5,10 +5,10 @@ metadata: name: gcp spec: name: gcp - namespace: gcp-capi + namespace: operator cloud: gcp cloudSettings: gcp: name: credentials - namespace: gcp-capi + namespace: operator diff --git a/controller/pkg/reconciler/provider_reconciler.go b/controller/pkg/reconciler/provider_reconciler.go index c3f527990..3d08ef1cd 100644 --- a/controller/pkg/reconciler/provider_reconciler.go +++ b/controller/pkg/reconciler/provider_reconciler.go @@ -7,6 +7,7 @@ import ( console "github.com/pluralsh/console-client-go" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -41,7 +42,7 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques log.Info("Reconciling") // Read Provider CRD from the K8S API - var provider *v1alpha1.Provider + provider := new(v1alpha1.Provider) if err := r.Get(ctx, req.NamespacedName, provider); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -49,6 +50,7 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques scope, err := NewProviderScope(ctx, r.Client, provider) if err != nil { log.Error(err, "failed to create scope") + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } @@ -62,6 +64,7 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques // Check if resource already exists in the API and only sync the ID exists, err := r.isAlreadyExists(ctx, provider) if err != nil { + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if exists { @@ -76,6 +79,7 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques err = r.tryAddControllerRef(ctx, provider) if err != nil { + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } @@ -83,6 +87,7 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques changed, sha, err := provider.Diff(ctx, r.toCloudProviderSettingsAttributes, utils.HashObject) if err != nil { log.Error(err, "unable to calculate provider SHA") + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } @@ -90,12 +95,14 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques apiProvider, err := r.sync(ctx, provider, changed) if err != nil { log.Error(err, "unable to create or update provider") + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } provider.Status.ID = &apiProvider.ID provider.Status.SHA = &sha - //provider.Status.Existing = lo.ToPtr(false) + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadonlyConditionType, v1.ConditionFalse, v1alpha1.ReadonlyConditionReason, "") + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } @@ -103,11 +110,13 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Reques func (r *ProviderReconciler) handleExistingProvider(ctx context.Context, provider *v1alpha1.Provider) (reconcile.Result, error) { apiProvider, err := r.ConsoleClient.GetProviderByCloud(ctx, provider.Spec.Cloud) if err != nil { + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } provider.Status.ID = &apiProvider.ID - //provider.Status.Existing = lo.ToPtr(true) + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadonlyConditionType, v1.ConditionTrue, v1alpha1.ReadonlyConditionReason, v1alpha1.ReadonlyTrueConditionMessage.String()) + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } @@ -128,7 +137,6 @@ func (r *ProviderReconciler) isAlreadyExists(ctx context.Context, provider *v1al if !provider.Status.HasID() { log.FromContext(ctx).Info("Provider already exists in the API, running in read-only mode") - //utils.MarkCondition(setter, conditionType, conditionStatus, conditionReason, message) return true, nil } @@ -138,17 +146,13 @@ func (r *ProviderReconciler) isAlreadyExists(ctx context.Context, provider *v1al func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider *v1alpha1.Provider) (*ctrl.Result, error) { log := log.FromContext(ctx) - // If 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 - // to registering our finalizer. + // If object is not being deleted and if it does not have our finalizer, + // then lets add the finalizer. This is equivalent to registering our finalizer. if provider.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(provider, ProviderProtectionFinalizerName) { controllerutil.AddFinalizer(provider, ProviderProtectionFinalizerName) - //if err := r.Update(ctx, provider); err != nil { - // return &ctrl.Result{}, err - //} } - // If object is being deleted + // If object is being deleted cleanup and remove the finalizer. if !provider.ObjectMeta.DeletionTimestamp.IsZero() { // If object is already being deleted from Console API requeue if r.ConsoleClient.IsProviderDeleting(ctx, provider.Status.GetID()) { @@ -160,8 +164,9 @@ func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider if r.ConsoleClient.IsProviderExists(ctx, provider.Status.GetID()) { log.Info("Deleting provider") if err := r.ConsoleClient.DeleteProvider(ctx, provider.Status.GetID()); err != nil { - // if fail to delete the external dependency here, return with error + // if it fails to delete the external dependency here, return with error // so that it can be retried. + utils.MarkCondition(provider.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return &ctrl.Result{}, err } @@ -170,14 +175,6 @@ func (r *ProviderReconciler) addOrRemoveFinalizer(ctx context.Context, provider return &requeue, nil } - //// If our finalizer is present, remove it - //if controllerutil.ContainsFinalizer(provider, ProviderProtectionFinalizerName) { - // controllerutil.RemoveFinalizer(provider, ProviderProtectionFinalizerName) - // //if err := r.Update(ctx, provider); err != nil { - // // return &ctrl.Result{}, err - // //} - //} - // Stop reconciliation as the item is being deleted controllerutil.RemoveFinalizer(provider, ProviderProtectionFinalizerName) return &ctrl.Result{}, nil diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index def7b4bf8..e213ce506 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -205,6 +205,11 @@ func GetConfigMapData(ctx context.Context, client ctrlruntimeclient.Client, name return "", nil } -func MarkCondition(setter func(), conditionType v1alpha1.ConditionType, conditionStatus corev1.ConditionStatus, conditionReason v1alpha1.ConditionReason, message string) { - +func MarkCondition(set func(condition metav1.Condition), conditionType v1alpha1.ConditionType, conditionStatus metav1.ConditionStatus, conditionReason v1alpha1.ConditionReason, message string, messageArgs ...interface{}) { + set(metav1.Condition{ + Type: conditionType.String(), + Status: conditionStatus, + Reason: conditionReason.String(), + Message: fmt.Sprintf(message, messageArgs...), + }) } From 113401d2f1f7131db184ffaa87b76dea93ea7623 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 15 Dec 2023 12:39:38 +0100 Subject: [PATCH 129/198] add unit tests for services --- .../pkg/reconciler/service_reconciler.go | 3 - .../pkg/reconciler/service_reconciler_test.go | 293 ++++++++++++++++++ 2 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 controller/pkg/reconciler/service_reconciler_test.go diff --git a/controller/pkg/reconciler/service_reconciler.go b/controller/pkg/reconciler/service_reconciler.go index e41b80270..cd77877a3 100644 --- a/controller/pkg/reconciler/service_reconciler.go +++ b/controller/pkg/reconciler/service_reconciler.go @@ -94,9 +94,6 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err != nil { return ctrl.Result{}, err } - if err := utils.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { - return ctrl.Result{}, err - } } err = r.addOwnerReferences(ctx, service) if err != nil { diff --git a/controller/pkg/reconciler/service_reconciler_test.go b/controller/pkg/reconciler/service_reconciler_test.go new file mode 100644 index 000000000..154cd7ab1 --- /dev/null +++ b/controller/pkg/reconciler/service_reconciler_test.go @@ -0,0 +1,293 @@ +package reconciler_test + +import ( + "context" + "testing" + "time" + + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/reconciler" + "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func init() { + utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +} + +func TestCreateNewService(t *testing.T) { + const ( + serviceName = "test" + clusterName = "testCluster" + repoName = "testRepo" + ) + tests := []struct { + name string + service string + returnGetService *gqlclient.ServiceDeploymentExtended + returnIsClusterExisting bool + returnCreateCluster *gqlclient.CreateServiceDeployment + returnErrorCreateCluster error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ServiceStatus + }{ + { + name: "scenario 1: create a new service", + service: "test", + expectedStatus: v1alpha1.ServiceStatus{ + Id: lo.ToPtr("123"), + Sha: "E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ====", + }, + returnGetService: &gqlclient.ServiceDeploymentExtended{ + ID: "123", + }, + returnIsClusterExisting: false, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.ServiceDeployment{ + ObjectMeta: metav1.ObjectMeta{Name: serviceName}, + Spec: v1alpha1.ServiceSpec{ + Version: "1.24", + ClusterRef: corev1.ObjectReference{Name: clusterName}, + RepositoryRef: corev1.ObjectReference{Name: repoName}, + }, + }, + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + Status: v1alpha1.ClusterStatus{ + ID: lo.ToPtr("123"), + }, + }, + &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + }, + Status: v1alpha1.GitRepositoryStatus{ + Id: lo.ToPtr("123"), + Health: v1alpha1.GitHealthPullable, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() + fakeConsoleClient.On("CreateService", mock.Anything, mock.Anything).Return(nil, nil) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) + + ctx := context.Background() + + target := &reconciler.ServiceReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("reconcilers").WithName("ServiceReconciler"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) + assert.NoError(t, err) + + existingService := &v1alpha1.ServiceDeployment{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingService.Status) + }) + } +} + +func TestDeleteService(t *testing.T) { + const ( + serviceName = "test" + clusterName = "testCluster" + repoName = "testRepo" + ) + tests := []struct { + name string + service string + returnGetService *gqlclient.ServiceDeploymentExtended + returnIsClusterExisting bool + returnCreateCluster *gqlclient.CreateServiceDeployment + returnErrorCreateCluster error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ServiceStatus + }{ + { + name: "scenario 1: delete service", + service: "test", + expectedStatus: v1alpha1.ServiceStatus{ + Id: lo.ToPtr("123"), + Sha: "E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ====", + }, + returnGetService: &gqlclient.ServiceDeploymentExtended{ + ID: "123", + }, + returnIsClusterExisting: false, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.ServiceDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + DeletionTimestamp: &metav1.Time{Time: time.Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, + Finalizers: []string{reconciler.ServiceFinalizer}, + }, + Spec: v1alpha1.ServiceSpec{ + Version: "1.24", + ClusterRef: corev1.ObjectReference{Name: clusterName}, + RepositoryRef: corev1.ObjectReference{Name: repoName}, + }, + Status: v1alpha1.ServiceStatus{ + Id: lo.ToPtr("123"), + }, + }, + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + Status: v1alpha1.ClusterStatus{ + ID: lo.ToPtr("123"), + }, + }, + &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + }, + Status: v1alpha1.GitRepositoryStatus{ + Id: lo.ToPtr("123"), + Health: v1alpha1.GitHealthPullable, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() + ctx := context.Background() + + target := &reconciler.ServiceReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("reconcilers").WithName("ServiceReconciler"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) + assert.NoError(t, err) + + existingService := &v1alpha1.ServiceDeployment{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) + assert.True(t, apierrors.IsNotFound(err)) + }) + } +} + +func TestUpdateService(t *testing.T) { + const ( + serviceName = "test" + clusterName = "testCluster" + repoName = "testRepo" + ) + tests := []struct { + name string + service string + returnGetService *gqlclient.ServiceDeploymentExtended + returnIsClusterExisting bool + returnCreateCluster *gqlclient.CreateServiceDeployment + returnErrorCreateCluster error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.ServiceStatus + }{ + { + name: "scenario 1: create a new service", + service: "test", + expectedStatus: v1alpha1.ServiceStatus{ + Id: lo.ToPtr("123"), + Sha: "E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ====", + }, + returnGetService: &gqlclient.ServiceDeploymentExtended{ + ID: "123", + }, + returnIsClusterExisting: false, + existingObjects: []ctrlruntimeclient.Object{ + &v1alpha1.ServiceDeployment{ + ObjectMeta: metav1.ObjectMeta{Name: serviceName}, + Spec: v1alpha1.ServiceSpec{ + Version: "1.24", + ClusterRef: corev1.ObjectReference{Name: clusterName}, + RepositoryRef: corev1.ObjectReference{Name: repoName}, + }, + Status: v1alpha1.ServiceStatus{ + Id: lo.ToPtr("123"), + Sha: "abc", + }, + }, + &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + Status: v1alpha1.ClusterStatus{ + ID: lo.ToPtr("123"), + }, + }, + &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + }, + Status: v1alpha1.GitRepositoryStatus{ + Id: lo.ToPtr("123"), + Health: v1alpha1.GitHealthPullable, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() + + fakeConsoleClient := mocks.NewConsoleClient(t) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) + fakeConsoleClient.On("UpdateService", mock.Anything, mock.Anything).Return(nil) + + ctx := context.Background() + + target := &reconciler.ServiceReconciler{ + Client: fakeClient, + Log: ctrl.Log.WithName("reconcilers").WithName("ServiceReconciler"), + Scheme: scheme.Scheme, + ConsoleClient: fakeConsoleClient, + } + + _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) + assert.NoError(t, err) + + existingService := &v1alpha1.ServiceDeployment{} + err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) + assert.NoError(t, err) + assert.EqualValues(t, test.expectedStatus, existingService.Status) + }) + } +} From 96a8c85862a26568200814cfac5270c563bfcf17 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 15 Dec 2023 13:57:55 +0100 Subject: [PATCH 130/198] add cluster conditions --- .../apis/deployments/v1alpha1/cluster.go | 4 ++ .../pkg/reconciler/cluster_reconciler.go | 44 +++++++++---------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/apis/deployments/v1alpha1/cluster.go index 900ed5caf..6278260c1 100644 --- a/controller/apis/deployments/v1alpha1/cluster.go +++ b/controller/apis/deployments/v1alpha1/cluster.go @@ -39,6 +39,10 @@ type Cluster struct { Status ClusterStatus `json:"status,omitempty"` } +func (c *Cluster) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&c.Status.Conditions, condition) +} + func (c *Cluster) Attributes(providerId *string) console.ClusterAttributes { attrs := console.ClusterAttributes{ Name: c.Name, diff --git a/controller/pkg/reconciler/cluster_reconciler.go b/controller/pkg/reconciler/cluster_reconciler.go index 1222a4539..29df8a55f 100644 --- a/controller/pkg/reconciler/cluster_reconciler.go +++ b/controller/pkg/reconciler/cluster_reconciler.go @@ -6,7 +6,11 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -14,10 +18,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/utils" ) const ( @@ -53,6 +53,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request scope, err := NewClusterScope(ctx, r.Client, cluster) if err != nil { logger.Error(err, "Failed to create cluster scope") + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } defer func() { @@ -64,11 +65,12 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request // Handle existing resource. existing, err := r.isExisting(cluster) if err != nil { + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, fmt.Errorf("could not check if cluster is existing resource, got error: %+v", err) } if existing { logger.V(9).Info("Cluster already exists in the API, running in read-only mode") - return r.handleExisting(ctx, cluster) + return r.handleExisting(cluster) } // Handle resource deletion both in Kubernetes cluster and in Console API. @@ -79,18 +81,21 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request // Get Provider ID from the reference if it is set and ensure that controller reference is set properly. providerId, result, err := r.getProviderIdAndSetControllerRef(ctx, cluster) if result != nil { + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return *result, err } // Calculate SHA to detect changes that should be applied in the Console API. sha, err := utils.HashObject(cluster.UpdateAttributes()) if err != nil { + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } // Sync resource with Console API. apiCluster, err := r.sync(ctx, cluster, providerId, sha) if err != nil { + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } @@ -100,7 +105,8 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request cluster.Status.CurrentVersion = apiCluster.CurrentVersion cluster.Status.PingedAt = apiCluster.PingedAt cluster.Status.SHA = &sha - // TODO cluster.Status.Existing = lo.ToPtr(false) + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadonlyConditionType, v1.ConditionFalse, v1alpha1.ReadonlyConditionReason, "") + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } @@ -125,9 +131,10 @@ func (r *ClusterReconciler) isExisting(cluster *v1alpha1.Cluster) (bool, error) return !cluster.Status.HasID(), nil } -func (r *ClusterReconciler) handleExisting(ctx context.Context, cluster *v1alpha1.Cluster) (ctrl.Result, error) { +func (r *ClusterReconciler) handleExisting(cluster *v1alpha1.Cluster) (ctrl.Result, error) { apiCluster, err := r.ConsoleClient.GetClusterByHandle(cluster.Spec.Handle) if err != nil { + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } @@ -135,30 +142,20 @@ func (r *ClusterReconciler) handleExisting(ctx context.Context, cluster *v1alpha cluster.Status.KasURL = apiCluster.KasURL cluster.Status.CurrentVersion = apiCluster.CurrentVersion cluster.Status.PingedAt = apiCluster.PingedAt - // TODO c.Status.Existing = lo.ToPtr(true) - - //meta.SetStatusCondition(&cluster.Status.Conditions, v1.Condition{ - // Type: v1alpha1.ReadonlyConditionType.String(), - // Status: v1.ConditionTrue, - // Reason: v1alpha1.ReadonlyConditionReason.String(), - // Message: "Cluster already exists, running in read-only mode where only status will be updated and no changes in Console will be made", - //}) - // // Existing if set to true, then Console will not be synced with the data from this resource. - // // It can be used to read already existing resources. - // // +kubebuilder:validation:Optional - // Existing *bool `json:"existing,omitempty"` + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadonlyConditionType, v1.ConditionTrue, v1alpha1.ReadonlyConditionReason, v1alpha1.ReadonlyTrueConditionMessage.String()) + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } func (r *ClusterReconciler) addOrRemoveFinalizer(cluster *v1alpha1.Cluster) *ctrl.Result { - // If 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 to registering our finalizer. + /// If object is not being deleted and if it does not have our finalizer, + // then lets add the finalizer. This is equivalent to registering our finalizer. if cluster.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(cluster, FinalizerName) { controllerutil.AddFinalizer(cluster, FinalizerName) } - // If object is being deleted. + // If object is being deleted cleanup and remove the finalizer. if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { // If object is already being deleted from Console API requeue. if r.ConsoleClient.IsClusterDeleting(cluster.Status.ID) { @@ -168,8 +165,9 @@ func (r *ClusterReconciler) addOrRemoveFinalizer(cluster *v1alpha1.Cluster) *ctr // Remove Cluster from Console API if it exists. if r.ConsoleClient.IsClusterExisting(cluster.Status.ID) { if _, err := r.ConsoleClient.DeleteCluster(*cluster.Status.ID); err != nil { - // if fail to delete the external dependency here, return with error + // If it fails to delete the external dependency here, return with error // so that it can be retried. + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return &ctrl.Result{} } From a4714822668ff20b5c8caf6429c579085ad4dfa6 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 15 Dec 2023 14:08:16 +0100 Subject: [PATCH 131/198] update cluster tests --- .../pkg/reconciler/cluster_reconciler_test.go | 146 +++++++++++++++--- 1 file changed, 125 insertions(+), 21 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index f8e42b424..57140b4b5 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -52,9 +52,22 @@ func TestCreateNewCluster(t *testing.T) { name: "scenario 1: create a new AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, returnGetClusterByHandle: nil, returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), @@ -85,9 +98,22 @@ func TestCreateNewCluster(t *testing.T) { name: "scenario 2: create a new BYOK cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, returnGetClusterByHandle: nil, returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), @@ -156,9 +182,22 @@ func TestUpdateCluster(t *testing.T) { name: "scenario 1: update AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, returnIsClusterExisting: true, returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, @@ -172,9 +211,22 @@ func TestUpdateCluster(t *testing.T) { ProviderRef: &corev1.ObjectReference{Name: providerName}, }, Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, }, &v1alpha1.Provider{ @@ -192,9 +244,22 @@ func TestUpdateCluster(t *testing.T) { name: "scenario 2: update BYOK cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, returnIsClusterExisting: true, returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, @@ -206,9 +271,22 @@ func TestUpdateCluster(t *testing.T) { Cloud: "byok", }, Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, }, }, @@ -261,8 +339,21 @@ func TestAdoptExistingCluster(t *testing.T) { name: "scenario 1: adopt existing AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - Existing: lo.ToPtr(true), + ID: lo.ToPtr(clusterConsoleID), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, returnErrorGetClusterByHandle: nil, @@ -278,8 +369,21 @@ func TestAdoptExistingCluster(t *testing.T) { cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ ID: lo.ToPtr(clusterConsoleID), - Existing: lo.ToPtr(true), CurrentVersion: lo.ToPtr("1.24.11"), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: "", + Message: "", + }, + }, }, returnGetClusterByHandle: &gqlclient.ClusterFragment{ ID: clusterConsoleID, From 07a45cac1814e3b8cbca7da3d38b8aa1722f8d2c Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 15 Dec 2023 14:20:19 +0100 Subject: [PATCH 132/198] Fix provider unit tests --- .../pkg/reconciler/cluster_reconciler_test.go | 41 ++++----- .../reconciler/provider_reconciler_test.go | 91 +++++++++++++++---- 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index f8e42b424..582699f10 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -5,9 +5,6 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - "github.com/pluralsh/console/controller/pkg/reconciler" - "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -22,6 +19,10 @@ import ( ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/reconciler" + "github.com/pluralsh/console/controller/pkg/test/mocks" ) func init() { @@ -52,9 +53,8 @@ func TestCreateNewCluster(t *testing.T) { name: "scenario 1: create a new AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), }, returnGetClusterByHandle: nil, returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), @@ -85,9 +85,8 @@ func TestCreateNewCluster(t *testing.T) { name: "scenario 2: create a new BYOK cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), }, returnGetClusterByHandle: nil, returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), @@ -156,9 +155,8 @@ func TestUpdateCluster(t *testing.T) { name: "scenario 1: update AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), }, returnIsClusterExisting: true, returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, @@ -172,9 +170,8 @@ func TestUpdateCluster(t *testing.T) { ProviderRef: &corev1.ObjectReference{Name: providerName}, }, Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), }, }, &v1alpha1.Provider{ @@ -192,9 +189,8 @@ func TestUpdateCluster(t *testing.T) { name: "scenario 2: update BYOK cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), }, returnIsClusterExisting: true, returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, @@ -206,9 +202,8 @@ func TestUpdateCluster(t *testing.T) { Cloud: "byok", }, Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), }, }, }, @@ -261,8 +256,7 @@ func TestAdoptExistingCluster(t *testing.T) { name: "scenario 1: adopt existing AWS cluster", cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - Existing: lo.ToPtr(true), + ID: lo.ToPtr(clusterConsoleID), }, returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, returnErrorGetClusterByHandle: nil, @@ -278,7 +272,6 @@ func TestAdoptExistingCluster(t *testing.T) { cluster: clusterName, expectedStatus: v1alpha1.ClusterStatus{ ID: lo.ToPtr(clusterConsoleID), - Existing: lo.ToPtr(true), CurrentVersion: lo.ToPtr("1.24.11"), }, returnGetClusterByHandle: &gqlclient.ClusterFragment{ diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/pkg/reconciler/provider_reconciler_test.go index 39d939be8..1b061228a 100644 --- a/controller/pkg/reconciler/provider_reconciler_test.go +++ b/controller/pkg/reconciler/provider_reconciler_test.go @@ -31,6 +31,17 @@ func init() { utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) } +func sanitizeConditions(status v1alpha1.ProviderStatus) v1alpha1.ProviderStatus { + for i := range status.Conditions { + c := status.Conditions[i] + c.LastTransitionTime = metav1.Time{} + c.ObservedGeneration = 0 + status.Conditions[i] = c + } + + return status +} + func TestCreateNewProvider(t *testing.T) { test := struct { name string @@ -76,9 +87,22 @@ func TestCreateNewProvider(t *testing.T) { }, }, expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: "", + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + Message: "", + }, + }, }, } @@ -110,8 +134,8 @@ func TestCreateNewProvider(t *testing.T) { existingProvider := &v1alpha1.Provider{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - existingProviderStatusJson, err := json.Marshal(existingProvider.Status) - expectedStatusJson, err := json.Marshal(test.expectedStatus) + existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) + expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) assert.NoError(t, err) assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) @@ -160,8 +184,21 @@ func TestAdoptProvider(t *testing.T) { }, }, expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - Existing: lo.ToPtr(true), + ID: lo.ToPtr("1234"), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + Message: "", + }, + }, }, } @@ -191,8 +228,8 @@ func TestAdoptProvider(t *testing.T) { existingProvider := &v1alpha1.Provider{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - existingProviderStatusJson, err := json.Marshal(existingProvider.Status) - expectedStatusJson, err := json.Marshal(test.expectedStatus) + existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) + expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) assert.NoError(t, err) assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) @@ -231,9 +268,16 @@ func TestUpdateProvider(t *testing.T) { Namespace: "gcp", }, Status: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr(""), - Existing: lo.ToPtr(false), + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr(""), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: "", + }, + }, }, }, &corev1.Secret{ @@ -246,9 +290,22 @@ func TestUpdateProvider(t *testing.T) { }, }, expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: "", + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + Message: "", + }, + }, }, } @@ -279,8 +336,8 @@ func TestUpdateProvider(t *testing.T) { existingProvider := &v1alpha1.Provider{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - existingProviderStatusJson, err := json.Marshal(existingProvider.Status) - expectedStatusJson, err := json.Marshal(test.expectedStatus) + existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) + expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) assert.NoError(t, err) assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) From 6e475ec509a7d31dd2576aa8a8b91871c25ab9b6 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 15 Dec 2023 14:47:34 +0100 Subject: [PATCH 133/198] update cluster tests --- .../pkg/reconciler/cluster_reconciler.go | 2 +- .../pkg/reconciler/cluster_reconciler_test.go | 73 ++++++++++++------- .../reconciler/provider_reconciler_test.go | 18 ++--- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler.go b/controller/pkg/reconciler/cluster_reconciler.go index 29df8a55f..09be5da1d 100644 --- a/controller/pkg/reconciler/cluster_reconciler.go +++ b/controller/pkg/reconciler/cluster_reconciler.go @@ -216,7 +216,7 @@ func (r *ClusterReconciler) getProviderIdAndSetControllerRef(ctx context.Context return nil, &requeue, nil } - err = utils.TryAddControllerRef(ctx, r.Client, provider, cluster, r.Scheme) + err := controllerutil.SetOwnerReference(provider, cluster, r.Scheme) if err != nil { return nil, &ctrl.Result{}, fmt.Errorf("could not set cluster owner reference, got error: %+v", err) } diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 57140b4b5..5948598fb 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -2,6 +2,7 @@ package reconciler_test import ( "context" + "encoding/json" "testing" gqlclient "github.com/pluralsh/console-client-go" @@ -28,6 +29,15 @@ func init() { utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) } +func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + func TestCreateNewCluster(t *testing.T) { const ( clusterName = "test-cluster" @@ -58,13 +68,13 @@ func TestCreateNewCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionFalse, - Reason: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), Message: "", }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -104,13 +114,13 @@ func TestCreateNewCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionFalse, - Reason: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), Message: "", }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -154,8 +164,11 @@ func TestCreateNewCluster(t *testing.T) { existingCluster := &v1alpha1.Cluster{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) + existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) + expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) + assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingCluster.Status) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } @@ -188,13 +201,13 @@ func TestUpdateCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionFalse, - Reason: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), Message: "", }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -217,13 +230,13 @@ func TestUpdateCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionFalse, - Reason: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), Message: "", }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -250,13 +263,13 @@ func TestUpdateCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionFalse, - Reason: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), Message: "", }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -315,8 +328,12 @@ func TestUpdateCluster(t *testing.T) { existingCluster := &v1alpha1.Cluster{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) + + existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) + expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) + assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingCluster.Status) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } @@ -344,13 +361,13 @@ func TestAdoptExistingCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", - Message: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -374,13 +391,13 @@ func TestAdoptExistingCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", - Message: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, @@ -423,8 +440,12 @@ func TestAdoptExistingCluster(t *testing.T) { existingCluster := &v1alpha1.Cluster{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) + + existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) + expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) + assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingCluster.Status) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/pkg/reconciler/provider_reconciler_test.go index 39d939be8..6f4ecffdd 100644 --- a/controller/pkg/reconciler/provider_reconciler_test.go +++ b/controller/pkg/reconciler/provider_reconciler_test.go @@ -76,9 +76,8 @@ func TestCreateNewProvider(t *testing.T) { }, }, expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), }, } @@ -160,8 +159,7 @@ func TestAdoptProvider(t *testing.T) { }, }, expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - Existing: lo.ToPtr(true), + ID: lo.ToPtr("1234"), }, } @@ -231,9 +229,8 @@ func TestUpdateProvider(t *testing.T) { Namespace: "gcp", }, Status: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr(""), - Existing: lo.ToPtr(false), + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr(""), }, }, &corev1.Secret{ @@ -246,9 +243,8 @@ func TestUpdateProvider(t *testing.T) { }, }, expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Existing: lo.ToPtr(false), + ID: lo.ToPtr("1234"), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), }, } From 4da5db5273d3f768865d12fed62a01fa84771ec0 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 15 Dec 2023 14:53:59 +0100 Subject: [PATCH 134/198] update cluster tests --- controller/pkg/reconciler/cluster_reconciler_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 5948598fb..221cb046c 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -290,13 +290,13 @@ func TestUpdateCluster(t *testing.T) { { Type: v1alpha1.ReadonlyConditionType.String(), Status: metav1.ConditionFalse, - Reason: "", + Reason: v1alpha1.ReadonlyConditionReason.String(), Message: "", }, { - Type: v1alpha1.ReadonlyConditionType.String(), + Type: v1alpha1.ReadyConditionType.String(), Status: metav1.ConditionTrue, - Reason: "", + Reason: v1alpha1.ReadyConditionReason.String(), Message: "", }, }, From ef785e96bf3673435f1302e67f8b201a9a2ff64d Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Fri, 15 Dec 2023 14:54:26 +0100 Subject: [PATCH 135/198] simplify unit tests --- .../pkg/reconciler/cluster_reconciler_test.go | 7 +-- .../reconciler/provider_reconciler_test.go | 48 ++++++++----------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 57140b4b5..6774740ad 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -5,9 +5,6 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" - "github.com/pluralsh/console/controller/pkg/reconciler" - "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -22,6 +19,10 @@ import ( ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/pkg/reconciler" + "github.com/pluralsh/console/controller/pkg/test/mocks" ) func init() { diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/pkg/reconciler/provider_reconciler_test.go index 1b061228a..c18ee1ff8 100644 --- a/controller/pkg/reconciler/provider_reconciler_test.go +++ b/controller/pkg/reconciler/provider_reconciler_test.go @@ -33,10 +33,8 @@ func init() { func sanitizeConditions(status v1alpha1.ProviderStatus) v1alpha1.ProviderStatus { for i := range status.Conditions { - c := status.Conditions[i] - c.LastTransitionTime = metav1.Time{} - c.ObservedGeneration = 0 - status.Conditions[i] = c + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 } return status @@ -91,16 +89,14 @@ func TestCreateNewProvider(t *testing.T) { SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -193,10 +189,9 @@ func TestAdoptProvider(t *testing.T) { Message: v1alpha1.ReadonlyTrueConditionMessage.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -272,10 +267,9 @@ func TestUpdateProvider(t *testing.T) { SHA: lo.ToPtr(""), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, }, }, @@ -294,16 +288,14 @@ func TestUpdateProvider(t *testing.T) { SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, From 569602d5a41343f83407da113a95cae0cf99042b Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Fri, 15 Dec 2023 16:53:11 +0100 Subject: [PATCH 136/198] update cluster tests --- .../pkg/reconciler/cluster_reconciler_test.go | 98 ++++++++----------- 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index 221cb046c..614a7b6c6 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -66,16 +66,14 @@ func TestCreateNewCluster(t *testing.T) { SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -112,16 +110,14 @@ func TestCreateNewCluster(t *testing.T) { SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -199,16 +195,14 @@ func TestUpdateCluster(t *testing.T) { SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -228,16 +222,14 @@ func TestUpdateCluster(t *testing.T) { SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -261,16 +253,14 @@ func TestUpdateCluster(t *testing.T) { SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -288,16 +278,14 @@ func TestUpdateCluster(t *testing.T) { SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), Conditions: []metav1.Condition{ { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -365,10 +353,9 @@ func TestAdoptExistingCluster(t *testing.T) { Message: v1alpha1.ReadonlyTrueConditionMessage.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, @@ -395,10 +382,9 @@ func TestAdoptExistingCluster(t *testing.T) { Message: v1alpha1.ReadonlyTrueConditionMessage.String(), }, { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - Message: "", + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, }, From 0cc20d8d425ffb5220cbd4e0431b78aece85eccc Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Mon, 18 Dec 2023 11:30:32 +0100 Subject: [PATCH 137/198] add git repo conditions --- .../deployments/v1alpha1/git_repository.go | 66 ++++++++-- .../v1alpha1/zz_generated.deepcopy.go | 17 ++- ...deployments.plural.sh_gitrepositories.yaml | 81 ++++++++++-- .../reconciler/git_repository_reconciler.go | 117 +++++++++--------- .../git_repository_reconciler_scope.go | 42 +++++++ .../git_repository_reconciler_test.go | 100 +++++++++++---- .../pkg/reconciler/service_reconciler.go | 6 +- .../pkg/reconciler/service_reconciler_test.go | 10 +- controller/pkg/types/reconciler.go | 2 - 9 files changed, 318 insertions(+), 123 deletions(-) create mode 100644 controller/pkg/reconciler/git_repository_reconciler_scope.go diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/apis/deployments/v1alpha1/git_repository.go index 9151cd828..886d0e445 100644 --- a/controller/apis/deployments/v1alpha1/git_repository.go +++ b/controller/apis/deployments/v1alpha1/git_repository.go @@ -2,6 +2,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -33,17 +34,20 @@ type GitRepositoryStatus struct { // Message indicating details about last transition. // +optional Message *string `json:"message,omitempty"` - // Id of repo in console. - // +optional - Id *string `json:"id,omitempty"` - // +optional - Sha string `json:"sha,omitempty"` - // Existing flag is set to true when Console API object already exists when CRD is created. - // CRD is then set to read-only mode and does not update Console API from CRD. + // ID of the provider in the Console API. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + ID *string `json:"id,omitempty"` + // SHA of last applied configuration. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Type:=boolean - // +kubebuilder:default:=false - Existing *bool `json:"existing,omitempty"` + // +kubebuilder:validation:Type:=string + SHA *string `json:"sha,omitempty"` + // Represents the observations of Repository current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // +kubebuilder:object:root=true @@ -67,10 +71,46 @@ type GitRepositoryList struct { Items []GitRepository `json:"items"` } -func (p *GitRepositoryStatus) IsExisting() bool { - return p.Existing != nil && *p.Existing +func (p *GitRepositoryStatus) HasReadonlyCondition() bool { + return meta.FindStatusCondition(p.Conditions, ReadonlyConditionType.String()) != nil +} + +func (p *GitRepositoryStatus) IsReadonly() bool { + return meta.IsStatusConditionTrue(p.Conditions, ReadonlyConditionType.String()) +} + +func (p *GitRepositoryStatus) IsSHAEqual(sha string) bool { + if !p.HasSHA() { + return false + } + + return p.GetSHA() == sha +} + +func (p *GitRepositoryStatus) GetSHA() string { + if !p.HasSHA() { + return "" + } + + return *p.SHA +} + +func (p *GitRepositoryStatus) HasSHA() bool { + return p.SHA != nil && len(*p.SHA) > 0 +} + +func (p *GitRepositoryStatus) GetID() string { + if !p.HasID() { + return "" + } + + return *p.ID } func (p *GitRepositoryStatus) HasID() bool { - return p.Id != nil && len(*p.Id) > 0 + return p.ID != nil && len(*p.ID) > 0 +} + +func (p *GitRepository) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&p.Status.Conditions, condition) } diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go index 003edec32..e35021985 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go @@ -515,16 +515,23 @@ func (in *GitRepositoryStatus) DeepCopyInto(out *GitRepositoryStatus) { *out = new(string) **out = **in } - if in.Id != nil { - in, out := &in.Id, &out.Id + if in.ID != nil { + in, out := &in.ID, &out.ID *out = new(string) **out = **in } - if in.Existing != nil { - in, out := &in.Existing, &out.Existing - *out = new(bool) + if in.SHA != nil { + in, out := &in.SHA, &out.SHA + *out = new(string) **out = **in } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositoryStatus. diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index cc4ccb3d1..e642a1f69 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -66,12 +66,78 @@ spec: type: object status: properties: - existing: - default: false - description: Existing flag is set to true when Console API object - already exists when CRD is created. CRD is then set to read-only - mode and does not update Console API from CRD. - type: boolean + conditions: + description: Represents the observations of Repository current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map health: description: Health status. enum: @@ -79,12 +145,13 @@ spec: - FAILED type: string id: - description: Id of repo in console. + description: ID of the provider in the Console API. type: string message: description: Message indicating details about last transition. type: string sha: + description: SHA of last applied configuration. type: string type: object type: object diff --git a/controller/pkg/reconciler/git_repository_reconciler.go b/controller/pkg/reconciler/git_repository_reconciler.go index bc9d359ba..ef398f2e1 100644 --- a/controller/pkg/reconciler/git_repository_reconciler.go +++ b/controller/pkg/reconciler/git_repository_reconciler.go @@ -4,10 +4,6 @@ import ( "context" "fmt" - "github.com/samber/lo" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" @@ -15,12 +11,14 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( @@ -42,11 +40,10 @@ type GitRepoCred struct { type GitRepositoryReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log logr.Logger Scheme *runtime.Scheme } -func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { logger := log.FromContext(ctx) repo := &v1alpha1.GitRepository{} if err := r.Get(ctx, req.NamespacedName, repo); err != nil { @@ -56,9 +53,24 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } + scope, err := NewGitRepositoryScope(ctx, r.Client, repo) + if err != nil { + logger.Error(err, "failed to create scope") + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + // Check if resource already exists in the API and only sync the ID exists, err := r.isAlreadyExists(repo) if err != nil { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if exists { @@ -70,36 +82,31 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques } cred, err := r.getRepositoryCredentials(ctx, repo) if err != nil { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } sha, err := utils.HashObject(cred) if err != nil { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } existingRepo, err := r.getRepository(repo.Spec.Url) if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if existingRepo == nil { - if err := utils.TryAddFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { - return ctrl.Result{}, err - } + controllerutil.AddFinalizer(repo, RepoFinalizer) resp, err := r.ConsoleClient.CreateRepository(repo.Spec.Url, cred.PrivateKey, cred.Passphrase, cred.Username, cred.Password) if err != nil { - return ctrl.Result{}, err - } - if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Existing = lo.ToPtr(false) - return original.Status, r.Status - }); err != nil { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } logger.Info("repository created") existingRepo = resp.CreateGitRepository - } - if repo.Status.Sha != "" && repo.Status.Sha != sha { + if repo.Status.HasSHA() && !repo.Status.IsSHAEqual(sha) { _, err := r.ConsoleClient.UpdateRepository(existingRepo.ID, console.GitAttributes{ URL: repo.Spec.Url, PrivateKey: cred.PrivateKey, @@ -108,24 +115,21 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques Password: cred.Password, }) if err != nil { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } logger.Info("repository updated") } - if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Message = existingRepo.Error - r.Status.Id = &existingRepo.ID - if existingRepo.Health != nil { - r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) - } - r.Status.Sha = sha - r.Status.Existing = lo.ToPtr(false) - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err + repo.Status.Message = existingRepo.Error + repo.Status.ID = &existingRepo.ID + if existingRepo.Health != nil { + repo.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } + repo.Status.SHA = &sha + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadonlyConditionType, v1.ConditionFalse, v1alpha1.ReadonlyConditionReason, "") + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } @@ -140,27 +144,28 @@ func (r *GitRepositoryReconciler) handleDelete(ctx context.Context, repo *v1alph logger := log.FromContext(ctx) if controllerutil.ContainsFinalizer(repo, RepoFinalizer) { logger.Info("delete git repository") - if repo.Status.Id == nil { - return ctrl.Result{}, fmt.Errorf("the repoository ID can not be nil") + if repo.Status.ID == nil { + idError := fmt.Errorf("the repoository ID can not be nil") + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, idError.Error()) + return ctrl.Result{}, idError } - existingRepos, err := r.getRepository(repo.Spec.Url) - if err != nil { - if !errors.IsNotFound(err) { - return ctrl.Result{}, err - } + existingRepo, err := r.getRepository(repo.Spec.Url) + if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err } - if existingRepos != nil { - if err := r.ConsoleClient.DeleteRepository(*repo.Status.Id); err != nil { + + if existingRepo != nil { + if err := r.ConsoleClient.DeleteRepository(*repo.Status.ID); err != nil { if !errors.IsDeleteRepository(err) { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } logger.Info("waiting for the services") return requeue, nil } } - if err := utils.TryRemoveFinalizer(ctx, r.Client, repo, RepoFinalizer); err != nil { - return ctrl.Result{}, err - } + controllerutil.RemoveFinalizer(repo, RepoFinalizer) } return ctrl.Result{}, nil } @@ -210,8 +215,8 @@ func (r *GitRepositoryReconciler) getRepository(url string) (*console.GitReposit } func (r *GitRepositoryReconciler) isAlreadyExists(repository *v1alpha1.GitRepository) (bool, error) { - if repository.Status.IsExisting() { - return true, nil + if repository.Status.HasReadonlyCondition() { + return repository.Status.IsReadonly(), nil } repo, err := r.getRepository(repository.Spec.Url) @@ -230,31 +235,25 @@ func (r *GitRepositoryReconciler) handleExistingRepo(ctx context.Context, repo * logger := log.FromContext(ctx) existingRepo, err := r.getRepository(repo.Spec.Url) if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if existingRepo == nil { msg := "existing Git repository was deleted from the console" logger.Info(msg) - if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Message = &msg - r.Status.Health = v1alpha1.GitHealthFailed - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err - } + repo.Status.Message = &msg + repo.Status.Health = v1alpha1.GitHealthFailed + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, msg) return requeue, nil } - if err = utils.TryUpdateStatus[*v1alpha1.GitRepository](ctx, r.Client, repo, func(r *v1alpha1.GitRepository, original *v1alpha1.GitRepository) (any, any) { - r.Status.Message = existingRepo.Error - r.Status.Id = &existingRepo.ID - if existingRepo.Health != nil { - r.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) - } - r.Status.Existing = lo.ToPtr(true) - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err + repo.Status.Message = existingRepo.Error + repo.Status.ID = &existingRepo.ID + if existingRepo.Health != nil { + repo.Status.Health = v1alpha1.GitHealth(*existingRepo.Health) } + + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadonlyConditionType, v1.ConditionTrue, v1alpha1.ReadonlyConditionReason, v1alpha1.ReadonlyTrueConditionMessage.String()) + utils.MarkCondition(repo.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } diff --git a/controller/pkg/reconciler/git_repository_reconciler_scope.go b/controller/pkg/reconciler/git_repository_reconciler_scope.go new file mode 100644 index 000000000..a9f683154 --- /dev/null +++ b/controller/pkg/reconciler/git_repository_reconciler_scope.go @@ -0,0 +1,42 @@ +package reconciler + +import ( + "context" + "errors" + "fmt" + + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" +) + +type GitRepositoryScope struct { + Client client.Client + GitRepository *v1alpha1.GitRepository + + ctx context.Context + patchHelper *patch.Helper +} + +func (p *GitRepositoryScope) PatchObject() error { + return p.patchHelper.Patch(p.ctx, p.GitRepository) +} + +func NewGitRepositoryScope(ctx context.Context, client client.Client, repository *v1alpha1.GitRepository) (*GitRepositoryScope, error) { + if repository == nil { + return nil, errors.New("failed to create new GitRepositoryScope from nil GitRepository") + } + + helper, err := patch.NewHelper(repository, client) + if err != nil { + return nil, fmt.Errorf("failed to init patch helper: %s", err) + } + + return &GitRepositoryScope{ + Client: client, + GitRepository: repository, + ctx: ctx, + patchHelper: helper, + }, nil +} diff --git a/controller/pkg/reconciler/git_repository_reconciler_test.go b/controller/pkg/reconciler/git_repository_reconciler_test.go index 727cf784b..d56d28623 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_test.go +++ b/controller/pkg/reconciler/git_repository_reconciler_test.go @@ -2,6 +2,7 @@ package reconciler_test import ( "context" + "encoding/json" "testing" gqlclient "github.com/pluralsh/console-client-go" @@ -17,7 +18,6 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -42,11 +42,20 @@ func TestCreateNewRepository(t *testing.T) { name: "scenario 1: create a new repository", repository: "test", expectedStatus: v1alpha1.GitRepositoryStatus{ - Health: "", - Message: nil, - Id: lo.ToPtr("123"), - Sha: "TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA====", - Existing: lo.ToPtr(false), + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, }, returnGetRepository: &gqlclient.GetGitRepository{ GitRepository: nil, @@ -93,7 +102,6 @@ func TestCreateNewRepository(t *testing.T) { ctx := context.Background() target := &reconciler.GitRepositoryReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -105,7 +113,12 @@ func TestCreateNewRepository(t *testing.T) { existingRepo := &v1alpha1.GitRepository{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingRepo.Status) + existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) + assert.NoError(t, err) + expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) + assert.NoError(t, err) + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } @@ -123,11 +136,20 @@ func TestUpdateRepository(t *testing.T) { name: "scenario 1: update credentials", repository: "test", expectedStatus: v1alpha1.GitRepositoryStatus{ - Health: "", - Message: nil, - Id: lo.ToPtr("123"), - Sha: "TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA====", - Existing: lo.ToPtr(false), + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, }, returnGetRepository: &gqlclient.GetGitRepository{ GitRepository: &gqlclient.GitRepositoryFragment{ @@ -147,11 +169,10 @@ func TestUpdateRepository(t *testing.T) { }, }, Status: v1alpha1.GitRepositoryStatus{ - Health: "", - Message: nil, - Id: lo.ToPtr("123"), - Sha: "ABC", - Existing: lo.ToPtr(false), + Health: "", + Message: nil, + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("ABC"), }, }, &corev1.Secret{ @@ -179,7 +200,6 @@ func TestUpdateRepository(t *testing.T) { ctx := context.Background() target := &reconciler.GitRepositoryReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -191,7 +211,12 @@ func TestUpdateRepository(t *testing.T) { existingRepo := &v1alpha1.GitRepository{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingRepo.Status) + existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) + assert.NoError(t, err) + expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) + assert.NoError(t, err) + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } @@ -209,10 +234,20 @@ func TestImportRepository(t *testing.T) { name: "scenario 1: update credentials", repository: "test", expectedStatus: v1alpha1.GitRepositoryStatus{ - Health: "", - Message: nil, - Id: lo.ToPtr("123"), - Existing: lo.ToPtr(true), + ID: lo.ToPtr("123"), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, }, returnGetRepository: &gqlclient.GetGitRepository{ GitRepository: &gqlclient.GitRepositoryFragment{ @@ -248,7 +283,6 @@ func TestImportRepository(t *testing.T) { ctx := context.Background() target := &reconciler.GitRepositoryReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("controllers").WithName("GitRepoController"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -259,7 +293,21 @@ func TestImportRepository(t *testing.T) { existingRepo := &v1alpha1.GitRepository{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingRepo.Status) + existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) + assert.NoError(t, err) + expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) + assert.NoError(t, err) + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } + +func sanitizeRepoConditions(status v1alpha1.GitRepositoryStatus) v1alpha1.GitRepositoryStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} diff --git a/controller/pkg/reconciler/service_reconciler.go b/controller/pkg/reconciler/service_reconciler.go index cd77877a3..10afec700 100644 --- a/controller/pkg/reconciler/service_reconciler.go +++ b/controller/pkg/reconciler/service_reconciler.go @@ -5,7 +5,6 @@ import ( "encoding/json" "sort" - "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" @@ -29,7 +28,6 @@ const ( type ServiceReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log logr.Logger Scheme *runtime.Scheme } @@ -64,7 +62,7 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return requeue, nil } - if repository.Status.Id == nil { + if repository.Status.ID == nil { log.Info("Repository is not ready") return requeue, nil } @@ -73,7 +71,7 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return requeue, nil } - attr, err := r.genServiceAttributes(ctx, service, repository.Status.Id) + attr, err := r.genServiceAttributes(ctx, service, repository.Status.ID) if err != nil { return ctrl.Result{}, err } diff --git a/controller/pkg/reconciler/service_reconciler_test.go b/controller/pkg/reconciler/service_reconciler_test.go index 154cd7ab1..f298b6486 100644 --- a/controller/pkg/reconciler/service_reconciler_test.go +++ b/controller/pkg/reconciler/service_reconciler_test.go @@ -18,7 +18,6 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -77,7 +76,7 @@ func TestCreateNewService(t *testing.T) { Name: repoName, }, Status: v1alpha1.GitRepositoryStatus{ - Id: lo.ToPtr("123"), + ID: lo.ToPtr("123"), Health: v1alpha1.GitHealthPullable, }, }, @@ -98,7 +97,6 @@ func TestCreateNewService(t *testing.T) { target := &reconciler.ServiceReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("reconcilers").WithName("ServiceReconciler"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -170,7 +168,7 @@ func TestDeleteService(t *testing.T) { Name: repoName, }, Status: v1alpha1.GitRepositoryStatus{ - Id: lo.ToPtr("123"), + ID: lo.ToPtr("123"), Health: v1alpha1.GitHealthPullable, }, }, @@ -188,7 +186,6 @@ func TestDeleteService(t *testing.T) { target := &reconciler.ServiceReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("reconcilers").WithName("ServiceReconciler"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -256,7 +253,7 @@ func TestUpdateService(t *testing.T) { Name: repoName, }, Status: v1alpha1.GitRepositoryStatus{ - Id: lo.ToPtr("123"), + ID: lo.ToPtr("123"), Health: v1alpha1.GitHealthPullable, }, }, @@ -276,7 +273,6 @@ func TestUpdateService(t *testing.T) { target := &reconciler.ServiceReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("reconcilers").WithName("ServiceReconciler"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 6cc3345ad..7a57711b5 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -40,14 +40,12 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.Console case GitRepositoryReconciler: return &reconciler.GitRepositoryReconciler{ Client: mgr.GetClient(), - Log: mgr.GetLogger(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ServiceDeploymentReconciler: return &reconciler.ServiceReconciler{ Client: mgr.GetClient(), - Log: mgr.GetLogger(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil From bfe0803f7eff0b41cab3b5bc85de2b86dd954628 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 11:55:37 +0100 Subject: [PATCH 138/198] rename apis to api update k8s dependencies regenerate files add PROJECT file --- controller/Makefile | 5 +- controller/PROJECT | 16 ++ .../deployments/v1alpha1/cluster.go | 0 .../deployments/v1alpha1/common.go | 0 .../{apis => api}/deployments/v1alpha1/doc.go | 0 .../deployments/v1alpha1/git_repository.go | 0 .../deployments/v1alpha1/groupversion_info.go | 2 +- .../deployments/v1alpha1/provider.go | 0 .../deployments/v1alpha1/register.go | 0 .../deployments/v1alpha1/service.go | 0 .../v1alpha1/zz_generated.deepcopy.go | 1 - .../bases/deployments.plural.sh_clusters.yaml | 10 +- ...deployments.plural.sh_gitrepositories.yaml | 3 +- .../deployments.plural.sh_providers.yaml | 6 +- ...loyments.plural.sh_servicedeployments.yaml | 3 +- controller/go.mod | 88 +++---- controller/go.sum | 230 ++++++++---------- controller/main.go | 2 +- controller/pkg/client/console.go | 2 +- controller/pkg/client/provider.go | 2 +- .../pkg/reconciler/cluster_reconciler.go | 2 +- .../reconciler/cluster_reconciler_scope.go | 2 +- .../pkg/reconciler/cluster_reconciler_test.go | 2 +- .../reconciler/git_repository_reconciler.go | 2 +- .../git_repository_reconciler_test.go | 2 +- .../pkg/reconciler/provider_reconciler.go | 2 +- .../provider_reconciler_attributes.go | 2 +- .../reconciler/provider_reconciler_scope.go | 2 +- .../reconciler/provider_reconciler_test.go | 2 +- .../pkg/reconciler/service_reconciler.go | 2 +- .../pkg/reconciler/service_reconciler_test.go | 2 +- controller/pkg/test/mocks/ConsoleClient.go | 2 +- controller/pkg/utils/kubernetes.go | 2 +- 33 files changed, 201 insertions(+), 195 deletions(-) create mode 100644 controller/PROJECT rename controller/{apis => api}/deployments/v1alpha1/cluster.go (100%) rename controller/{apis => api}/deployments/v1alpha1/common.go (100%) rename controller/{apis => api}/deployments/v1alpha1/doc.go (100%) rename controller/{apis => api}/deployments/v1alpha1/git_repository.go (100%) rename controller/{apis => api}/deployments/v1alpha1/groupversion_info.go (92%) rename controller/{apis => api}/deployments/v1alpha1/provider.go (100%) rename controller/{apis => api}/deployments/v1alpha1/register.go (100%) rename controller/{apis => api}/deployments/v1alpha1/service.go (100%) rename controller/{apis => api}/deployments/v1alpha1/zz_generated.deepcopy.go (99%) diff --git a/controller/Makefile b/controller/Makefile index ff0e9c32a..796155596 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -1,5 +1,6 @@ IMG ?= deployment-controller:latest # Image URL to use all building/pushing image targets CONTROLLER_GEN = $(shell which controller-gen) +CRD_OPTIONS ?= "crd" ifndef GOPATH $(error $$GOPATH environment variable not set) @@ -48,12 +49,12 @@ docker-push: ## push Docker image with the driver .PHONY: manifests manifests: install-tools ## generate Kubernetes manifests - $(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: install-tools ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations go generate ./pkg/... - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/... .PHONY: genmock genmock: ## generates mocks before running tests diff --git a/controller/PROJECT b/controller/PROJECT new file mode 100644 index 000000000..712c2b92b --- /dev/null +++ b/controller/PROJECT @@ -0,0 +1,16 @@ +version: "2" +domain: plural.sh +repo: pluralsh/console +resources: +- group: deployments + version: v1alpha1 + kind: Cluster +- group: deployments + version: v1alpha1 + kind: Provider +- group: deployments + version: v1alpha1 + kind: ServiceDeployment +- group: deployments + version: v1alpha1 + kind: GitRepository \ No newline at end of file diff --git a/controller/apis/deployments/v1alpha1/cluster.go b/controller/api/deployments/v1alpha1/cluster.go similarity index 100% rename from controller/apis/deployments/v1alpha1/cluster.go rename to controller/api/deployments/v1alpha1/cluster.go diff --git a/controller/apis/deployments/v1alpha1/common.go b/controller/api/deployments/v1alpha1/common.go similarity index 100% rename from controller/apis/deployments/v1alpha1/common.go rename to controller/api/deployments/v1alpha1/common.go diff --git a/controller/apis/deployments/v1alpha1/doc.go b/controller/api/deployments/v1alpha1/doc.go similarity index 100% rename from controller/apis/deployments/v1alpha1/doc.go rename to controller/api/deployments/v1alpha1/doc.go diff --git a/controller/apis/deployments/v1alpha1/git_repository.go b/controller/api/deployments/v1alpha1/git_repository.go similarity index 100% rename from controller/apis/deployments/v1alpha1/git_repository.go rename to controller/api/deployments/v1alpha1/git_repository.go diff --git a/controller/apis/deployments/v1alpha1/groupversion_info.go b/controller/api/deployments/v1alpha1/groupversion_info.go similarity index 92% rename from controller/apis/deployments/v1alpha1/groupversion_info.go rename to controller/api/deployments/v1alpha1/groupversion_info.go index 42f945438..ab6dcfeb4 100644 --- a/controller/apis/deployments/v1alpha1/groupversion_info.go +++ b/controller/api/deployments/v1alpha1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1alpha1 contains API Schema definitions for the bootstrap v1alpha1 API group +// Package v1alpha1 contains API Schema definitions for the deployments v1alpha1 API group // +kubebuilder:object:generate=true // +groupName=deployments.plural.sh package v1alpha1 diff --git a/controller/apis/deployments/v1alpha1/provider.go b/controller/api/deployments/v1alpha1/provider.go similarity index 100% rename from controller/apis/deployments/v1alpha1/provider.go rename to controller/api/deployments/v1alpha1/provider.go diff --git a/controller/apis/deployments/v1alpha1/register.go b/controller/api/deployments/v1alpha1/register.go similarity index 100% rename from controller/apis/deployments/v1alpha1/register.go rename to controller/api/deployments/v1alpha1/register.go diff --git a/controller/apis/deployments/v1alpha1/service.go b/controller/api/deployments/v1alpha1/service.go similarity index 100% rename from controller/apis/deployments/v1alpha1/service.go rename to controller/api/deployments/v1alpha1/service.go diff --git a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go similarity index 99% rename from controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go rename to controller/api/deployments/v1alpha1/zz_generated.deepcopy.go index 003edec32..a9dac298b 100644 --- a/controller/apis/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2023. diff --git a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml index 6c20b46a7..22a48d682 100644 --- a/controller/config/crd/bases/deployments.plural.sh_clusters.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_clusters.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: clusters.deployments.plural.sh spec: group: deployments.plural.sh @@ -80,6 +79,7 @@ spec: - azure - gcp - byok + example: azure type: string x-kubernetes-validations: - message: Cloud is immutable @@ -102,14 +102,17 @@ spec: properties: location: description: Location in Azure to deploy this cluster to. + example: eastus type: string network: description: Network is a name for the Azure virtual network for this cluster. + example: mynetwork type: string resourceGroup: description: ResourceGroup is a name for the Azure resource group for this cluster. + example: myresourcegroup type: string subscriptionId: description: SubscriptionId is GUID of the Azure subscription @@ -144,6 +147,7 @@ spec: description: Handle is a short, unique human-readable name used to identify this cluster. Does not necessarily map to the cloud resource name. This has to be specified in order to adopt existing cluster. + example: myclusterhandle type: string nodePools: description: NodePools contains specs of node pools managed by this @@ -223,6 +227,7 @@ spec: type: array protect: description: Protect cluster from being deleted. + example: false type: boolean providerRef: description: ProviderRef references provider to use for this cluster. @@ -270,6 +275,7 @@ spec: version: description: Version of Kubernetes to use for this cluster. Can be skipped only for BYOK. + example: 1.25.11 type: string type: object status: diff --git a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml index cc4ccb3d1..d0bd006c3 100644 --- a/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_gitrepositories.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: gitrepositories.deployments.plural.sh spec: group: deployments.plural.sh diff --git a/controller/config/crd/bases/deployments.plural.sh_providers.yaml b/controller/config/crd/bases/deployments.plural.sh_providers.yaml index e973fa2d3..e0847342f 100644 --- a/controller/config/crd/bases/deployments.plural.sh_providers.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_providers.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: providers.deployments.plural.sh spec: group: deployments.plural.sh @@ -55,6 +54,7 @@ spec: - gcp - aws - azure + example: aws type: string x-kubernetes-validations: - message: Cloud is immutable @@ -110,6 +110,7 @@ spec: x-kubernetes-map-type: atomic name: description: Name is a human-readable name of the Provider. + example: gcp-provider type: string x-kubernetes-validations: - message: Name is immutable @@ -117,6 +118,7 @@ spec: namespace: description: Namespace is the namespace ClusterAPI resources are deployed into. + example: capi-gcp type: string x-kubernetes-validations: - message: Namespace is immutable diff --git a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index 373c3c805..2f76a9284 100644 --- a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: servicedeployments.deployments.plural.sh spec: group: deployments.plural.sh diff --git a/controller/go.mod b/controller/go.mod index 6e6ebb2b6..259003798 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -4,25 +4,31 @@ go 1.21 toolchain go1.21.1 +// Dependencies require ( github.com/Yamashou/gqlgenc v0.16.0 - github.com/go-logr/logr v1.2.4 - github.com/golangci/golangci-lint v1.55.1 + github.com/go-logr/logr v1.3.0 github.com/pluralsh/console-client-go v0.0.55 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.39.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - go.uber.org/zap v1.24.0 - k8s.io/api v0.26.10 - k8s.io/apimachinery v0.26.10 - k8s.io/client-go v0.26.10 + go.uber.org/zap v1.25.0 + k8s.io/api v0.28.4 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 k8s.io/klog v1.0.0 - sigs.k8s.io/cluster-api v1.4.9 - sigs.k8s.io/controller-runtime v0.14.7 - sigs.k8s.io/controller-tools v0.10.0 + sigs.k8s.io/cluster-api v1.6.0 + sigs.k8s.io/controller-runtime v0.16.3 +) + +// Tools +require ( + github.com/golangci/golangci-lint v1.55.1 + sigs.k8s.io/controller-tools v0.13.0 ) +// Indirect dependencies require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect @@ -44,7 +50,7 @@ require ( github.com/ashanbrown/makezero v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v3 v3.4.0 // indirect github.com/breml/bidichk v0.2.7 // indirect @@ -59,14 +65,14 @@ require ( github.com/chavacava/garif v0.1.0 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect github.com/daixiang0/gci v0.11.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.4.3 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.1.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -100,7 +106,7 @@ require ( github.com/golangci/misspell v0.4.1 // indirect github.com/golangci/revgrep v0.5.2 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect - github.com/google/gnostic v0.6.9 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.1 // indirect @@ -139,7 +145,7 @@ require ( github.com/maratori/testpackage v1.1.1 // indirect github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mbilski/exhaustivestruct v1.2.0 // indirect @@ -156,15 +162,15 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.14.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/onsi/gomega v1.28.1 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/onsi/gomega v1.30.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.4.5 // indirect - github.com/prometheus/client_golang v1.16.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.10.1 // indirect + github.com/prometheus/procfs v0.11.1 // indirect github.com/quasilyte/go-ruleguard v0.4.0 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect @@ -172,6 +178,8 @@ require ( github.com/rivo/uniseg v0.4.2 // indirect github.com/ryancurrah/gomodguard v1.3.0 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.24.0 // indirect @@ -183,16 +191,16 @@ require ( github.com/sivchari/nosnakecase v1.7.0 // indirect github.com/sivchari/tenv v1.7.1 // indirect github.com/sonatard/noctx v0.0.2 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect - github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/cobra v1.7.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.16.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/viper v1.17.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.0 // indirect - github.com/subosito/gotenv v1.4.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.15 // indirect @@ -211,21 +219,21 @@ require ( gitlab.com/bosi/decorder v0.4.1 // indirect go-simpler.org/sloglint v0.1.2 // indirect go.tmz.dev/musttag v0.7.2 // indirect - go.uber.org/atomic v1.9.0 // indirect + go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.11.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/term v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.14.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -233,8 +241,8 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.4.6 // indirect - k8s.io/apiextensions-apiserver v0.26.10 // indirect - k8s.io/component-base v0.26.10 // indirect + k8s.io/apiextensions-apiserver v0.28.4 // indirect + k8s.io/component-base v0.28.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect @@ -244,7 +252,5 @@ require ( mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) - -replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 diff --git a/controller/go.sum b/controller/go.sum index ff1db4f8e..4cb343163 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -68,7 +68,6 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/Yamashou/gqlgenc v0.16.0 h1:k1X/dvwnkiDImaeYw+C1j+GDX3MnzB4aONOTE6Mrku4= @@ -92,25 +91,23 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -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/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.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/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= @@ -121,7 +118,6 @@ github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/butuzov/ireturn v0.2.2 h1:jWI36dxXwVrI+RnXDwux2IZOewpmfv930OuIRfaBUJ0= github.com/butuzov/ireturn v0.2.2/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= @@ -133,7 +129,6 @@ github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQd github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -149,33 +144,31 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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/coredns/caddy v1.1.0 h1:ezvsPrT/tA/7pYDBZxu0cT0VmWk75AfIaf6GSYCNMf0= github.com/coredns/caddy v1.1.0/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= github.com/coredns/corefile-migration v1.0.21 h1:W/DCETrHDiFo0Wj03EyMkaQ9fwsmSgqTCQDHpceaSsE= github.com/coredns/corefile-migration v1.0.21/go.mod h1:XnhgULOEouimnzgn0t4WPuFDN2/PJQcTxdWKC5eXNGE= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= github.com/daixiang0/gci v0.11.2 h1:Oji+oPsp3bQ6bNNgX30NBAVT18P4uBH4sRZnlOlTj7Y= github.com/daixiang0/gci v0.11.2/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 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/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= @@ -183,22 +176,20 @@ github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghostiam/protogetter v0.2.3 h1:qdv2pzo3BpLqezwqfGDLZ+nHEYmc5bUpIdsMbBVwMjw= github.com/ghostiam/protogetter v0.2.3/go.mod h1:KmNLOsy1v04hKbvZs8EfGI1fk39AgTdRDxWNYPfXVc4= github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= @@ -214,8 +205,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V 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.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -309,10 +301,10 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSW github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 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.12.7 h1:jM6p55R0MKBg79hZjn1zs2OlrywZ1Vk00rxVvad1/O0= -github.com/google/cel-go v0.12.7/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw= -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/cel-go v0.16.1 h1:3hZfSNiAU3KOiNtxuFXVp5WFy4hf/Ly3Sa4/7F8SXNo= +github.com/google/cel-go v0.16.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -325,6 +317,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -343,8 +336,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= @@ -366,7 +359,6 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -388,7 +380,6 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jgautheron/goconst v1.6.0 h1:gbMLWKRMkzAc6kYsQL6/TxaoBUg3Jm9LSF/Ih1ADWGA= github.com/jgautheron/goconst v1.6.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= @@ -422,7 +413,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -462,8 +452,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= @@ -512,10 +502,10 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= -github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= +github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= @@ -525,8 +515,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 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= @@ -536,8 +526,9 @@ github.com/pluralsh/console-client-go v0.0.55 h1:+j1Ur8ixNx4se4NEfTcul87/oVhUqFs github.com/pluralsh/console-client-go v0.0.55/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= -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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v1.4.5 h1:70YWmMy4FgRHehGNOUask3HtSFSOLKgmDn7ryNe7LqI= github.com/polyfloyd/go-errorlint v1.4.5/go.mod h1:sIZEbFoDOCnTYYZoVkjc4hTnM459tuWA9H/EkdXwsKk= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -545,14 +536,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn 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.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= 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= @@ -564,8 +555,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT 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/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/quasilyte/go-ruleguard v0.4.0 h1:DyM6r+TKL+xbKB4Nm7Afd1IQh9kEUKQs2pboWGKtvQo= github.com/quasilyte/go-ruleguard v0.4.0/go.mod h1:Eu76Z/R8IXtViWUIHkE3p8gdH3/PKk1eh3YGfaEof10= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= @@ -577,7 +568,6 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -586,6 +576,10 @@ github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJ github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= @@ -619,21 +613,20 @@ github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= @@ -653,11 +646,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= @@ -684,9 +676,6 @@ github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvni github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= -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/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -714,20 +703,18 @@ 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 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/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= 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= @@ -739,8 +726,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 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= @@ -751,8 +738,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= -golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 h1:jWGQJV4niP+CCmFW9ekjA9Zx8vYORzOUH2/Nl5WPuLQ= @@ -824,7 +811,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -832,8 +818,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 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= @@ -844,8 +830,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= 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= @@ -921,30 +907,30 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= 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.4/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.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= @@ -1026,8 +1012,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= -gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/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= @@ -1078,7 +1064,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG 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= @@ -1092,12 +1077,11 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/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-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= 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= @@ -1111,12 +1095,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji 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.31.1/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.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -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/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= @@ -1129,7 +1110,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -1147,7 +1127,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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= @@ -1155,7 +1134,6 @@ 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/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= @@ -1168,26 +1146,26 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -k8s.io/api v0.26.10 h1:skTnrDR0r8dg4MMLf6YZIzugxNM0BjFsWKPkNc5kOvk= -k8s.io/api v0.26.10/go.mod h1:ou/H3yviqrHtP/DSPVTfsc7qNfmU06OhajytJfYXkXw= -k8s.io/apiextensions-apiserver v0.26.10 h1:wAriTUc6l7gUqJKOxhmXnYo/VNJzk4oh4QLCUR4Uq+k= -k8s.io/apiextensions-apiserver v0.26.10/go.mod h1:N2qhlxkhJLSoC4f0M1/1lNG627b45SYqnOPEVFoQXw4= -k8s.io/apimachinery v0.26.10 h1:aE+J2KIbjctFqPp3Y0q4Wh2PD+l1p2g3Zp4UYjSvtGU= -k8s.io/apimachinery v0.26.10/go.mod h1:iT1ZP4JBP34wwM+ZQ8ByPEQ81u043iqAcsJYftX9amM= -k8s.io/apiserver v0.26.10 h1:gradpIHygzZN87yK+o6V3gpbCSF78HZ0hejLZQQwdDs= -k8s.io/apiserver v0.26.10/go.mod h1:TGrQKQWUfQcotK3P4TtoVZxXOWklFF36QZlA5wufLs4= -k8s.io/client-go v0.26.10 h1:4mDzl+1IrfRxh4Ro0s65JRGJp14w77gSMUTjACYWVRo= -k8s.io/client-go v0.26.10/go.mod h1:sh74ig838gCckU4ElYclWb24lTesPdEDPnlyg5vcbkA= -k8s.io/cluster-bootstrap v0.25.0 h1:KJ2/r0dV+bLfTK5EBobAVKvjGel3N4Qqh3bvnzh9qPk= -k8s.io/cluster-bootstrap v0.25.0/go.mod h1:x/TCtY3EiuR/rODkA3SvVQT3uSssQLf9cXcmSjdDTe0= -k8s.io/component-base v0.26.10 h1:vl3Gfe5aC09mNxfnQtTng7u3rnBVrShOK3MAkqEleb0= -k8s.io/component-base v0.26.10/go.mod h1:/IDdENUHG5uGxqcofZajovYXE9KSPzJ4yQbkYQt7oN0= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= +k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg= +k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q= +k8s.io/cluster-bootstrap v0.28.4/go.mod h1:/c4ro/R4yf4EtJgFgFtvnHkbDOHwubeKJXh5R1c89Bc= +k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= +k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= 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.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 h1:8cNCQs+WqqnSpZ7y0LMQPKD+RZUHU17VqLPMW3qxnxc= -k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596/go.mod h1:/BYxry62FuDzmI+i9B+X2pqfySRmSOW2ARmj5Zbqhj0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= @@ -1201,15 +1179,15 @@ mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RF 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/cluster-api v1.4.9 h1:3cS3r09k/iCEKbV3OJvPOVwF/bZtD3BhTRx2oGa+fO8= -sigs.k8s.io/cluster-api v1.4.9/go.mod h1:pf1yqnCM26vXEVEjHhVAbnMtSJs+EKdpzyFiWwUAoZY= -sigs.k8s.io/controller-runtime v0.14.7 h1:Vrnm2vk9ZFlRkXATHz0W0wXcqNl7kPat8q2JyxVy0Q8= -sigs.k8s.io/controller-runtime v0.14.7/go.mod h1:ErTs3SJCOujNUnTz4AS+uh8hp6DHMo1gj6fFndJT1X8= -sigs.k8s.io/controller-tools v0.10.0 h1:0L5DTDTFB67jm9DkfrONgTGmfc/zYow0ZaHyppizU2U= -sigs.k8s.io/controller-tools v0.10.0/go.mod h1:uvr0EW6IsprfB0jpQq6evtKy+hHyHCXNfdWI5ONPx94= +sigs.k8s.io/cluster-api v1.6.0 h1:2bhVSnUbtWI8taCjd9lGiHExsRUpKf7Z1fXqi/IwYx4= +sigs.k8s.io/cluster-api v1.6.0/go.mod h1:LB7u/WxiWj4/bbpHNOa1oQ8nq0MQ5iYlD0pGfRSBGLI= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= +sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controller/main.go b/controller/main.go index 4ae6e1ef4..72878680d 100644 --- a/controller/main.go +++ b/controller/main.go @@ -14,7 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" - deploymentsv1alpha "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + deploymentsv1alpha "github.com/pluralsh/console/controller/api/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/types" ) diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 2b6c29497..170b58bbb 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -8,7 +8,7 @@ import ( console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) type authedTransport struct { diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go index 036db9628..b2aabcec8 100644 --- a/controller/pkg/client/provider.go +++ b/controller/pkg/client/provider.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) func (c *client) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { diff --git a/controller/pkg/reconciler/cluster_reconciler.go b/controller/pkg/reconciler/cluster_reconciler.go index 09be5da1d..ce34f6dd5 100644 --- a/controller/pkg/reconciler/cluster_reconciler.go +++ b/controller/pkg/reconciler/cluster_reconciler.go @@ -6,7 +6,7 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/api/errors" diff --git a/controller/pkg/reconciler/cluster_reconciler_scope.go b/controller/pkg/reconciler/cluster_reconciler_scope.go index 8a6963b15..e56fcd1b5 100644 --- a/controller/pkg/reconciler/cluster_reconciler_scope.go +++ b/controller/pkg/reconciler/cluster_reconciler_scope.go @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) type ClusterScope struct { diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/pkg/reconciler/cluster_reconciler_test.go index ca78a7b23..be3d6f796 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/pkg/reconciler/cluster_reconciler_test.go @@ -21,7 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/reconciler" "github.com/pluralsh/console/controller/pkg/test/mocks" ) diff --git a/controller/pkg/reconciler/git_repository_reconciler.go b/controller/pkg/reconciler/git_repository_reconciler.go index bc9d359ba..af2cb4fc1 100644 --- a/controller/pkg/reconciler/git_repository_reconciler.go +++ b/controller/pkg/reconciler/git_repository_reconciler.go @@ -9,7 +9,7 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/utils" diff --git a/controller/pkg/reconciler/git_repository_reconciler_test.go b/controller/pkg/reconciler/git_repository_reconciler_test.go index 727cf784b..6b4980a25 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_test.go +++ b/controller/pkg/reconciler/git_repository_reconciler_test.go @@ -9,7 +9,7 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/mock" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" diff --git a/controller/pkg/reconciler/provider_reconciler.go b/controller/pkg/reconciler/provider_reconciler.go index 3d08ef1cd..df0bd4317 100644 --- a/controller/pkg/reconciler/provider_reconciler.go +++ b/controller/pkg/reconciler/provider_reconciler.go @@ -15,7 +15,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/utils" ) diff --git a/controller/pkg/reconciler/provider_reconciler_attributes.go b/controller/pkg/reconciler/provider_reconciler_attributes.go index 052033815..cb1500d28 100644 --- a/controller/pkg/reconciler/provider_reconciler_attributes.go +++ b/controller/pkg/reconciler/provider_reconciler_attributes.go @@ -9,7 +9,7 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) func (r *ProviderReconciler) missingCredentialKeyError(key string) error { diff --git a/controller/pkg/reconciler/provider_reconciler_scope.go b/controller/pkg/reconciler/provider_reconciler_scope.go index 85b155b3c..d661c4088 100644 --- a/controller/pkg/reconciler/provider_reconciler_scope.go +++ b/controller/pkg/reconciler/provider_reconciler_scope.go @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) type ProviderScope struct { diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/pkg/reconciler/provider_reconciler_test.go index c18ee1ff8..6d4e60739 100644 --- a/controller/pkg/reconciler/provider_reconciler_test.go +++ b/controller/pkg/reconciler/provider_reconciler_test.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/test/mocks" ) diff --git a/controller/pkg/reconciler/service_reconciler.go b/controller/pkg/reconciler/service_reconciler.go index cd77877a3..f170c20bb 100644 --- a/controller/pkg/reconciler/service_reconciler.go +++ b/controller/pkg/reconciler/service_reconciler.go @@ -7,7 +7,7 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/utils" diff --git a/controller/pkg/reconciler/service_reconciler_test.go b/controller/pkg/reconciler/service_reconciler_test.go index 154cd7ab1..0de248a00 100644 --- a/controller/pkg/reconciler/service_reconciler_test.go +++ b/controller/pkg/reconciler/service_reconciler_test.go @@ -6,7 +6,7 @@ import ( "time" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" "github.com/pluralsh/console/controller/pkg/reconciler" "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/samber/lo" diff --git a/controller/pkg/test/mocks/ConsoleClient.go b/controller/pkg/test/mocks/ConsoleClient.go index f673a0faf..3fcb79762 100644 --- a/controller/pkg/test/mocks/ConsoleClient.go +++ b/controller/pkg/test/mocks/ConsoleClient.go @@ -10,7 +10,7 @@ import ( mock "github.com/stretchr/testify/mock" - v1alpha1 "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + v1alpha1 "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) // ConsoleClient is an autogenerated mock type for the ConsoleClient type diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index e213ce506..1d42652ef 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -15,7 +15,7 @@ import ( ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, object ctrlruntimeclient.Object, scheme *runtime.Scheme) error { From da0e8466470836ed713ce64a0c5ae7acbf9db2e5 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 11:59:08 +0100 Subject: [PATCH 139/198] fix git repo import --- controller/pkg/reconciler/git_repository_reconciler_scope.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/pkg/reconciler/git_repository_reconciler_scope.go b/controller/pkg/reconciler/git_repository_reconciler_scope.go index a9f683154..eb643b056 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_scope.go +++ b/controller/pkg/reconciler/git_repository_reconciler_scope.go @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/deployments/v1alpha1" ) type GitRepositoryScope struct { From c95b8fc62f1093e0c7592aa2462f8afbda482418 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 12:04:11 +0100 Subject: [PATCH 140/198] rename samples --- controller/config/crd/{examples => samples}/all_in_one_gcp.yaml | 0 controller/config/crd/{examples => samples}/cluster_aws.yaml | 0 controller/config/crd/{examples => samples}/cluster_byok.yaml | 0 controller/config/crd/{examples => samples}/cluster_existing.yaml | 0 controller/config/crd/{examples => samples}/git_repository.yaml | 0 .../config/crd/{examples => samples}/provider_aws_readonly.yaml | 0 controller/config/crd/{examples => samples}/provider_gcp.yaml | 0 .../config/crd/{examples => samples}/service_deployment.yaml | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename controller/config/crd/{examples => samples}/all_in_one_gcp.yaml (100%) rename controller/config/crd/{examples => samples}/cluster_aws.yaml (100%) rename controller/config/crd/{examples => samples}/cluster_byok.yaml (100%) rename controller/config/crd/{examples => samples}/cluster_existing.yaml (100%) rename controller/config/crd/{examples => samples}/git_repository.yaml (100%) rename controller/config/crd/{examples => samples}/provider_aws_readonly.yaml (100%) rename controller/config/crd/{examples => samples}/provider_gcp.yaml (100%) rename controller/config/crd/{examples => samples}/service_deployment.yaml (100%) diff --git a/controller/config/crd/examples/all_in_one_gcp.yaml b/controller/config/crd/samples/all_in_one_gcp.yaml similarity index 100% rename from controller/config/crd/examples/all_in_one_gcp.yaml rename to controller/config/crd/samples/all_in_one_gcp.yaml diff --git a/controller/config/crd/examples/cluster_aws.yaml b/controller/config/crd/samples/cluster_aws.yaml similarity index 100% rename from controller/config/crd/examples/cluster_aws.yaml rename to controller/config/crd/samples/cluster_aws.yaml diff --git a/controller/config/crd/examples/cluster_byok.yaml b/controller/config/crd/samples/cluster_byok.yaml similarity index 100% rename from controller/config/crd/examples/cluster_byok.yaml rename to controller/config/crd/samples/cluster_byok.yaml diff --git a/controller/config/crd/examples/cluster_existing.yaml b/controller/config/crd/samples/cluster_existing.yaml similarity index 100% rename from controller/config/crd/examples/cluster_existing.yaml rename to controller/config/crd/samples/cluster_existing.yaml diff --git a/controller/config/crd/examples/git_repository.yaml b/controller/config/crd/samples/git_repository.yaml similarity index 100% rename from controller/config/crd/examples/git_repository.yaml rename to controller/config/crd/samples/git_repository.yaml diff --git a/controller/config/crd/examples/provider_aws_readonly.yaml b/controller/config/crd/samples/provider_aws_readonly.yaml similarity index 100% rename from controller/config/crd/examples/provider_aws_readonly.yaml rename to controller/config/crd/samples/provider_aws_readonly.yaml diff --git a/controller/config/crd/examples/provider_gcp.yaml b/controller/config/crd/samples/provider_gcp.yaml similarity index 100% rename from controller/config/crd/examples/provider_gcp.yaml rename to controller/config/crd/samples/provider_gcp.yaml diff --git a/controller/config/crd/examples/service_deployment.yaml b/controller/config/crd/samples/service_deployment.yaml similarity index 100% rename from controller/config/crd/examples/service_deployment.yaml rename to controller/config/crd/samples/service_deployment.yaml From 47bc694065b1a45f3b2b49d680c64e1c64c090de Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Mon, 18 Dec 2023 12:50:41 +0100 Subject: [PATCH 141/198] add service conditions --- .../api/deployments/v1alpha1/service.go | 64 +++++++++++++-- .../v1alpha1/zz_generated.deepcopy.go | 22 +++-- ...loyments.plural.sh_servicedeployments.yaml | 75 ++++++++++++++++- .../reconciler/git_repository_reconciler.go | 3 + .../git_repository_reconciler_test.go | 1 - .../pkg/reconciler/service_reconciler.go | 80 +++++++++++-------- .../reconciler/service_reconciler_scope.go | 42 ++++++++++ .../pkg/reconciler/service_reconciler_test.go | 56 ++++++++++--- 8 files changed, 286 insertions(+), 57 deletions(-) create mode 100644 controller/pkg/reconciler/service_reconciler_scope.go diff --git a/controller/api/deployments/v1alpha1/service.go b/controller/api/deployments/v1alpha1/service.go index 956d21a06..9801f9670 100644 --- a/controller/api/deployments/v1alpha1/service.go +++ b/controller/api/deployments/v1alpha1/service.go @@ -2,6 +2,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -77,15 +78,24 @@ type ServiceSpec struct { } type ServiceStatus struct { - // Id of service in console. - // +optional - Id *string `json:"id,omitempty"` // +optional Errors []ServiceError `json:"errors,omitempty"` // +optional Components []ServiceComponent `json:"components,omitempty"` - // +optional - Sha string `json:"sha,omitempty"` + // ID of the provider in the Console API. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + ID *string `json:"id,omitempty"` + // SHA of last applied configuration. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=string + SHA *string `json:"sha,omitempty"` + // Represents the observations of Repository current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type ServiceError struct { @@ -130,3 +140,47 @@ type ServiceDeploymentList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []ServiceDeployment `json:"items"` } + +func (p *ServiceStatus) HasReadonlyCondition() bool { + return meta.FindStatusCondition(p.Conditions, ReadonlyConditionType.String()) != nil +} + +func (p *ServiceStatus) IsReadonly() bool { + return meta.IsStatusConditionTrue(p.Conditions, ReadonlyConditionType.String()) +} + +func (p *ServiceStatus) IsSHAEqual(sha string) bool { + if !p.HasSHA() { + return false + } + + return p.GetSHA() == sha +} + +func (p *ServiceStatus) GetSHA() string { + if !p.HasSHA() { + return "" + } + + return *p.SHA +} + +func (p *ServiceStatus) HasSHA() bool { + return p.SHA != nil && len(*p.SHA) > 0 +} + +func (p *ServiceStatus) GetID() string { + if !p.HasID() { + return "" + } + + return *p.ID +} + +func (p *ServiceStatus) HasID() bool { + return p.ID != nil && len(*p.ID) > 0 +} + +func (p *ServiceDeployment) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&p.Status.Conditions, condition) +} diff --git a/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go index aadda4a69..55ab21d71 100644 --- a/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go +++ b/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go @@ -909,11 +909,6 @@ func (in *ServiceSpec) DeepCopy() *ServiceSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { *out = *in - if in.Id != nil { - in, out := &in.Id, &out.Id - *out = new(string) - **out = **in - } if in.Errors != nil { in, out := &in.Errors, &out.Errors *out = make([]ServiceError, len(*in)) @@ -926,6 +921,23 @@ func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.SHA != nil { + in, out := &in.SHA, &out.SHA + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceStatus. diff --git a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index 2f76a9284..3db2f1d12 100644 --- a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -333,6 +333,78 @@ spec: - synced type: object type: array + conditions: + description: Represents the observations of Repository current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map errors: items: properties: @@ -346,9 +418,10 @@ spec: type: object type: array id: - description: Id of service in console. + description: ID of the provider in the Console API. type: string sha: + description: SHA of last applied configuration. type: string type: object type: object diff --git a/controller/pkg/reconciler/git_repository_reconciler.go b/controller/pkg/reconciler/git_repository_reconciler.go index c78338f0e..a902bf881 100644 --- a/controller/pkg/reconciler/git_repository_reconciler.go +++ b/controller/pkg/reconciler/git_repository_reconciler.go @@ -215,6 +215,9 @@ func (r *GitRepositoryReconciler) getRepository(url string) (*console.GitReposit } func (r *GitRepositoryReconciler) isAlreadyExists(repository *v1alpha1.GitRepository) (bool, error) { + if controllerutil.ContainsFinalizer(repository, RepoFinalizer) { + return false, nil + } if repository.Status.HasReadonlyCondition() { return repository.Status.IsReadonly(), nil } diff --git a/controller/pkg/reconciler/git_repository_reconciler_test.go b/controller/pkg/reconciler/git_repository_reconciler_test.go index 9db7987ec..627dd628f 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_test.go +++ b/controller/pkg/reconciler/git_repository_reconciler_test.go @@ -117,7 +117,6 @@ func TestCreateNewRepository(t *testing.T) { assert.NoError(t, err) expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) assert.NoError(t, err) - assert.NoError(t, err) assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } diff --git a/controller/pkg/reconciler/service_reconciler.go b/controller/pkg/reconciler/service_reconciler.go index c6d189799..3963f147f 100644 --- a/controller/pkg/reconciler/service_reconciler.go +++ b/controller/pkg/reconciler/service_reconciler.go @@ -12,6 +12,7 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -31,19 +32,34 @@ type ServiceReconciler struct { Scheme *runtime.Scheme } -func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := log.FromContext(ctx) +func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + logger := log.FromContext(ctx) service := &v1alpha1.ServiceDeployment{} if err := r.Get(ctx, req.NamespacedName, service); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } + scope, err := NewServiceScope(ctx, r.Client, service) + if err != nil { + logger.Error(err, "failed to create scope") + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + cluster := &v1alpha1.Cluster{} if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if cluster.Status.ID == nil { - log.Info("Cluster is not ready") + logger.Info("Cluster is not ready") + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "cluster is not ready") return requeue, nil } if !service.GetDeletionTimestamp().IsZero() { @@ -52,49 +68,55 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct repository := &v1alpha1.GitRepository{} if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.RepositoryRef.Name, Namespace: service.Spec.RepositoryRef.Namespace}, repository); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if !repository.DeletionTimestamp.IsZero() { - log.Info("deleting service after repository deletion") + logger.Info("deleting service after repository deletion") if err := r.Delete(ctx, service); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } return requeue, nil } if repository.Status.ID == nil { - log.Info("Repository is not ready") + logger.Info("Repository is not ready") + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "repository is not ready") return requeue, nil } if repository.Status.Health == v1alpha1.GitHealthFailed { - log.Info("Repository is not healthy") + logger.Info("Repository is not healthy") return requeue, nil } attr, err := r.genServiceAttributes(ctx, service, repository.Status.ID) if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if existingService == nil { - if err := utils.TryAddFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { - return ctrl.Result{}, err - } + controllerutil.AddFinalizer(service, ServiceFinalizer) _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } existingService, err = r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } } err = r.addOwnerReferences(ctx, service) if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } sort.Slice(attr.Configuration, func(i, j int) bool { @@ -111,38 +133,35 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct sha, err := utils.HashObject(updater) if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } - if service.Status.Sha != "" && service.Status.Sha != sha { + if service.Status.HasSHA() && !service.Status.IsSHAEqual(sha) { // update service if err := r.ConsoleClient.UpdateService(existingService.ID, updater); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } } - - err = utils.TryAddOwnerRef(ctx, r.Client, cluster, service, r.Scheme) - if err != nil { + if err := controllerutil.SetOwnerReference(cluster, service, r.Scheme); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } - err = utils.TryAddOwnerRef(ctx, r.Client, repository, service, r.Scheme) - if err != nil { + if err = controllerutil.SetOwnerReference(cluster, service, r.Scheme); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } - if err = utils.TryUpdateStatus[*v1alpha1.ServiceDeployment](ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment, original *v1alpha1.ServiceDeployment) (any, any) { - updateStatus(r, existingService, sha) - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err - } + updateStatus(service, existingService, sha) + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") return requeue, nil } func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.ServiceDeploymentExtended, sha string) { - r.Status.Id = &existingService.ID - r.Status.Sha = sha + r.Status.ID = &existingService.ID + r.Status.SHA = &sha if existingService.Errors != nil { r.Status.Errors = algorithms.Map(existingService.Errors, func(b *console.ErrorFragment) v1alpha1.ServiceError { @@ -327,27 +346,22 @@ func (r *ServiceReconciler) handleDelete(ctx context.Context, cluster *v1alpha1. log.Info("try to delete service") existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } if existingService != nil && existingService.DeletedAt != nil { log.Info("waiting for the console") - if err = utils.TryUpdateStatus[*v1alpha1.ServiceDeployment](ctx, r.Client, service, func(r *v1alpha1.ServiceDeployment, original *v1alpha1.ServiceDeployment) (any, any) { - updateStatus(r, existingService, "") - return original.Status, r.Status - }); err != nil { - return ctrl.Result{}, err - } + updateStatus(service, existingService, "") return requeue, nil } if existingService != nil { - if err := r.ConsoleClient.DeleteService(*service.Status.Id); err != nil { + if err := r.ConsoleClient.DeleteService(*service.Status.ID); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) return ctrl.Result{}, err } return requeue, nil } - if err := utils.TryRemoveFinalizer(ctx, r.Client, service, ServiceFinalizer); err != nil { - return ctrl.Result{}, err - } + controllerutil.RemoveFinalizer(service, ServiceFinalizer) } return ctrl.Result{}, nil } diff --git a/controller/pkg/reconciler/service_reconciler_scope.go b/controller/pkg/reconciler/service_reconciler_scope.go new file mode 100644 index 000000000..993467b09 --- /dev/null +++ b/controller/pkg/reconciler/service_reconciler_scope.go @@ -0,0 +1,42 @@ +package reconciler + +import ( + "context" + "errors" + "fmt" + + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" +) + +type ServiceScope struct { + Client client.Client + Service *v1alpha1.ServiceDeployment + + ctx context.Context + patchHelper *patch.Helper +} + +func (p *ServiceScope) PatchObject() error { + return p.patchHelper.Patch(p.ctx, p.Service) +} + +func NewServiceScope(ctx context.Context, client client.Client, service *v1alpha1.ServiceDeployment) (*ServiceScope, error) { + if service == nil { + return nil, errors.New("failed to create new ServiceScope from nil ServiceDeployment") + } + + helper, err := patch.NewHelper(service, client) + if err != nil { + return nil, fmt.Errorf("failed to init patch helper: %s", err) + } + + return &ServiceScope{ + Client: client, + Service: service, + ctx: ctx, + patchHelper: helper, + }, nil +} diff --git a/controller/pkg/reconciler/service_reconciler_test.go b/controller/pkg/reconciler/service_reconciler_test.go index 4942f4e12..e9e8a561a 100644 --- a/controller/pkg/reconciler/service_reconciler_test.go +++ b/controller/pkg/reconciler/service_reconciler_test.go @@ -2,6 +2,7 @@ package reconciler_test import ( "context" + "encoding/json" "testing" "time" @@ -47,8 +48,15 @@ func TestCreateNewService(t *testing.T) { name: "scenario 1: create a new service", service: "test", expectedStatus: v1alpha1.ServiceStatus{ - Id: lo.ToPtr("123"), - Sha: "E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ====", + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, }, returnGetService: &gqlclient.ServiceDeploymentExtended{ ID: "123", @@ -107,11 +115,24 @@ func TestCreateNewService(t *testing.T) { existingService := &v1alpha1.ServiceDeployment{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingService.Status) + existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) + assert.NoError(t, err) + expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } +func sanitizeServiceConditions(status v1alpha1.ServiceStatus) v1alpha1.ServiceStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + func TestDeleteService(t *testing.T) { const ( serviceName = "test" @@ -132,8 +153,8 @@ func TestDeleteService(t *testing.T) { name: "scenario 1: delete service", service: "test", expectedStatus: v1alpha1.ServiceStatus{ - Id: lo.ToPtr("123"), - Sha: "E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ====", + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), }, returnGetService: &gqlclient.ServiceDeploymentExtended{ ID: "123", @@ -152,7 +173,7 @@ func TestDeleteService(t *testing.T) { RepositoryRef: corev1.ObjectReference{Name: repoName}, }, Status: v1alpha1.ServiceStatus{ - Id: lo.ToPtr("123"), + ID: lo.ToPtr("123"), }, }, &v1alpha1.Cluster{ @@ -217,11 +238,18 @@ func TestUpdateService(t *testing.T) { expectedStatus v1alpha1.ServiceStatus }{ { - name: "scenario 1: create a new service", + name: "scenario 1: update service", service: "test", expectedStatus: v1alpha1.ServiceStatus{ - Id: lo.ToPtr("123"), - Sha: "E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ====", + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, }, returnGetService: &gqlclient.ServiceDeploymentExtended{ ID: "123", @@ -236,8 +264,8 @@ func TestUpdateService(t *testing.T) { RepositoryRef: corev1.ObjectReference{Name: repoName}, }, Status: v1alpha1.ServiceStatus{ - Id: lo.ToPtr("123"), - Sha: "abc", + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("abc"), }, }, &v1alpha1.Cluster{ @@ -283,7 +311,11 @@ func TestUpdateService(t *testing.T) { existingService := &v1alpha1.ServiceDeployment{} err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) assert.NoError(t, err) - assert.EqualValues(t, test.expectedStatus, existingService.Status) + existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) + assert.NoError(t, err) + expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) + assert.NoError(t, err) + assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) }) } } From 5d20cde717e1a3cf48d08b5bb9972814cb11e8ed Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 13:10:18 +0100 Subject: [PATCH 142/198] adopt existing apis with kubebuilder --- controller/PROJECT | 16 ++-- controller/api/v1alpha1/cluster_types.go | 64 +++++++++++++++ .../api/v1alpha1/gitrepository_types.go | 64 +++++++++++++++ controller/api/v1alpha1/groupversion_info.go | 36 ++++++++ controller/api/v1alpha1/provider_types.go | 64 +++++++++++++++ .../api/v1alpha1/servicedeployment_types.go | 64 +++++++++++++++ controller/config/crd/kustomization.yaml | 30 +++++++ controller/config/crd/kustomizeconfig.yaml | 17 ++++ .../crd/patches/cainjection_in_clusters.yaml | 8 ++ .../cainjection_in_gitrepositories.yaml | 8 ++ .../crd/patches/cainjection_in_providers.yaml | 8 ++ .../cainjection_in_servicedeployments.yaml | 8 ++ .../crd/patches/webhook_in_clusters.yaml | 17 ++++ .../patches/webhook_in_gitrepositories.yaml | 17 ++++ .../crd/patches/webhook_in_providers.yaml | 17 ++++ .../webhook_in_servicedeployments.yaml | 17 ++++ .../config/rbac/cluster_editor_role.yaml | 24 ++++++ .../config/rbac/cluster_viewer_role.yaml | 20 +++++ .../rbac/gitrepository_editor_role.yaml | 24 ++++++ .../rbac/gitrepository_viewer_role.yaml | 20 +++++ .../config/rbac/provider_editor_role.yaml | 24 ++++++ .../config/rbac/provider_viewer_role.yaml | 20 +++++ .../rbac/servicedeployment_editor_role.yaml | 24 ++++++ .../rbac/servicedeployment_viewer_role.yaml | 20 +++++ .../samples/deployments_v1alpha1_cluster.yaml | 6 ++ .../deployments_v1alpha1_gitrepository.yaml | 6 ++ .../deployments_v1alpha1_provider.yaml | 6 ++ ...eployments_v1alpha1_servicedeployment.yaml | 6 ++ .../cluster_controller.go} | 5 +- .../cluster_controller_test.go} | 10 +-- .../cluster_scope.go} | 2 +- .../{pkg/reconciler => controllers}/common.go | 2 +- .../gitrepository_controller.go} | 5 +- .../gitrepository_controller_test.go} | 10 +-- .../gitrepository_scope.go} | 2 +- .../provider_controller.go} | 5 +- .../provider_controller_attributes.go} | 2 +- .../provider_controller_test.go} | 2 +- .../provider_scope.go} | 2 +- .../service_reconciler.go | 2 +- .../service_reconciler_test.go | 2 +- .../servicedeployment_controller.go | 63 ++++++++++++++ controller/controllers/suite_test.go | 82 +++++++++++++++++++ 43 files changed, 824 insertions(+), 27 deletions(-) create mode 100644 controller/api/v1alpha1/cluster_types.go create mode 100644 controller/api/v1alpha1/gitrepository_types.go create mode 100644 controller/api/v1alpha1/groupversion_info.go create mode 100644 controller/api/v1alpha1/provider_types.go create mode 100644 controller/api/v1alpha1/servicedeployment_types.go create mode 100644 controller/config/crd/kustomization.yaml create mode 100644 controller/config/crd/kustomizeconfig.yaml create mode 100644 controller/config/crd/patches/cainjection_in_clusters.yaml create mode 100644 controller/config/crd/patches/cainjection_in_gitrepositories.yaml create mode 100644 controller/config/crd/patches/cainjection_in_providers.yaml create mode 100644 controller/config/crd/patches/cainjection_in_servicedeployments.yaml create mode 100644 controller/config/crd/patches/webhook_in_clusters.yaml create mode 100644 controller/config/crd/patches/webhook_in_gitrepositories.yaml create mode 100644 controller/config/crd/patches/webhook_in_providers.yaml create mode 100644 controller/config/crd/patches/webhook_in_servicedeployments.yaml create mode 100644 controller/config/rbac/cluster_editor_role.yaml create mode 100644 controller/config/rbac/cluster_viewer_role.yaml create mode 100644 controller/config/rbac/gitrepository_editor_role.yaml create mode 100644 controller/config/rbac/gitrepository_viewer_role.yaml create mode 100644 controller/config/rbac/provider_editor_role.yaml create mode 100644 controller/config/rbac/provider_viewer_role.yaml create mode 100644 controller/config/rbac/servicedeployment_editor_role.yaml create mode 100644 controller/config/rbac/servicedeployment_viewer_role.yaml create mode 100644 controller/config/samples/deployments_v1alpha1_cluster.yaml create mode 100644 controller/config/samples/deployments_v1alpha1_gitrepository.yaml create mode 100644 controller/config/samples/deployments_v1alpha1_provider.yaml create mode 100644 controller/config/samples/deployments_v1alpha1_servicedeployment.yaml rename controller/{pkg/reconciler/cluster_reconciler.go => controllers/cluster_controller.go} (97%) rename controller/{pkg/reconciler/cluster_reconciler_test.go => controllers/cluster_controller_test.go} (98%) rename controller/{pkg/reconciler/cluster_reconciler_scope.go => controllers/cluster_scope.go} (97%) rename controller/{pkg/reconciler => controllers}/common.go (92%) rename controller/{pkg/reconciler/git_repository_reconciler.go => controllers/gitrepository_controller.go} (97%) rename controller/{pkg/reconciler/git_repository_reconciler_test.go => controllers/gitrepository_controller_test.go} (97%) rename controller/{pkg/reconciler/git_repository_reconciler_scope.go => controllers/gitrepository_scope.go} (98%) rename controller/{pkg/reconciler/provider_reconciler.go => controllers/provider_controller.go} (97%) rename controller/{pkg/reconciler/provider_reconciler_attributes.go => controllers/provider_controller_attributes.go} (99%) rename controller/{pkg/reconciler/provider_reconciler_test.go => controllers/provider_controller_test.go} (99%) rename controller/{pkg/reconciler/provider_reconciler_scope.go => controllers/provider_scope.go} (97%) rename controller/{pkg/reconciler => controllers}/service_reconciler.go (99%) rename controller/{pkg/reconciler => controllers}/service_reconciler_test.go (99%) create mode 100644 controller/controllers/servicedeployment_controller.go create mode 100644 controller/controllers/suite_test.go diff --git a/controller/PROJECT b/controller/PROJECT index 712c2b92b..93b45fae8 100644 --- a/controller/PROJECT +++ b/controller/PROJECT @@ -1,16 +1,20 @@ -version: "2" +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: plural.sh -repo: pluralsh/console +repo: github.com/pluralsh/console/controller resources: - group: deployments - version: v1alpha1 kind: Cluster -- group: deployments version: v1alpha1 - kind: Provider - group: deployments + kind: Provider version: v1alpha1 +- group: deployments kind: ServiceDeployment + version: v1alpha1 - group: deployments + kind: GitRepository version: v1alpha1 - kind: GitRepository \ No newline at end of file +version: "2" diff --git a/controller/api/v1alpha1/cluster_types.go b/controller/api/v1alpha1/cluster_types.go new file mode 100644 index 000000000..34ead184c --- /dev/null +++ b/controller/api/v1alpha1/cluster_types.go @@ -0,0 +1,64 @@ +/* +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. + +// ClusterSpec defines the desired state of Cluster +type ClusterSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Cluster. Edit cluster_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// ClusterStatus defines the observed state of Cluster +type ClusterStatus 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 + +// Cluster is the Schema for the clusters API +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterSpec `json:"spec,omitempty"` + Status ClusterStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ClusterList contains a list of Cluster +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Cluster `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Cluster{}, &ClusterList{}) +} diff --git a/controller/api/v1alpha1/gitrepository_types.go b/controller/api/v1alpha1/gitrepository_types.go new file mode 100644 index 000000000..8697e2471 --- /dev/null +++ b/controller/api/v1alpha1/gitrepository_types.go @@ -0,0 +1,64 @@ +/* +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. + +// GitRepositorySpec defines the desired state of GitRepository +type GitRepositorySpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of GitRepository. Edit gitrepository_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// GitRepositoryStatus defines the observed state of GitRepository +type GitRepositoryStatus 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 + +// GitRepository is the Schema for the gitrepositories API +type GitRepository struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GitRepositorySpec `json:"spec,omitempty"` + Status GitRepositoryStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// GitRepositoryList contains a list of GitRepository +type GitRepositoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GitRepository `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GitRepository{}, &GitRepositoryList{}) +} diff --git a/controller/api/v1alpha1/groupversion_info.go b/controller/api/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..ab6dcfeb4 --- /dev/null +++ b/controller/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +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 contains API Schema definitions for the deployments v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=deployments.plural.sh +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: "deployments.plural.sh", 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/controller/api/v1alpha1/provider_types.go b/controller/api/v1alpha1/provider_types.go new file mode 100644 index 000000000..841cf79c8 --- /dev/null +++ b/controller/api/v1alpha1/provider_types.go @@ -0,0 +1,64 @@ +/* +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. + +// ProviderSpec defines the desired state of Provider +type ProviderSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Provider. Edit provider_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// ProviderStatus defines the observed state of Provider +type ProviderStatus 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 + +// Provider is the Schema for the providers API +type Provider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProviderSpec `json:"spec,omitempty"` + Status ProviderStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ProviderList contains a list of Provider +type ProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Provider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Provider{}, &ProviderList{}) +} diff --git a/controller/api/v1alpha1/servicedeployment_types.go b/controller/api/v1alpha1/servicedeployment_types.go new file mode 100644 index 000000000..e1bbbfd8d --- /dev/null +++ b/controller/api/v1alpha1/servicedeployment_types.go @@ -0,0 +1,64 @@ +/* +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. + +// ServiceDeploymentSpec defines the desired state of ServiceDeployment +type ServiceDeploymentSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of ServiceDeployment. Edit servicedeployment_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// ServiceDeploymentStatus defines the observed state of ServiceDeployment +type ServiceDeploymentStatus 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 + +// ServiceDeployment is the Schema for the servicedeployments API +type ServiceDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServiceDeploymentSpec `json:"spec,omitempty"` + Status ServiceDeploymentStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ServiceDeploymentList contains a list of ServiceDeployment +type ServiceDeploymentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServiceDeployment `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServiceDeployment{}, &ServiceDeploymentList{}) +} diff --git a/controller/config/crd/kustomization.yaml b/controller/config/crd/kustomization.yaml new file mode 100644 index 000000000..ab00c0151 --- /dev/null +++ b/controller/config/crd/kustomization.yaml @@ -0,0 +1,30 @@ +# 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/deployments.plural.sh_clusters.yaml +- bases/deployments.plural.sh_providers.yaml +- bases/deployments.plural.sh_gitrepositories.yaml +- bases/deployments.plural.sh_servicedeployments.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_clusters.yaml +#- patches/webhook_in_providers.yaml +#- patches/webhook_in_gitrepositories.yaml +#- patches/webhook_in_servicedeployments.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_clusters.yaml +#- patches/cainjection_in_providers.yaml +#- patches/cainjection_in_gitrepositories.yaml +#- patches/cainjection_in_servicedeployments.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/controller/config/crd/kustomizeconfig.yaml b/controller/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000..6f83d9a94 --- /dev/null +++ b/controller/config/crd/kustomizeconfig.yaml @@ -0,0 +1,17 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhookClientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhookClientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/controller/config/crd/patches/cainjection_in_clusters.yaml b/controller/config/crd/patches/cainjection_in_clusters.yaml new file mode 100644 index 000000000..721215e87 --- /dev/null +++ b/controller/config/crd/patches/cainjection_in_clusters.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: clusters.deployments.plural.sh diff --git a/controller/config/crd/patches/cainjection_in_gitrepositories.yaml b/controller/config/crd/patches/cainjection_in_gitrepositories.yaml new file mode 100644 index 000000000..e186bcee1 --- /dev/null +++ b/controller/config/crd/patches/cainjection_in_gitrepositories.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: gitrepositories.deployments.plural.sh diff --git a/controller/config/crd/patches/cainjection_in_providers.yaml b/controller/config/crd/patches/cainjection_in_providers.yaml new file mode 100644 index 000000000..0aebd5d03 --- /dev/null +++ b/controller/config/crd/patches/cainjection_in_providers.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: providers.deployments.plural.sh diff --git a/controller/config/crd/patches/cainjection_in_servicedeployments.yaml b/controller/config/crd/patches/cainjection_in_servicedeployments.yaml new file mode 100644 index 000000000..c450a07d1 --- /dev/null +++ b/controller/config/crd/patches/cainjection_in_servicedeployments.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: servicedeployments.deployments.plural.sh diff --git a/controller/config/crd/patches/webhook_in_clusters.yaml b/controller/config/crd/patches/webhook_in_clusters.yaml new file mode 100644 index 000000000..7eed68f82 --- /dev/null +++ b/controller/config/crd/patches/webhook_in_clusters.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: clusters.deployments.plural.sh +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/controller/config/crd/patches/webhook_in_gitrepositories.yaml b/controller/config/crd/patches/webhook_in_gitrepositories.yaml new file mode 100644 index 000000000..389e275a0 --- /dev/null +++ b/controller/config/crd/patches/webhook_in_gitrepositories.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: gitrepositories.deployments.plural.sh +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/controller/config/crd/patches/webhook_in_providers.yaml b/controller/config/crd/patches/webhook_in_providers.yaml new file mode 100644 index 000000000..d4f4e87d7 --- /dev/null +++ b/controller/config/crd/patches/webhook_in_providers.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: providers.deployments.plural.sh +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/controller/config/crd/patches/webhook_in_servicedeployments.yaml b/controller/config/crd/patches/webhook_in_servicedeployments.yaml new file mode 100644 index 000000000..a4d02a673 --- /dev/null +++ b/controller/config/crd/patches/webhook_in_servicedeployments.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: servicedeployments.deployments.plural.sh +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/controller/config/rbac/cluster_editor_role.yaml b/controller/config/rbac/cluster_editor_role.yaml new file mode 100644 index 000000000..7f5916279 --- /dev/null +++ b/controller/config/rbac/cluster_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit clusters. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-editor-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - clusters/status + verbs: + - get diff --git a/controller/config/rbac/cluster_viewer_role.yaml b/controller/config/rbac/cluster_viewer_role.yaml new file mode 100644 index 000000000..7f3f22251 --- /dev/null +++ b/controller/config/rbac/cluster_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view clusters. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-viewer-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - clusters + verbs: + - get + - list + - watch +- apiGroups: + - deployments.plural.sh + resources: + - clusters/status + verbs: + - get diff --git a/controller/config/rbac/gitrepository_editor_role.yaml b/controller/config/rbac/gitrepository_editor_role.yaml new file mode 100644 index 000000000..ebccc3ab0 --- /dev/null +++ b/controller/config/rbac/gitrepository_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit gitrepositories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: gitrepository-editor-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories/status + verbs: + - get diff --git a/controller/config/rbac/gitrepository_viewer_role.yaml b/controller/config/rbac/gitrepository_viewer_role.yaml new file mode 100644 index 000000000..192bf7621 --- /dev/null +++ b/controller/config/rbac/gitrepository_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view gitrepositories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: gitrepository-viewer-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories + verbs: + - get + - list + - watch +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories/status + verbs: + - get diff --git a/controller/config/rbac/provider_editor_role.yaml b/controller/config/rbac/provider_editor_role.yaml new file mode 100644 index 000000000..f457dca83 --- /dev/null +++ b/controller/config/rbac/provider_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit providers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: provider-editor-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - providers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - providers/status + verbs: + - get diff --git a/controller/config/rbac/provider_viewer_role.yaml b/controller/config/rbac/provider_viewer_role.yaml new file mode 100644 index 000000000..1bc0bb101 --- /dev/null +++ b/controller/config/rbac/provider_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view providers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: provider-viewer-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - providers + verbs: + - get + - list + - watch +- apiGroups: + - deployments.plural.sh + resources: + - providers/status + verbs: + - get diff --git a/controller/config/rbac/servicedeployment_editor_role.yaml b/controller/config/rbac/servicedeployment_editor_role.yaml new file mode 100644 index 000000000..35764d50c --- /dev/null +++ b/controller/config/rbac/servicedeployment_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit servicedeployments. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: servicedeployment-editor-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments/status + verbs: + - get diff --git a/controller/config/rbac/servicedeployment_viewer_role.yaml b/controller/config/rbac/servicedeployment_viewer_role.yaml new file mode 100644 index 000000000..1ae398a72 --- /dev/null +++ b/controller/config/rbac/servicedeployment_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view servicedeployments. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: servicedeployment-viewer-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments + verbs: + - get + - list + - watch +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments/status + verbs: + - get diff --git a/controller/config/samples/deployments_v1alpha1_cluster.yaml b/controller/config/samples/deployments_v1alpha1_cluster.yaml new file mode 100644 index 000000000..179b2e59f --- /dev/null +++ b/controller/config/samples/deployments_v1alpha1_cluster.yaml @@ -0,0 +1,6 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: cluster-sample +spec: + # TODO(user): Add fields here diff --git a/controller/config/samples/deployments_v1alpha1_gitrepository.yaml b/controller/config/samples/deployments_v1alpha1_gitrepository.yaml new file mode 100644 index 000000000..e1fa2ca5c --- /dev/null +++ b/controller/config/samples/deployments_v1alpha1_gitrepository.yaml @@ -0,0 +1,6 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: gitrepository-sample +spec: + # TODO(user): Add fields here diff --git a/controller/config/samples/deployments_v1alpha1_provider.yaml b/controller/config/samples/deployments_v1alpha1_provider.yaml new file mode 100644 index 000000000..33538183d --- /dev/null +++ b/controller/config/samples/deployments_v1alpha1_provider.yaml @@ -0,0 +1,6 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: provider-sample +spec: + # TODO(user): Add fields here diff --git a/controller/config/samples/deployments_v1alpha1_servicedeployment.yaml b/controller/config/samples/deployments_v1alpha1_servicedeployment.yaml new file mode 100644 index 000000000..0af76eddf --- /dev/null +++ b/controller/config/samples/deployments_v1alpha1_servicedeployment.yaml @@ -0,0 +1,6 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: ServiceDeployment +metadata: + name: servicedeployment-sample +spec: + # TODO(user): Add fields here diff --git a/controller/pkg/reconciler/cluster_reconciler.go b/controller/controllers/cluster_controller.go similarity index 97% rename from controller/pkg/reconciler/cluster_reconciler.go rename to controller/controllers/cluster_controller.go index ce34f6dd5..a70a8a437 100644 --- a/controller/pkg/reconciler/cluster_reconciler.go +++ b/controller/controllers/cluster_controller.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" @@ -39,6 +39,9 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=clusters,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=clusters/status,verbs=get;update;patch + func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { logger := log.FromContext(ctx) diff --git a/controller/pkg/reconciler/cluster_reconciler_test.go b/controller/controllers/cluster_controller_test.go similarity index 98% rename from controller/pkg/reconciler/cluster_reconciler_test.go rename to controller/controllers/cluster_controller_test.go index be3d6f796..eeaaa1619 100644 --- a/controller/pkg/reconciler/cluster_reconciler_test.go +++ b/controller/controllers/cluster_controller_test.go @@ -1,4 +1,4 @@ -package reconciler_test +package controllers_test import ( "context" @@ -6,6 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/controllers" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -22,7 +23,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/pluralsh/console/controller/api/deployments/v1alpha1" - "github.com/pluralsh/console/controller/pkg/reconciler" "github.com/pluralsh/console/controller/pkg/test/mocks" ) @@ -149,7 +149,7 @@ func TestCreateNewCluster(t *testing.T) { ctx := context.Background() - target := &reconciler.ClusterReconciler{ + target := &controllers.ClusterReconciler{ Client: fakeClient, Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), Scheme: scheme.Scheme, @@ -305,7 +305,7 @@ func TestUpdateCluster(t *testing.T) { ctx := context.Background() - target := &reconciler.ClusterReconciler{ + target := &controllers.ClusterReconciler{ Client: fakeClient, Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), Scheme: scheme.Scheme, @@ -415,7 +415,7 @@ func TestAdoptExistingCluster(t *testing.T) { ctx := context.Background() - target := &reconciler.ClusterReconciler{ + target := &controllers.ClusterReconciler{ Client: fakeClient, Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), Scheme: scheme.Scheme, diff --git a/controller/pkg/reconciler/cluster_reconciler_scope.go b/controller/controllers/cluster_scope.go similarity index 97% rename from controller/pkg/reconciler/cluster_reconciler_scope.go rename to controller/controllers/cluster_scope.go index e56fcd1b5..8fb041202 100644 --- a/controller/pkg/reconciler/cluster_reconciler_scope.go +++ b/controller/controllers/cluster_scope.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" diff --git a/controller/pkg/reconciler/common.go b/controller/controllers/common.go similarity index 92% rename from controller/pkg/reconciler/common.go rename to controller/controllers/common.go index 468b07bbe..e58704f80 100644 --- a/controller/pkg/reconciler/common.go +++ b/controller/controllers/common.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "time" diff --git a/controller/pkg/reconciler/git_repository_reconciler.go b/controller/controllers/gitrepository_controller.go similarity index 97% rename from controller/pkg/reconciler/git_repository_reconciler.go rename to controller/controllers/gitrepository_controller.go index c78338f0e..e87ed3085 100644 --- a/controller/pkg/reconciler/git_repository_reconciler.go +++ b/controller/controllers/gitrepository_controller.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" @@ -43,6 +43,9 @@ type GitRepositoryReconciler struct { Scheme *runtime.Scheme } +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories/status,verbs=get;update;patch + func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { logger := log.FromContext(ctx) repo := &v1alpha1.GitRepository{} diff --git a/controller/pkg/reconciler/git_repository_reconciler_test.go b/controller/controllers/gitrepository_controller_test.go similarity index 97% rename from controller/pkg/reconciler/git_repository_reconciler_test.go rename to controller/controllers/gitrepository_controller_test.go index 9db7987ec..00c772078 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_test.go +++ b/controller/controllers/gitrepository_controller_test.go @@ -1,4 +1,4 @@ -package reconciler_test +package controllers_test import ( "context" @@ -6,7 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/pkg/reconciler" + "github.com/pluralsh/console/controller/controllers" "github.com/samber/lo" "github.com/stretchr/testify/mock" @@ -100,7 +100,7 @@ func TestCreateNewRepository(t *testing.T) { // act ctx := context.Background() - target := &reconciler.GitRepositoryReconciler{ + target := &controllers.GitRepositoryReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -198,7 +198,7 @@ func TestUpdateRepository(t *testing.T) { // act ctx := context.Background() - target := &reconciler.GitRepositoryReconciler{ + target := &controllers.GitRepositoryReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -281,7 +281,7 @@ func TestImportRepository(t *testing.T) { // act ctx := context.Background() - target := &reconciler.GitRepositoryReconciler{ + target := &controllers.GitRepositoryReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, diff --git a/controller/pkg/reconciler/git_repository_reconciler_scope.go b/controller/controllers/gitrepository_scope.go similarity index 98% rename from controller/pkg/reconciler/git_repository_reconciler_scope.go rename to controller/controllers/gitrepository_scope.go index eb643b056..97ac64d28 100644 --- a/controller/pkg/reconciler/git_repository_reconciler_scope.go +++ b/controller/controllers/gitrepository_scope.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" diff --git a/controller/pkg/reconciler/provider_reconciler.go b/controller/controllers/provider_controller.go similarity index 97% rename from controller/pkg/reconciler/provider_reconciler.go rename to controller/controllers/provider_controller.go index df0bd4317..01d4c01f8 100644 --- a/controller/pkg/reconciler/provider_reconciler.go +++ b/controller/controllers/provider_controller.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" @@ -35,6 +35,9 @@ const ( ProviderProtectionFinalizerName = "providers.deployments.plural.sh/provider-protection" ) +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=providers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/status,verbs=get;update;patch + // Reconcile ... // TODO: Add kubebuilder rbac annotation func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { diff --git a/controller/pkg/reconciler/provider_reconciler_attributes.go b/controller/controllers/provider_controller_attributes.go similarity index 99% rename from controller/pkg/reconciler/provider_reconciler_attributes.go rename to controller/controllers/provider_controller_attributes.go index cb1500d28..083f7574e 100644 --- a/controller/pkg/reconciler/provider_reconciler_attributes.go +++ b/controller/controllers/provider_controller_attributes.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" diff --git a/controller/pkg/reconciler/provider_reconciler_test.go b/controller/controllers/provider_controller_test.go similarity index 99% rename from controller/pkg/reconciler/provider_reconciler_test.go rename to controller/controllers/provider_controller_test.go index 6d4e60739..f3af1d69b 100644 --- a/controller/pkg/reconciler/provider_reconciler_test.go +++ b/controller/controllers/provider_controller_test.go @@ -1,4 +1,4 @@ -package reconciler_test +package controllers_test import ( "context" diff --git a/controller/pkg/reconciler/provider_reconciler_scope.go b/controller/controllers/provider_scope.go similarity index 97% rename from controller/pkg/reconciler/provider_reconciler_scope.go rename to controller/controllers/provider_scope.go index d661c4088..e368ea68f 100644 --- a/controller/pkg/reconciler/provider_reconciler_scope.go +++ b/controller/controllers/provider_scope.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" diff --git a/controller/pkg/reconciler/service_reconciler.go b/controller/controllers/service_reconciler.go similarity index 99% rename from controller/pkg/reconciler/service_reconciler.go rename to controller/controllers/service_reconciler.go index c6d189799..cb413e1e5 100644 --- a/controller/pkg/reconciler/service_reconciler.go +++ b/controller/controllers/service_reconciler.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" diff --git a/controller/pkg/reconciler/service_reconciler_test.go b/controller/controllers/service_reconciler_test.go similarity index 99% rename from controller/pkg/reconciler/service_reconciler_test.go rename to controller/controllers/service_reconciler_test.go index 4942f4e12..593e3b519 100644 --- a/controller/pkg/reconciler/service_reconciler_test.go +++ b/controller/controllers/service_reconciler_test.go @@ -1,4 +1,4 @@ -package reconciler_test +package controllers_test import ( "context" diff --git a/controller/controllers/servicedeployment_controller.go b/controller/controllers/servicedeployment_controller.go new file mode 100644 index 000000000..b478b3bae --- /dev/null +++ b/controller/controllers/servicedeployment_controller.go @@ -0,0 +1,63 @@ +/* +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 controllers + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + deploymentsv1alpha1 "pluralsh/console/api/v1alpha1" +) + +// ServiceDeploymentReconciler reconciles a ServiceDeployment object +type ServiceDeploymentReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/status,verbs=get;update;patch + +// 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 ServiceDeployment 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.6.4/pkg/reconcile +func (r *ServiceDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + _ = context.Background() + _ = r.Log.WithValues("servicedeployment", req.NamespacedName) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ServiceDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&deploymentsv1alpha1.ServiceDeployment{}). + Complete(r) +} diff --git a/controller/controllers/suite_test.go b/controller/controllers/suite_test.go new file mode 100644 index 000000000..e1edd1a07 --- /dev/null +++ b/controller/controllers/suite_test.go @@ -0,0 +1,82 @@ +/* +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 controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo" + . "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" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + deploymentsv1alpha1 "github.com/pluralsh/console/controller/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) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = deploymentsv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + close(done) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) From 75afe51278c251ccf05cfa9f7387a8c21ad6bc70 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 13:19:20 +0100 Subject: [PATCH 143/198] fix api path --- .../deployments/v1alpha1/groupversion_info.go | 36 -- .../api/{deployments => }/v1alpha1/cluster.go | 0 controller/api/v1alpha1/cluster_types.go | 64 --- .../api/{deployments => }/v1alpha1/common.go | 0 .../api/{deployments => }/v1alpha1/doc.go | 0 .../v1alpha1/git_repository.go | 0 .../api/v1alpha1/gitrepository_types.go | 64 --- .../{deployments => }/v1alpha1/provider.go | 0 controller/api/v1alpha1/provider_types.go | 64 --- .../{deployments => }/v1alpha1/register.go | 0 .../api/{deployments => }/v1alpha1/service.go | 0 .../api/v1alpha1/servicedeployment_types.go | 64 --- .../v1alpha1/zz_generated.deepcopy.go | 0 controller/config/rbac/role.yaml | 86 ++++ controller/controllers/cluster_controller.go | 2 +- .../controllers/cluster_controller_test.go | 2 +- controller/controllers/cluster_scope.go | 2 +- .../controllers/gitrepository_controller.go | 2 +- .../gitrepository_controller_test.go | 2 +- controller/controllers/gitrepository_scope.go | 2 +- controller/controllers/provider_controller.go | 2 +- .../provider_controller_attributes.go | 2 +- .../controllers/provider_controller_test.go | 14 +- controller/controllers/provider_scope.go | 2 +- controller/controllers/service_reconciler.go | 367 ----------------- .../controllers/service_reconciler_scope.go | 4 +- .../servicedeployment_controller.go | 385 ++++++++++++++++-- ...o => servicedeployment_controller_test.go} | 12 +- controller/main.go | 2 +- controller/pkg/client/console.go | 2 +- controller/pkg/client/provider.go | 2 +- controller/pkg/test/mocks/ConsoleClient.go | 2 +- controller/pkg/types/reconciler.go | 10 +- controller/pkg/utils/kubernetes.go | 2 +- 34 files changed, 465 insertions(+), 733 deletions(-) delete mode 100644 controller/api/deployments/v1alpha1/groupversion_info.go rename controller/api/{deployments => }/v1alpha1/cluster.go (100%) delete mode 100644 controller/api/v1alpha1/cluster_types.go rename controller/api/{deployments => }/v1alpha1/common.go (100%) rename controller/api/{deployments => }/v1alpha1/doc.go (100%) rename controller/api/{deployments => }/v1alpha1/git_repository.go (100%) delete mode 100644 controller/api/v1alpha1/gitrepository_types.go rename controller/api/{deployments => }/v1alpha1/provider.go (100%) delete mode 100644 controller/api/v1alpha1/provider_types.go rename controller/api/{deployments => }/v1alpha1/register.go (100%) rename controller/api/{deployments => }/v1alpha1/service.go (100%) delete mode 100644 controller/api/v1alpha1/servicedeployment_types.go rename controller/api/{deployments => }/v1alpha1/zz_generated.deepcopy.go (100%) create mode 100644 controller/config/rbac/role.yaml delete mode 100644 controller/controllers/service_reconciler.go rename controller/controllers/{service_reconciler_test.go => servicedeployment_controller_test.go} (96%) diff --git a/controller/api/deployments/v1alpha1/groupversion_info.go b/controller/api/deployments/v1alpha1/groupversion_info.go deleted file mode 100644 index ab6dcfeb4..000000000 --- a/controller/api/deployments/v1alpha1/groupversion_info.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 contains API Schema definitions for the deployments v1alpha1 API group -// +kubebuilder:object:generate=true -// +groupName=deployments.plural.sh -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: "deployments.plural.sh", 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/controller/api/deployments/v1alpha1/cluster.go b/controller/api/v1alpha1/cluster.go similarity index 100% rename from controller/api/deployments/v1alpha1/cluster.go rename to controller/api/v1alpha1/cluster.go diff --git a/controller/api/v1alpha1/cluster_types.go b/controller/api/v1alpha1/cluster_types.go deleted file mode 100644 index 34ead184c..000000000 --- a/controller/api/v1alpha1/cluster_types.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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. - -// ClusterSpec defines the desired state of Cluster -type ClusterSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of Cluster. Edit cluster_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// ClusterStatus defines the observed state of Cluster -type ClusterStatus 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 - -// Cluster is the Schema for the clusters API -type Cluster struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ClusterSpec `json:"spec,omitempty"` - Status ClusterStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// ClusterList contains a list of Cluster -type ClusterList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Cluster `json:"items"` -} - -func init() { - SchemeBuilder.Register(&Cluster{}, &ClusterList{}) -} diff --git a/controller/api/deployments/v1alpha1/common.go b/controller/api/v1alpha1/common.go similarity index 100% rename from controller/api/deployments/v1alpha1/common.go rename to controller/api/v1alpha1/common.go diff --git a/controller/api/deployments/v1alpha1/doc.go b/controller/api/v1alpha1/doc.go similarity index 100% rename from controller/api/deployments/v1alpha1/doc.go rename to controller/api/v1alpha1/doc.go diff --git a/controller/api/deployments/v1alpha1/git_repository.go b/controller/api/v1alpha1/git_repository.go similarity index 100% rename from controller/api/deployments/v1alpha1/git_repository.go rename to controller/api/v1alpha1/git_repository.go diff --git a/controller/api/v1alpha1/gitrepository_types.go b/controller/api/v1alpha1/gitrepository_types.go deleted file mode 100644 index 8697e2471..000000000 --- a/controller/api/v1alpha1/gitrepository_types.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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. - -// GitRepositorySpec defines the desired state of GitRepository -type GitRepositorySpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of GitRepository. Edit gitrepository_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// GitRepositoryStatus defines the observed state of GitRepository -type GitRepositoryStatus 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 - -// GitRepository is the Schema for the gitrepositories API -type GitRepository struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec GitRepositorySpec `json:"spec,omitempty"` - Status GitRepositoryStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// GitRepositoryList contains a list of GitRepository -type GitRepositoryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []GitRepository `json:"items"` -} - -func init() { - SchemeBuilder.Register(&GitRepository{}, &GitRepositoryList{}) -} diff --git a/controller/api/deployments/v1alpha1/provider.go b/controller/api/v1alpha1/provider.go similarity index 100% rename from controller/api/deployments/v1alpha1/provider.go rename to controller/api/v1alpha1/provider.go diff --git a/controller/api/v1alpha1/provider_types.go b/controller/api/v1alpha1/provider_types.go deleted file mode 100644 index 841cf79c8..000000000 --- a/controller/api/v1alpha1/provider_types.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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. - -// ProviderSpec defines the desired state of Provider -type ProviderSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of Provider. Edit provider_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// ProviderStatus defines the observed state of Provider -type ProviderStatus 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 - -// Provider is the Schema for the providers API -type Provider struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ProviderSpec `json:"spec,omitempty"` - Status ProviderStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// ProviderList contains a list of Provider -type ProviderList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Provider `json:"items"` -} - -func init() { - SchemeBuilder.Register(&Provider{}, &ProviderList{}) -} diff --git a/controller/api/deployments/v1alpha1/register.go b/controller/api/v1alpha1/register.go similarity index 100% rename from controller/api/deployments/v1alpha1/register.go rename to controller/api/v1alpha1/register.go diff --git a/controller/api/deployments/v1alpha1/service.go b/controller/api/v1alpha1/service.go similarity index 100% rename from controller/api/deployments/v1alpha1/service.go rename to controller/api/v1alpha1/service.go diff --git a/controller/api/v1alpha1/servicedeployment_types.go b/controller/api/v1alpha1/servicedeployment_types.go deleted file mode 100644 index e1bbbfd8d..000000000 --- a/controller/api/v1alpha1/servicedeployment_types.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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. - -// ServiceDeploymentSpec defines the desired state of ServiceDeployment -type ServiceDeploymentSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of ServiceDeployment. Edit servicedeployment_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// ServiceDeploymentStatus defines the observed state of ServiceDeployment -type ServiceDeploymentStatus 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 - -// ServiceDeployment is the Schema for the servicedeployments API -type ServiceDeployment struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ServiceDeploymentSpec `json:"spec,omitempty"` - Status ServiceDeploymentStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// ServiceDeploymentList contains a list of ServiceDeployment -type ServiceDeploymentList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []ServiceDeployment `json:"items"` -} - -func init() { - SchemeBuilder.Register(&ServiceDeployment{}, &ServiceDeploymentList{}) -} diff --git a/controller/api/deployments/v1alpha1/zz_generated.deepcopy.go b/controller/api/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from controller/api/deployments/v1alpha1/zz_generated.deepcopy.go rename to controller/api/v1alpha1/zz_generated.deepcopy.go diff --git a/controller/config/rbac/role.yaml b/controller/config/rbac/role.yaml new file mode 100644 index 000000000..ff92f6066 --- /dev/null +++ b/controller/config/rbac/role.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - clusters/status + verbs: + - get + - patch + - update +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories/status + verbs: + - get + - patch + - update +- apiGroups: + - deployments.plural.sh + resources: + - providers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - providers/status + verbs: + - get + - patch + - update +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments/status + verbs: + - get + - patch + - update diff --git a/controller/controllers/cluster_controller.go b/controller/controllers/cluster_controller.go index a70a8a437..3ef053562 100644 --- a/controller/controllers/cluster_controller.go +++ b/controller/controllers/cluster_controller.go @@ -6,7 +6,7 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/utils" "k8s.io/apimachinery/pkg/api/errors" diff --git a/controller/controllers/cluster_controller_test.go b/controller/controllers/cluster_controller_test.go index eeaaa1619..1316a6316 100644 --- a/controller/controllers/cluster_controller_test.go +++ b/controller/controllers/cluster_controller_test.go @@ -22,7 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" "github.com/pluralsh/console/controller/pkg/test/mocks" ) diff --git a/controller/controllers/cluster_scope.go b/controller/controllers/cluster_scope.go index 8fb041202..893594027 100644 --- a/controller/controllers/cluster_scope.go +++ b/controller/controllers/cluster_scope.go @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) type ClusterScope struct { diff --git a/controller/controllers/gitrepository_controller.go b/controller/controllers/gitrepository_controller.go index 99dd18686..68869211f 100644 --- a/controller/controllers/gitrepository_controller.go +++ b/controller/controllers/gitrepository_controller.go @@ -5,7 +5,7 @@ import ( "fmt" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/errors" "github.com/pluralsh/console/controller/pkg/utils" diff --git a/controller/controllers/gitrepository_controller_test.go b/controller/controllers/gitrepository_controller_test.go index 081e119a2..780904d9d 100644 --- a/controller/controllers/gitrepository_controller_test.go +++ b/controller/controllers/gitrepository_controller_test.go @@ -10,7 +10,7 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/mock" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" diff --git a/controller/controllers/gitrepository_scope.go b/controller/controllers/gitrepository_scope.go index 97ac64d28..80d794628 100644 --- a/controller/controllers/gitrepository_scope.go +++ b/controller/controllers/gitrepository_scope.go @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) type GitRepositoryScope struct { diff --git a/controller/controllers/provider_controller.go b/controller/controllers/provider_controller.go index 01d4c01f8..9d6d8cfec 100644 --- a/controller/controllers/provider_controller.go +++ b/controller/controllers/provider_controller.go @@ -15,7 +15,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" consoleclient "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/utils" ) diff --git a/controller/controllers/provider_controller_attributes.go b/controller/controllers/provider_controller_attributes.go index 083f7574e..e3041a3ee 100644 --- a/controller/controllers/provider_controller_attributes.go +++ b/controller/controllers/provider_controller_attributes.go @@ -9,7 +9,7 @@ import ( "github.com/pluralsh/console/controller/pkg/utils" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) func (r *ProviderReconciler) missingCredentialKeyError(key string) error { diff --git a/controller/controllers/provider_controller_test.go b/controller/controllers/provider_controller_test.go index f3af1d69b..22bbd1449 100644 --- a/controller/controllers/provider_controller_test.go +++ b/controller/controllers/provider_controller_test.go @@ -6,16 +6,14 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/controllers" "github.com/samber/lo" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/pluralsh/console/controller/pkg/reconciler" - - "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" @@ -23,7 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" "github.com/pluralsh/console/controller/pkg/test/mocks" ) @@ -114,7 +112,7 @@ func TestCreateNewProvider(t *testing.T) { // act ctx := context.Background() - providerReconciler := &reconciler.ProviderReconciler{ + providerReconciler := &controllers.ProviderReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -209,7 +207,7 @@ func TestAdoptProvider(t *testing.T) { // act ctx := context.Background() - providerReconciler := &reconciler.ProviderReconciler{ + providerReconciler := &controllers.ProviderReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -313,7 +311,7 @@ func TestUpdateProvider(t *testing.T) { // act ctx := context.Background() - providerReconciler := &reconciler.ProviderReconciler{ + providerReconciler := &controllers.ProviderReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, diff --git a/controller/controllers/provider_scope.go b/controller/controllers/provider_scope.go index e368ea68f..e9b27989b 100644 --- a/controller/controllers/provider_scope.go +++ b/controller/controllers/provider_scope.go @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) type ProviderScope struct { diff --git a/controller/controllers/service_reconciler.go b/controller/controllers/service_reconciler.go deleted file mode 100644 index bb2832577..000000000 --- a/controller/controllers/service_reconciler.go +++ /dev/null @@ -1,367 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - "sort" - - console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/utils" - "github.com/pluralsh/polly/algorithms" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -const ( - ServiceFinalizer = "deployments.plural.sh/service-protection" -) - -// ServiceReconciler reconciles a Service object -type ServiceReconciler struct { - client.Client - ConsoleClient consoleclient.ConsoleClient - Scheme *runtime.Scheme -} - -func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { - logger := log.FromContext(ctx) - service := &v1alpha1.ServiceDeployment{} - if err := r.Get(ctx, req.NamespacedName, service); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - scope, err := NewServiceScope(ctx, r.Client, service) - if err != nil { - logger.Error(err, "failed to create scope") - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - // Always patch object when exiting this function, so we can persist any object changes. - defer func() { - if err := scope.PatchObject(); err != nil && reterr == nil { - reterr = err - } - }() - - cluster := &v1alpha1.Cluster{} - if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - - if cluster.Status.ID == nil { - logger.Info("Cluster is not ready") - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "cluster is not ready") - return requeue, nil - } - if !service.GetDeletionTimestamp().IsZero() { - return r.handleDelete(ctx, cluster, service) - } - - repository := &v1alpha1.GitRepository{} - if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.RepositoryRef.Name, Namespace: service.Spec.RepositoryRef.Namespace}, repository); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - if !repository.DeletionTimestamp.IsZero() { - logger.Info("deleting service after repository deletion") - if err := r.Delete(ctx, service); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - return requeue, nil - } - - if repository.Status.ID == nil { - logger.Info("Repository is not ready") - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "repository is not ready") - return requeue, nil - } - if repository.Status.Health == v1alpha1.GitHealthFailed { - logger.Info("Repository is not healthy") - return requeue, nil - } - - attr, err := r.genServiceAttributes(ctx, service, repository.Status.ID) - if err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - - existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) - if err != nil && !errors.IsNotFound(err) { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - if existingService == nil { - controllerutil.AddFinalizer(service, ServiceFinalizer) - _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) - if err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - existingService, err = r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) - if err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - } - err = r.addOwnerReferences(ctx, service) - if err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - sort.Slice(attr.Configuration, func(i, j int) bool { - return attr.Configuration[i].Name < attr.Configuration[j].Name - }) - updater := console.ServiceUpdateAttributes{ - Version: attr.Version, - Protect: attr.Protect, - Git: attr.Git, - Helm: attr.Helm, - Configuration: attr.Configuration, - Kustomize: attr.Kustomize, - } - - sha, err := utils.HashObject(updater) - if err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - - if service.Status.HasSHA() && !service.Status.IsSHAEqual(sha) { - // update service - if err := r.ConsoleClient.UpdateService(existingService.ID, updater); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - } - if err := controllerutil.SetOwnerReference(cluster, service, r.Scheme); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - if err = controllerutil.SetOwnerReference(cluster, service, r.Scheme); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - - updateStatus(service, existingService, sha) - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") - - return requeue, nil -} - -func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.ServiceDeploymentExtended, sha string) { - r.Status.ID = &existingService.ID - r.Status.SHA = &sha - if existingService.Errors != nil { - r.Status.Errors = algorithms.Map(existingService.Errors, - func(b *console.ErrorFragment) v1alpha1.ServiceError { - return v1alpha1.ServiceError{ - Source: b.Source, - Message: b.Message, - } - }) - } - r.Status.Components = make([]v1alpha1.ServiceComponent, 0) - for _, c := range existingService.Components { - sc := v1alpha1.ServiceComponent{ - ID: c.ID, - Name: c.Name, - Group: c.Group, - Kind: c.Kind, - Namespace: c.Namespace, - Synced: c.Synced, - Version: c.Version, - } - if c.State != nil { - state := v1alpha1.ComponentState(*c.State) - sc.State = &state - } - r.Status.Components = append(r.Status.Components, sc) - } -} - -// SetupWithManager sets up the controller with the Manager. -func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.ServiceDeployment{}). - Owns(&corev1.Secret{}). - Owns(&corev1.ConfigMap{}). - Complete(r) -} - -func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.ServiceDeployment, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { - attr := &console.ServiceDeploymentAttributes{ - Name: service.Name, - Namespace: service.Namespace, - Version: &service.Spec.Version, - DocsPath: service.Spec.DocsPath, - Protect: &service.Spec.Protect, - RepositoryID: repositoryId, - } - if service.Spec.Bindings != nil { - attr.ReadBindings = make([]*console.PolicyBindingAttributes, 0) - attr.WriteBindings = make([]*console.PolicyBindingAttributes, 0) - attr.ReadBindings = algorithms.Map(service.Spec.Bindings.Read, - func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) - attr.WriteBindings = algorithms.Map(service.Spec.Bindings.Write, - func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) - } - - if service.Spec.Kustomize != nil { - attr.Kustomize = &console.KustomizeAttributes{ - Path: service.Spec.Kustomize.Path, - } - } - if service.Spec.Git != nil { - attr.Git = &console.GitRefAttributes{ - Ref: service.Spec.Git.Ref, - Folder: service.Spec.Git.Folder, - } - } - if service.Spec.ConfigurationRef != nil { - attr.Configuration = make([]*console.ConfigAttributes, 0) - secret := &corev1.Secret{} - name := types.NamespacedName{Name: service.Spec.ConfigurationRef.Name, Namespace: service.Spec.ConfigurationRef.Namespace} - err := r.Get(ctx, name, secret) - if err != nil { - return nil, err - } - for k, v := range secret.Data { - value := string(v) - attr.Configuration = append(attr.Configuration, &console.ConfigAttributes{ - Name: k, - Value: &value, - }) - } - } - if service.Spec.Helm != nil { - attr.Helm = &console.HelmConfigAttributes{ - ValuesFiles: service.Spec.Helm.ValuesFiles, - Version: service.Spec.Helm.Version, - } - if service.Spec.Helm.Repository != nil { - attr.Helm.Repository = &console.NamespacedName{ - Name: service.Spec.Helm.Repository.Name, - Namespace: service.Spec.Helm.Repository.Namespace, - } - } - if service.Spec.Helm.ValuesRef != nil { - val, err := utils.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ValuesRef) - if err != nil { - return nil, err - } - attr.Helm.Values = &val - } - if service.Spec.Helm.ChartRef != nil { - val, err := utils.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ChartRef) - if err != nil { - return nil, err - } - attr.Helm.Chart = &val - } - } - if service.Spec.SyncConfig != nil { - var annotations *string - var labels *string - if service.Spec.SyncConfig.Annotations != nil { - result, err := json.Marshal(service.Spec.SyncConfig.Annotations) - if err != nil { - return nil, err - } - rawAnnotations := string(result) - annotations = &rawAnnotations - } - if service.Spec.SyncConfig.Labels != nil { - result, err := json.Marshal(service.Spec.SyncConfig.Labels) - if err != nil { - return nil, err - } - rawLabels := string(result) - labels = &rawLabels - } - attr.SyncConfig = &console.SyncConfigAttributes{ - NamespaceMetadata: &console.MetadataAttributes{ - Labels: labels, - Annotations: annotations, - }, - } - } - - return attr, nil -} - -func (r *ServiceReconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.ServiceDeployment) error { - if service.Spec.ConfigurationRef != nil { - configurationSecret, err := utils.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) - if err != nil { - return err - } - if err := utils.TryAddControllerRef(ctx, r.Client, service, configurationSecret, r.Scheme); err != nil { - return err - } - - } - - if service.Spec.Helm != nil && service.Spec.Helm.ValuesRef != nil { - configMap := &corev1.ConfigMap{} - name := types.NamespacedName{Name: service.Spec.Helm.ValuesRef.Name, Namespace: service.Namespace} - err := r.Get(ctx, name, configMap) - if err != nil { - return err - } - err = utils.TryAddControllerRef(ctx, r.Client, service, configMap, r.Scheme) - if err != nil { - return err - } - } - if service.Spec.Helm != nil && service.Spec.Helm.ChartRef != nil { - configMap := &corev1.ConfigMap{} - name := types.NamespacedName{Name: service.Spec.Helm.ChartRef.Name, Namespace: service.Namespace} - err := r.Get(ctx, name, configMap) - if err != nil { - return err - } - err = utils.TryAddControllerRef(ctx, r.Client, service, configMap, r.Scheme) - if err != nil { - return err - } - } - - return nil -} - -func (r *ServiceReconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { - log := log.FromContext(ctx) - if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { - log.Info("try to delete service") - existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) - if err != nil && !errors.IsNotFound(err) { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - if existingService != nil && existingService.DeletedAt != nil { - log.Info("waiting for the console") - updateStatus(service, existingService, "") - return requeue, nil - } - if existingService != nil { - if err := r.ConsoleClient.DeleteService(*service.Status.ID); err != nil { - utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) - return ctrl.Result{}, err - } - return requeue, nil - } - controllerutil.RemoveFinalizer(service, ServiceFinalizer) - } - return ctrl.Result{}, nil -} diff --git a/controller/controllers/service_reconciler_scope.go b/controller/controllers/service_reconciler_scope.go index 993467b09..14337fc50 100644 --- a/controller/controllers/service_reconciler_scope.go +++ b/controller/controllers/service_reconciler_scope.go @@ -1,4 +1,4 @@ -package reconciler +package controllers import ( "context" @@ -8,7 +8,7 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/pluralsh/console/controller/apis/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) type ServiceScope struct { diff --git a/controller/controllers/servicedeployment_controller.go b/controller/controllers/servicedeployment_controller.go index b478b3bae..05170b8c3 100644 --- a/controller/controllers/servicedeployment_controller.go +++ b/controller/controllers/servicedeployment_controller.go @@ -1,63 +1,370 @@ -/* -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 controllers import ( "context" + "encoding/json" + "sort" - "github.com/go-logr/logr" + console "github.com/pluralsh/console-client-go" + "github.com/pluralsh/console/controller/api/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/pkg/errors" + "github.com/pluralsh/console/controller/pkg/utils" + "github.com/pluralsh/polly/algorithms" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) - deploymentsv1alpha1 "pluralsh/console/api/v1alpha1" +const ( + ServiceFinalizer = "deployments.plural.sh/service-protection" ) -// ServiceDeploymentReconciler reconciles a ServiceDeployment object -type ServiceDeploymentReconciler struct { +// ServiceReconciler reconciles a Service object +type ServiceReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme + ConsoleClient consoleclient.ConsoleClient + Scheme *runtime.Scheme } //+kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/status,verbs=get;update;patch -// 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 ServiceDeployment 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.6.4/pkg/reconcile -func (r *ServiceDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("servicedeployment", req.NamespacedName) - - // TODO(user): your logic here +func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + logger := log.FromContext(ctx) + service := &v1alpha1.ServiceDeployment{} + if err := r.Get(ctx, req.NamespacedName, service); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + scope, err := NewServiceScope(ctx, r.Client, service) + if err != nil { + logger.Error(err, "failed to create scope") + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() - return ctrl.Result{}, nil + cluster := &v1alpha1.Cluster{} + if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.ClusterRef.Name, Namespace: service.Spec.ClusterRef.Namespace}, cluster); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + + if cluster.Status.ID == nil { + logger.Info("Cluster is not ready") + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "cluster is not ready") + return requeue, nil + } + if !service.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, cluster, service) + } + + repository := &v1alpha1.GitRepository{} + if err := r.Get(ctx, client.ObjectKey{Name: service.Spec.RepositoryRef.Name, Namespace: service.Spec.RepositoryRef.Namespace}, repository); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + if !repository.DeletionTimestamp.IsZero() { + logger.Info("deleting service after repository deletion") + if err := r.Delete(ctx, service); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + return requeue, nil + } + + if repository.Status.ID == nil { + logger.Info("Repository is not ready") + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "repository is not ready") + return requeue, nil + } + if repository.Status.Health == v1alpha1.GitHealthFailed { + logger.Info("Repository is not healthy") + return requeue, nil + } + + attr, err := r.genServiceAttributes(ctx, service, repository.Status.ID) + if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + + existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) + if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + if existingService == nil { + controllerutil.AddFinalizer(service, ServiceFinalizer) + _, err = r.ConsoleClient.CreateService(cluster.Status.ID, *attr) + if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + existingService, err = r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) + if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + } + err = r.addOwnerReferences(ctx, service) + if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + sort.Slice(attr.Configuration, func(i, j int) bool { + return attr.Configuration[i].Name < attr.Configuration[j].Name + }) + updater := console.ServiceUpdateAttributes{ + Version: attr.Version, + Protect: attr.Protect, + Git: attr.Git, + Helm: attr.Helm, + Configuration: attr.Configuration, + Kustomize: attr.Kustomize, + } + + sha, err := utils.HashObject(updater) + if err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + + if service.Status.HasSHA() && !service.Status.IsSHAEqual(sha) { + // update service + if err := r.ConsoleClient.UpdateService(existingService.ID, updater); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + } + if err := controllerutil.SetOwnerReference(cluster, service, r.Scheme); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + if err = controllerutil.SetOwnerReference(cluster, service, r.Scheme); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + + updateStatus(service, existingService, sha) + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") + + return requeue, nil +} + +func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.ServiceDeploymentExtended, sha string) { + r.Status.ID = &existingService.ID + r.Status.SHA = &sha + if existingService.Errors != nil { + r.Status.Errors = algorithms.Map(existingService.Errors, + func(b *console.ErrorFragment) v1alpha1.ServiceError { + return v1alpha1.ServiceError{ + Source: b.Source, + Message: b.Message, + } + }) + } + r.Status.Components = make([]v1alpha1.ServiceComponent, 0) + for _, c := range existingService.Components { + sc := v1alpha1.ServiceComponent{ + ID: c.ID, + Name: c.Name, + Group: c.Group, + Kind: c.Kind, + Namespace: c.Namespace, + Synced: c.Synced, + Version: c.Version, + } + if c.State != nil { + state := v1alpha1.ComponentState(*c.State) + sc.State = &state + } + r.Status.Components = append(r.Status.Components, sc) + } } // SetupWithManager sets up the controller with the Manager. -func (r *ServiceDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&deploymentsv1alpha1.ServiceDeployment{}). + For(&v1alpha1.ServiceDeployment{}). + Owns(&corev1.Secret{}). + Owns(&corev1.ConfigMap{}). Complete(r) } + +func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.ServiceDeployment, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { + attr := &console.ServiceDeploymentAttributes{ + Name: service.Name, + Namespace: service.Namespace, + Version: &service.Spec.Version, + DocsPath: service.Spec.DocsPath, + Protect: &service.Spec.Protect, + RepositoryID: repositoryId, + } + if service.Spec.Bindings != nil { + attr.ReadBindings = make([]*console.PolicyBindingAttributes, 0) + attr.WriteBindings = make([]*console.PolicyBindingAttributes, 0) + attr.ReadBindings = algorithms.Map(service.Spec.Bindings.Read, + func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + attr.WriteBindings = algorithms.Map(service.Spec.Bindings.Write, + func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + } + + if service.Spec.Kustomize != nil { + attr.Kustomize = &console.KustomizeAttributes{ + Path: service.Spec.Kustomize.Path, + } + } + if service.Spec.Git != nil { + attr.Git = &console.GitRefAttributes{ + Ref: service.Spec.Git.Ref, + Folder: service.Spec.Git.Folder, + } + } + if service.Spec.ConfigurationRef != nil { + attr.Configuration = make([]*console.ConfigAttributes, 0) + secret := &corev1.Secret{} + name := types.NamespacedName{Name: service.Spec.ConfigurationRef.Name, Namespace: service.Spec.ConfigurationRef.Namespace} + err := r.Get(ctx, name, secret) + if err != nil { + return nil, err + } + for k, v := range secret.Data { + value := string(v) + attr.Configuration = append(attr.Configuration, &console.ConfigAttributes{ + Name: k, + Value: &value, + }) + } + } + if service.Spec.Helm != nil { + attr.Helm = &console.HelmConfigAttributes{ + ValuesFiles: service.Spec.Helm.ValuesFiles, + Version: service.Spec.Helm.Version, + } + if service.Spec.Helm.Repository != nil { + attr.Helm.Repository = &console.NamespacedName{ + Name: service.Spec.Helm.Repository.Name, + Namespace: service.Spec.Helm.Repository.Namespace, + } + } + if service.Spec.Helm.ValuesRef != nil { + val, err := utils.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ValuesRef) + if err != nil { + return nil, err + } + attr.Helm.Values = &val + } + if service.Spec.Helm.ChartRef != nil { + val, err := utils.GetConfigMapData(ctx, r.Client, service.Namespace, service.Spec.Helm.ChartRef) + if err != nil { + return nil, err + } + attr.Helm.Chart = &val + } + } + if service.Spec.SyncConfig != nil { + var annotations *string + var labels *string + if service.Spec.SyncConfig.Annotations != nil { + result, err := json.Marshal(service.Spec.SyncConfig.Annotations) + if err != nil { + return nil, err + } + rawAnnotations := string(result) + annotations = &rawAnnotations + } + if service.Spec.SyncConfig.Labels != nil { + result, err := json.Marshal(service.Spec.SyncConfig.Labels) + if err != nil { + return nil, err + } + rawLabels := string(result) + labels = &rawLabels + } + attr.SyncConfig = &console.SyncConfigAttributes{ + NamespaceMetadata: &console.MetadataAttributes{ + Labels: labels, + Annotations: annotations, + }, + } + } + + return attr, nil +} + +func (r *ServiceReconciler) addOwnerReferences(ctx context.Context, service *v1alpha1.ServiceDeployment) error { + if service.Spec.ConfigurationRef != nil { + configurationSecret, err := utils.GetSecret(ctx, r.Client, service.Spec.ConfigurationRef) + if err != nil { + return err + } + if err := utils.TryAddControllerRef(ctx, r.Client, service, configurationSecret, r.Scheme); err != nil { + return err + } + + } + + if service.Spec.Helm != nil && service.Spec.Helm.ValuesRef != nil { + configMap := &corev1.ConfigMap{} + name := types.NamespacedName{Name: service.Spec.Helm.ValuesRef.Name, Namespace: service.Namespace} + err := r.Get(ctx, name, configMap) + if err != nil { + return err + } + err = utils.TryAddControllerRef(ctx, r.Client, service, configMap, r.Scheme) + if err != nil { + return err + } + } + if service.Spec.Helm != nil && service.Spec.Helm.ChartRef != nil { + configMap := &corev1.ConfigMap{} + name := types.NamespacedName{Name: service.Spec.Helm.ChartRef.Name, Namespace: service.Namespace} + err := r.Get(ctx, name, configMap) + if err != nil { + return err + } + err = utils.TryAddControllerRef(ctx, r.Client, service, configMap, r.Scheme) + if err != nil { + return err + } + } + + return nil +} + +func (r *ServiceReconciler) handleDelete(ctx context.Context, cluster *v1alpha1.Cluster, service *v1alpha1.ServiceDeployment) (ctrl.Result, error) { + log := log.FromContext(ctx) + if controllerutil.ContainsFinalizer(service, ServiceFinalizer) { + log.Info("try to delete service") + existingService, err := r.ConsoleClient.GetService(*cluster.Status.ID, service.Name) + if err != nil && !errors.IsNotFound(err) { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + if existingService != nil && existingService.DeletedAt != nil { + log.Info("waiting for the console") + updateStatus(service, existingService, "") + return requeue, nil + } + if existingService != nil { + if err := r.ConsoleClient.DeleteService(*service.Status.ID); err != nil { + utils.MarkCondition(service.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + return requeue, nil + } + controllerutil.RemoveFinalizer(service, ServiceFinalizer) + } + return ctrl.Result{}, nil +} diff --git a/controller/controllers/service_reconciler_test.go b/controller/controllers/servicedeployment_controller_test.go similarity index 96% rename from controller/controllers/service_reconciler_test.go rename to controller/controllers/servicedeployment_controller_test.go index bca9958cc..9dceb2d6e 100644 --- a/controller/controllers/service_reconciler_test.go +++ b/controller/controllers/servicedeployment_controller_test.go @@ -7,8 +7,8 @@ import ( "time" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" - "github.com/pluralsh/console/controller/pkg/reconciler" + "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/controllers" "github.com/pluralsh/console/controller/pkg/test/mocks" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -103,7 +103,7 @@ func TestCreateNewService(t *testing.T) { ctx := context.Background() - target := &reconciler.ServiceReconciler{ + target := &controllers.ServiceReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -165,7 +165,7 @@ func TestDeleteService(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: serviceName, DeletionTimestamp: &metav1.Time{Time: time.Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, - Finalizers: []string{reconciler.ServiceFinalizer}, + Finalizers: []string{controllers.ServiceFinalizer}, }, Spec: v1alpha1.ServiceSpec{ Version: "1.24", @@ -205,7 +205,7 @@ func TestDeleteService(t *testing.T) { fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() ctx := context.Background() - target := &reconciler.ServiceReconciler{ + target := &controllers.ServiceReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -299,7 +299,7 @@ func TestUpdateService(t *testing.T) { ctx := context.Background() - target := &reconciler.ServiceReconciler{ + target := &controllers.ServiceReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, diff --git a/controller/main.go b/controller/main.go index 72878680d..e272fd4c9 100644 --- a/controller/main.go +++ b/controller/main.go @@ -14,7 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" - deploymentsv1alpha "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + deploymentsv1alpha "github.com/pluralsh/console/controller/api/v1alpha1" "github.com/pluralsh/console/controller/pkg/client" "github.com/pluralsh/console/controller/pkg/types" ) diff --git a/controller/pkg/client/console.go b/controller/pkg/client/console.go index 170b58bbb..0f8472c44 100644 --- a/controller/pkg/client/console.go +++ b/controller/pkg/client/console.go @@ -8,7 +8,7 @@ import ( console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) type authedTransport struct { diff --git a/controller/pkg/client/provider.go b/controller/pkg/client/provider.go index b2aabcec8..6c7675a6f 100644 --- a/controller/pkg/client/provider.go +++ b/controller/pkg/client/provider.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) func (c *client) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgenclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { diff --git a/controller/pkg/test/mocks/ConsoleClient.go b/controller/pkg/test/mocks/ConsoleClient.go index 3fcb79762..5ba4ab7b6 100644 --- a/controller/pkg/test/mocks/ConsoleClient.go +++ b/controller/pkg/test/mocks/ConsoleClient.go @@ -10,7 +10,7 @@ import ( mock "github.com/stretchr/testify/mock" - v1alpha1 "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + v1alpha1 "github.com/pluralsh/console/controller/api/v1alpha1" ) // ConsoleClient is an autogenerated mock type for the ConsoleClient type diff --git a/controller/pkg/types/reconciler.go b/controller/pkg/types/reconciler.go index 7a57711b5..ad8e887dc 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/pkg/types/reconciler.go @@ -3,8 +3,8 @@ package types import ( "fmt" + "github.com/pluralsh/console/controller/controllers" "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/reconciler" ctrl "sigs.k8s.io/controller-runtime" ) @@ -38,26 +38,26 @@ func ToReconciler(reconciler string) (Reconciler, error) { func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.ConsoleClient) (Controller, error) { switch sc { case GitRepositoryReconciler: - return &reconciler.GitRepositoryReconciler{ + return &controllers.GitRepositoryReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ServiceDeploymentReconciler: - return &reconciler.ServiceReconciler{ + return &controllers.ServiceReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ClusterReconciler: - return &reconciler.ClusterReconciler{ + return &controllers.ClusterReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Log: mgr.GetLogger(), Scheme: mgr.GetScheme(), }, nil case ProviderReconciler: - return &reconciler.ProviderReconciler{ + return &controllers.ProviderReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), diff --git a/controller/pkg/utils/kubernetes.go b/controller/pkg/utils/kubernetes.go index 1d42652ef..2525145fd 100644 --- a/controller/pkg/utils/kubernetes.go +++ b/controller/pkg/utils/kubernetes.go @@ -15,7 +15,7 @@ import ( ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/pluralsh/console/controller/api/deployments/v1alpha1" + "github.com/pluralsh/console/controller/api/v1alpha1" ) func TryAddOwnerRef(ctx context.Context, client ctrlruntimeclient.Client, owner ctrlruntimeclient.Object, object ctrlruntimeclient.Object, scheme *runtime.Scheme) error { From 8fefd71b22888cdf7382f9c7a3536140728efac9 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 13:27:22 +0100 Subject: [PATCH 144/198] fix build --- ...er_scope.go => servicedeployment_scope.go} | 0 controller/controllers/suite_test.go | 30 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) rename controller/controllers/{service_reconciler_scope.go => servicedeployment_scope.go} (100%) diff --git a/controller/controllers/service_reconciler_scope.go b/controller/controllers/servicedeployment_scope.go similarity index 100% rename from controller/controllers/service_reconciler_scope.go rename to controller/controllers/servicedeployment_scope.go diff --git a/controller/controllers/suite_test.go b/controller/controllers/suite_test.go index e1edd1a07..ea5f8444e 100644 --- a/controller/controllers/suite_test.go +++ b/controller/controllers/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023. +Copyright 2023 The Kubernetes authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,13 +20,13 @@ import ( "path/filepath" "testing" - . "github.com/onsi/ginkgo" + . "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" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -44,24 +44,23 @@ var testEnv *envtest.Environment func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) + RunSpecs(t, "Controller Suite") } -var _ = BeforeSuite(func(done Done) { - logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) +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")}, + 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).ToNot(HaveOccurred()) - Expect(cfg).ToNot(BeNil()) + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) err = deploymentsv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) @@ -69,14 +68,13 @@ var _ = BeforeSuite(func(done Done) { //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).ToNot(HaveOccurred()) - Expect(k8sClient).ToNot(BeNil()) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) - close(done) -}, 60) +}) var _ = AfterSuite(func() { By("tearing down the test environment") err := testEnv.Stop() - Expect(err).ToNot(HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) }) From ee924a98b3dcb934a6fe8d132967f71b3e578cd8 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Mon, 18 Dec 2023 13:31:32 +0100 Subject: [PATCH 145/198] add kubebuilder installation target and kustomize/setup-envtest tools --- controller/Makefile | 11 ++++++ controller/go.mod | 31 +++++++++++----- controller/go.sum | 62 +++++++++++++++++++++----------- controller/hack/include/build.mk | 27 ++++++++++++++ controller/tools.go | 2 ++ 5 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 controller/hack/include/build.mk diff --git a/controller/Makefile b/controller/Makefile index 796155596..6d5f2ccdf 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -1,6 +1,11 @@ +ROOT_DIRECTORY := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +include $(ROOT_DIRECTORY)/hack/include/build.mk + IMG ?= deployment-controller:latest # Image URL to use all building/pushing image targets CONTROLLER_GEN = $(shell which controller-gen) CRD_OPTIONS ?= "crd" +KUBEBUILDER_VERSION := 3.11.1 ifndef GOPATH $(error $$GOPATH environment variable not set) @@ -29,6 +34,12 @@ update-dependencies: ## update dependencies install-tools: ## install required tools @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI {} go install {} +.PHONY: install-kubebuilder +install-kubebuilder: ## install kubebuilder + @curl -L -O --output-dir bin/ "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_${OS}_${ARCH}" + @chmod +x bin/kubebuilder_${OS}_${ARCH} + @mv bin/kubebuilder_${OS}_${ARCH} ${GOPATH}/bin/kubebuilder + ##@ Build .PHONY: build diff --git a/controller/go.mod b/controller/go.mod index 259003798..67c850908 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -8,12 +8,12 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 github.com/go-logr/logr v1.3.0 - github.com/pluralsh/console-client-go v0.0.55 + github.com/pluralsh/console-client-go v0.0.57 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.39.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - go.uber.org/zap v1.25.0 + go.uber.org/zap v1.26.0 k8s.io/api v0.28.4 k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 @@ -24,8 +24,10 @@ require ( // Tools require ( - github.com/golangci/golangci-lint v1.55.1 + github.com/golangci/golangci-lint v1.55.2 + sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231215020716-1b80b9629af8 sigs.k8s.io/controller-tools v0.13.0 + sigs.k8s.io/kustomize/kustomize/v5 v5.3.0 ) // Indirect dependencies @@ -219,10 +221,9 @@ require ( gitlab.com/bosi/decorder v0.4.1 // indirect go-simpler.org/sloglint v0.1.2 // indirect go.tmz.dev/musttag v0.7.2 // indirect - go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.15.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.18.0 // indirect @@ -243,14 +244,26 @@ require ( honnef.co/go/tools v0.4.6 // indirect k8s.io/apiextensions-apiserver v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect - k8s.io/klog/v2 v2.100.1 // indirect - k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect mvdan.cc/gofumpt v0.5.0 // indirect mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +require ( + github.com/go-errors/errors v1.4.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect + gopkg.in/evanphx/json-patch.v5 v5.6.0 // indirect + sigs.k8s.io/kustomize/api v0.16.0 // indirect + sigs.k8s.io/kustomize/cmd/config v0.13.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect +) diff --git a/controller/go.sum b/controller/go.sum index 4cb343163..ec57e6dd9 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -93,15 +93,13 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.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= @@ -194,6 +192,8 @@ github.com/ghostiam/protogetter v0.2.3 h1:qdv2pzo3BpLqezwqfGDLZ+nHEYmc5bUpIdsMbB github.com/ghostiam/protogetter v0.2.3/go.mod h1:KmNLOsy1v04hKbvZs8EfGI1fk39AgTdRDxWNYPfXVc4= github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= github.com/go-critic/go-critic v0.9.0/go.mod h1:5P8tdXL7m/6qnyG6oRAlYLORvoXH0WDypYgAEmagT40= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-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= @@ -204,7 +204,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 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.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -287,8 +286,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/golangci-lint v1.55.1 h1:DL2j9Eeapg1N3WEkKnQFX5L40SYtjZZJjGVdyEgNrDc= -github.com/golangci/golangci-lint v1.55.1/go.mod h1:z00biPRqjo5MISKV1+RWgONf2KvrPDmfqxHpHKB6bI4= +github.com/golangci/golangci-lint v1.55.2 h1:yllEIsSJ7MtlDBwDJ9IMBkyEUz2fYE0b5B8IUgO1oP8= +github.com/golangci/golangci-lint v1.55.2/go.mod h1:H60CZ0fuqoTwlTvnbyjhpZPWp7KmsjwV2yupIMiMXbM= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= @@ -339,6 +338,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -482,6 +483,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -522,8 +525,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pluralsh/console-client-go v0.0.55 h1:+j1Ur8ixNx4se4NEfTcul87/oVhUqFs+ZdsvCzvPYFM= -github.com/pluralsh/console-client-go v0.0.55/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= +github.com/pluralsh/console-client-go v0.0.57 h1:XVs2fSrHCU/gB79DKqmsHF9Fo/D9oy8R69oSewFgGfI= +github.com/pluralsh/console-client-go v0.0.57/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -678,6 +681,8 @@ github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zs github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= @@ -703,6 +708,8 @@ 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -713,8 +720,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -738,8 +745,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 h1:jWGQJV4niP+CCmFW9ekjA9Zx8vYORzOUH2/Nl5WPuLQ= @@ -912,6 +919,7 @@ golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= @@ -1119,6 +1127,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v5 v5.6.0 h1:BMT6KIwBD9CaU91PJCZIe46bDmBWa9ynTQgJIOpfQBk= +gopkg.in/evanphx/json-patch.v5 v5.6.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -1162,12 +1172,12 @@ k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= 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.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= @@ -1183,11 +1193,21 @@ sigs.k8s.io/cluster-api v1.6.0 h1:2bhVSnUbtWI8taCjd9lGiHExsRUpKf7Z1fXqi/IwYx4= sigs.k8s.io/cluster-api v1.6.0/go.mod h1:LB7u/WxiWj4/bbpHNOa1oQ8nq0MQ5iYlD0pGfRSBGLI= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231215020716-1b80b9629af8 h1:BjvzWD09lfJ/40dOFv6HbEH3eUg/Rj+BSwNks/Fe+DY= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231215020716-1b80b9629af8/go.mod h1:TF/lVLWS+JNNaVqJuDDictY2hZSXSsIHCx4FClMvqFg= sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g= +sigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c= +sigs.k8s.io/kustomize/cmd/config v0.13.0 h1:Z/bRyFQupMIqGz1KlRkLimK/VjtE4/Oj/DinJmQqTDc= +sigs.k8s.io/kustomize/cmd/config v0.13.0/go.mod h1:YlsZ9JysiHN7OjSmIZ17zvq9kl1oN2Osn+3wVyERkcM= +sigs.k8s.io/kustomize/kustomize/v5 v5.3.0 h1:OUKaQwArd1udTz3ykibOjaUwdfly6FnkQiDSSft6+Fg= +sigs.k8s.io/kustomize/kustomize/v5 v5.3.0/go.mod h1:qGalrWojwFYaT7KQXLo3kmLyuyr6VaIQYY+BWeRENus= +sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0= +sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controller/hack/include/build.mk b/controller/hack/include/build.mk new file mode 100644 index 000000000..89059872c --- /dev/null +++ b/controller/hack/include/build.mk @@ -0,0 +1,27 @@ +# BUILDARCH is the host machine architecture +BUILDARCH ?= $(shell uname -m) + +# BUILDOS is the host machine OS +BUILDOS ?= $(shell uname -s) + +ifeq ($(BUILDARCH),x86_64) + BUILDARCH=amd64 +endif +ifeq ($(BUILDARCH),arch64) + BUILDARCH=arm64 +endif +ifeq ($(BUILDARCH),armv7l) + BUILDARCH=armv7 +endif + +ifeq ($(BUILDOS),Linux) + BUILDOS=linux +endif +ifeq ($(BUILDOS),Darwin) + BUILDOS=darwin +endif + +# ARCH is the target build architecture. Unless overridden during build, host architecture (BUILDARCH) will be used +ARCH ?= $(BUILDARCH) +# OS is the target build OS. Unless overridden during build, host OS (BUILDOS) will be used +OS ?= $(BUILDOS) diff --git a/controller/tools.go b/controller/tools.go index 655a352e1..b28dc825b 100644 --- a/controller/tools.go +++ b/controller/tools.go @@ -4,5 +4,7 @@ package main import ( _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" _ "sigs.k8s.io/controller-tools/cmd/controller-gen" + _ "sigs.k8s.io/kustomize/kustomize/v5" ) From ee154450d46c7b9b4a15ec41b7100be898973ea0 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Mon, 18 Dec 2023 13:38:25 +0100 Subject: [PATCH 146/198] bump console-client-go --- controller/go.mod | 8 ++++++-- controller/go.sum | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/controller/go.mod b/controller/go.mod index 67c850908..fe3355860 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/Yamashou/gqlgenc v0.16.0 github.com/go-logr/logr v1.3.0 - github.com/pluralsh/console-client-go v0.0.57 + github.com/pluralsh/console-client-go v0.0.59 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.39.0 github.com/spf13/pflag v1.0.5 @@ -164,7 +164,7 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.14.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/onsi/gomega v1.30.0 // indirect + github.com/onsi/gomega v1.30.0 github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -256,8 +256,12 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) +require github.com/onsi/ginkgo/v2 v2.13.1 + require ( github.com/go-errors/errors v1.4.2 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/controller/go.sum b/controller/go.sum index ec57e6dd9..96b2a519d 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -525,8 +525,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pluralsh/console-client-go v0.0.57 h1:XVs2fSrHCU/gB79DKqmsHF9Fo/D9oy8R69oSewFgGfI= -github.com/pluralsh/console-client-go v0.0.57/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= +github.com/pluralsh/console-client-go v0.0.59 h1:Yts24uUofVq9e2Q2Fmp+4jVh0XVBrdFj9lAV4o2YLQ0= +github.com/pluralsh/console-client-go v0.0.59/go.mod h1:u/RjzXE3wtl3L6wiWxwhQHSpxFX46+EYvpkss2mALN4= github.com/pluralsh/polly v0.1.4 h1:Kz90peCgvsfF3ERt8cujr5TR9z4wUlqQE60Eg09ZItY= github.com/pluralsh/polly v0.1.4/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -645,6 +645,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf 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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= From 0fc51c4aee29ff0e970965c85c04b586fd9c02cc Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 13:52:02 +0100 Subject: [PATCH 147/198] add internal package --- controller/{pkg => internal}/client/cluster.go | 0 controller/{pkg => internal}/client/console.go | 0 controller/{pkg => internal}/client/provider.go | 0 controller/{pkg => internal}/client/repository.go | 0 controller/{pkg => internal}/client/service.go | 0 controller/{ => internal}/controllers/cluster_controller.go | 4 ++-- .../{ => internal}/controllers/cluster_controller_test.go | 4 ++-- controller/{ => internal}/controllers/cluster_scope.go | 0 controller/{ => internal}/controllers/common.go | 0 .../{ => internal}/controllers/gitrepository_controller.go | 6 +++--- .../controllers/gitrepository_controller_test.go | 4 ++-- .../{ => internal}/controllers/gitrepository_scope.go | 0 .../{ => internal}/controllers/provider_controller.go | 4 ++-- .../controllers/provider_controller_attributes.go | 2 +- .../{ => internal}/controllers/provider_controller_test.go | 4 ++-- controller/{ => internal}/controllers/provider_scope.go | 0 .../controllers/servicedeployment_controller.go | 6 +++--- .../controllers/servicedeployment_controller_test.go | 4 ++-- .../{ => internal}/controllers/servicedeployment_scope.go | 0 controller/{ => internal}/controllers/suite_test.go | 0 controller/{pkg => internal}/errors/base.go | 0 controller/{pkg => internal}/log/zap.go | 0 controller/{pkg => internal}/test/mocks/ConsoleClient.go | 0 controller/{pkg => internal}/types/controller.go | 0 controller/{pkg => internal}/types/reconciler.go | 4 ++-- controller/{pkg => internal}/utils/kubernetes.go | 0 controller/{pkg => internal}/utils/sha.go | 0 controller/main.go | 4 ++-- 28 files changed, 23 insertions(+), 23 deletions(-) rename controller/{pkg => internal}/client/cluster.go (100%) rename controller/{pkg => internal}/client/console.go (100%) rename controller/{pkg => internal}/client/provider.go (100%) rename controller/{pkg => internal}/client/repository.go (100%) rename controller/{pkg => internal}/client/service.go (100%) rename controller/{ => internal}/controllers/cluster_controller.go (98%) rename controller/{ => internal}/controllers/cluster_controller_test.go (99%) rename controller/{ => internal}/controllers/cluster_scope.go (100%) rename controller/{ => internal}/controllers/common.go (100%) rename controller/{ => internal}/controllers/gitrepository_controller.go (98%) rename controller/{ => internal}/controllers/gitrepository_controller_test.go (98%) rename controller/{ => internal}/controllers/gitrepository_scope.go (100%) rename controller/{ => internal}/controllers/provider_controller.go (98%) rename controller/{ => internal}/controllers/provider_controller_attributes.go (98%) rename controller/{ => internal}/controllers/provider_controller_test.go (98%) rename controller/{ => internal}/controllers/provider_scope.go (100%) rename controller/{ => internal}/controllers/servicedeployment_controller.go (98%) rename controller/{ => internal}/controllers/servicedeployment_controller_test.go (98%) rename controller/{ => internal}/controllers/servicedeployment_scope.go (100%) rename controller/{ => internal}/controllers/suite_test.go (100%) rename controller/{pkg => internal}/errors/base.go (100%) rename controller/{pkg => internal}/log/zap.go (100%) rename controller/{pkg => internal}/test/mocks/ConsoleClient.go (100%) rename controller/{pkg => internal}/types/controller.go (100%) rename controller/{pkg => internal}/types/reconciler.go (95%) rename controller/{pkg => internal}/utils/kubernetes.go (100%) rename controller/{pkg => internal}/utils/sha.go (100%) diff --git a/controller/pkg/client/cluster.go b/controller/internal/client/cluster.go similarity index 100% rename from controller/pkg/client/cluster.go rename to controller/internal/client/cluster.go diff --git a/controller/pkg/client/console.go b/controller/internal/client/console.go similarity index 100% rename from controller/pkg/client/console.go rename to controller/internal/client/console.go diff --git a/controller/pkg/client/provider.go b/controller/internal/client/provider.go similarity index 100% rename from controller/pkg/client/provider.go rename to controller/internal/client/provider.go diff --git a/controller/pkg/client/repository.go b/controller/internal/client/repository.go similarity index 100% rename from controller/pkg/client/repository.go rename to controller/internal/client/repository.go diff --git a/controller/pkg/client/service.go b/controller/internal/client/service.go similarity index 100% rename from controller/pkg/client/service.go rename to controller/internal/client/service.go diff --git a/controller/controllers/cluster_controller.go b/controller/internal/controllers/cluster_controller.go similarity index 98% rename from controller/controllers/cluster_controller.go rename to controller/internal/controllers/cluster_controller.go index 3ef053562..074382143 100644 --- a/controller/controllers/cluster_controller.go +++ b/controller/internal/controllers/cluster_controller.go @@ -7,8 +7,8 @@ import ( "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/utils" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/utils" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/controller/controllers/cluster_controller_test.go b/controller/internal/controllers/cluster_controller_test.go similarity index 99% rename from controller/controllers/cluster_controller_test.go rename to controller/internal/controllers/cluster_controller_test.go index 1316a6316..937ba0eeb 100644 --- a/controller/controllers/cluster_controller_test.go +++ b/controller/internal/controllers/cluster_controller_test.go @@ -6,7 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/controllers" + "github.com/pluralsh/console/controller/internal/controllers" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/pluralsh/console/controller/internal/test/mocks" ) func init() { diff --git a/controller/controllers/cluster_scope.go b/controller/internal/controllers/cluster_scope.go similarity index 100% rename from controller/controllers/cluster_scope.go rename to controller/internal/controllers/cluster_scope.go diff --git a/controller/controllers/common.go b/controller/internal/controllers/common.go similarity index 100% rename from controller/controllers/common.go rename to controller/internal/controllers/common.go diff --git a/controller/controllers/gitrepository_controller.go b/controller/internal/controllers/gitrepository_controller.go similarity index 98% rename from controller/controllers/gitrepository_controller.go rename to controller/internal/controllers/gitrepository_controller.go index 68869211f..08060ce96 100644 --- a/controller/controllers/gitrepository_controller.go +++ b/controller/internal/controllers/gitrepository_controller.go @@ -6,9 +6,9 @@ import ( console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/utils" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/errors" + "github.com/pluralsh/console/controller/internal/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/controller/controllers/gitrepository_controller_test.go b/controller/internal/controllers/gitrepository_controller_test.go similarity index 98% rename from controller/controllers/gitrepository_controller_test.go rename to controller/internal/controllers/gitrepository_controller_test.go index 780904d9d..f0f0fb50f 100644 --- a/controller/controllers/gitrepository_controller_test.go +++ b/controller/internal/controllers/gitrepository_controller_test.go @@ -6,12 +6,12 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/controllers" + "github.com/pluralsh/console/controller/internal/controllers" "github.com/samber/lo" "github.com/stretchr/testify/mock" "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/pluralsh/console/controller/internal/test/mocks" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/controller/controllers/gitrepository_scope.go b/controller/internal/controllers/gitrepository_scope.go similarity index 100% rename from controller/controllers/gitrepository_scope.go rename to controller/internal/controllers/gitrepository_scope.go diff --git a/controller/controllers/provider_controller.go b/controller/internal/controllers/provider_controller.go similarity index 98% rename from controller/controllers/provider_controller.go rename to controller/internal/controllers/provider_controller.go index 9d6d8cfec..829fc1c60 100644 --- a/controller/controllers/provider_controller.go +++ b/controller/internal/controllers/provider_controller.go @@ -16,8 +16,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/utils" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/utils" ) // ProviderReconciler reconciles a v1alpha1.Provider object. diff --git a/controller/controllers/provider_controller_attributes.go b/controller/internal/controllers/provider_controller_attributes.go similarity index 98% rename from controller/controllers/provider_controller_attributes.go rename to controller/internal/controllers/provider_controller_attributes.go index e3041a3ee..c5c186dcb 100644 --- a/controller/controllers/provider_controller_attributes.go +++ b/controller/internal/controllers/provider_controller_attributes.go @@ -7,7 +7,7 @@ import ( console "github.com/pluralsh/console-client-go" corev1 "k8s.io/api/core/v1" - "github.com/pluralsh/console/controller/pkg/utils" + "github.com/pluralsh/console/controller/internal/utils" "github.com/pluralsh/console/controller/api/v1alpha1" ) diff --git a/controller/controllers/provider_controller_test.go b/controller/internal/controllers/provider_controller_test.go similarity index 98% rename from controller/controllers/provider_controller_test.go rename to controller/internal/controllers/provider_controller_test.go index 22bbd1449..07dfd098b 100644 --- a/controller/controllers/provider_controller_test.go +++ b/controller/internal/controllers/provider_controller_test.go @@ -6,7 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/controllers" + "github.com/pluralsh/console/controller/internal/controllers" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -22,7 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/pluralsh/console/controller/internal/test/mocks" ) func init() { diff --git a/controller/controllers/provider_scope.go b/controller/internal/controllers/provider_scope.go similarity index 100% rename from controller/controllers/provider_scope.go rename to controller/internal/controllers/provider_scope.go diff --git a/controller/controllers/servicedeployment_controller.go b/controller/internal/controllers/servicedeployment_controller.go similarity index 98% rename from controller/controllers/servicedeployment_controller.go rename to controller/internal/controllers/servicedeployment_controller.go index 05170b8c3..751a20d49 100644 --- a/controller/controllers/servicedeployment_controller.go +++ b/controller/internal/controllers/servicedeployment_controller.go @@ -7,9 +7,9 @@ import ( console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/errors" - "github.com/pluralsh/console/controller/pkg/utils" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/errors" + "github.com/pluralsh/console/controller/internal/utils" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/controller/controllers/servicedeployment_controller_test.go b/controller/internal/controllers/servicedeployment_controller_test.go similarity index 98% rename from controller/controllers/servicedeployment_controller_test.go rename to controller/internal/controllers/servicedeployment_controller_test.go index 9dceb2d6e..decbd8730 100644 --- a/controller/controllers/servicedeployment_controller_test.go +++ b/controller/internal/controllers/servicedeployment_controller_test.go @@ -8,8 +8,8 @@ import ( gqlclient "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/controllers" - "github.com/pluralsh/console/controller/pkg/test/mocks" + "github.com/pluralsh/console/controller/internal/controllers" + "github.com/pluralsh/console/controller/internal/test/mocks" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/controller/controllers/servicedeployment_scope.go b/controller/internal/controllers/servicedeployment_scope.go similarity index 100% rename from controller/controllers/servicedeployment_scope.go rename to controller/internal/controllers/servicedeployment_scope.go diff --git a/controller/controllers/suite_test.go b/controller/internal/controllers/suite_test.go similarity index 100% rename from controller/controllers/suite_test.go rename to controller/internal/controllers/suite_test.go diff --git a/controller/pkg/errors/base.go b/controller/internal/errors/base.go similarity index 100% rename from controller/pkg/errors/base.go rename to controller/internal/errors/base.go diff --git a/controller/pkg/log/zap.go b/controller/internal/log/zap.go similarity index 100% rename from controller/pkg/log/zap.go rename to controller/internal/log/zap.go diff --git a/controller/pkg/test/mocks/ConsoleClient.go b/controller/internal/test/mocks/ConsoleClient.go similarity index 100% rename from controller/pkg/test/mocks/ConsoleClient.go rename to controller/internal/test/mocks/ConsoleClient.go diff --git a/controller/pkg/types/controller.go b/controller/internal/types/controller.go similarity index 100% rename from controller/pkg/types/controller.go rename to controller/internal/types/controller.go diff --git a/controller/pkg/types/reconciler.go b/controller/internal/types/reconciler.go similarity index 95% rename from controller/pkg/types/reconciler.go rename to controller/internal/types/reconciler.go index ad8e887dc..152490ae9 100644 --- a/controller/pkg/types/reconciler.go +++ b/controller/internal/types/reconciler.go @@ -3,8 +3,8 @@ package types import ( "fmt" - "github.com/pluralsh/console/controller/controllers" - "github.com/pluralsh/console/controller/pkg/client" + "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/controllers" ctrl "sigs.k8s.io/controller-runtime" ) diff --git a/controller/pkg/utils/kubernetes.go b/controller/internal/utils/kubernetes.go similarity index 100% rename from controller/pkg/utils/kubernetes.go rename to controller/internal/utils/kubernetes.go diff --git a/controller/pkg/utils/sha.go b/controller/internal/utils/sha.go similarity index 100% rename from controller/pkg/utils/sha.go rename to controller/internal/utils/sha.go diff --git a/controller/main.go b/controller/main.go index e272fd4c9..6f0dd625d 100644 --- a/controller/main.go +++ b/controller/main.go @@ -15,8 +15,8 @@ import ( ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" deploymentsv1alpha "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/pkg/client" - "github.com/pluralsh/console/controller/pkg/types" + "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/types" ) var ( From c0c21276803724d7094cd7419d3fffb8273aae4d Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 13:54:47 +0100 Subject: [PATCH 148/198] clean up api dir --- .../v1alpha1/{cluster.go => cluster_types.go} | 0 .../v1alpha1/{common.go => common_types.go} | 0 controller/api/v1alpha1/doc.go | 19 ------------------- ...t_repository.go => gitrepository_types.go} | 0 .../{provider.go => provider_types.go} | 0 controller/api/v1alpha1/register.go | 12 ------------ ...rvice.go => servicedeployment_types.go.go} | 0 7 files changed, 31 deletions(-) rename controller/api/v1alpha1/{cluster.go => cluster_types.go} (100%) rename controller/api/v1alpha1/{common.go => common_types.go} (100%) delete mode 100644 controller/api/v1alpha1/doc.go rename controller/api/v1alpha1/{git_repository.go => gitrepository_types.go} (100%) rename controller/api/v1alpha1/{provider.go => provider_types.go} (100%) delete mode 100644 controller/api/v1alpha1/register.go rename controller/api/v1alpha1/{service.go => servicedeployment_types.go.go} (100%) diff --git a/controller/api/v1alpha1/cluster.go b/controller/api/v1alpha1/cluster_types.go similarity index 100% rename from controller/api/v1alpha1/cluster.go rename to controller/api/v1alpha1/cluster_types.go diff --git a/controller/api/v1alpha1/common.go b/controller/api/v1alpha1/common_types.go similarity index 100% rename from controller/api/v1alpha1/common.go rename to controller/api/v1alpha1/common_types.go diff --git a/controller/api/v1alpha1/doc.go b/controller/api/v1alpha1/doc.go deleted file mode 100644 index be42d6244..000000000 --- a/controller/api/v1alpha1/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -/* -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. -*/ - -// +groupName=deployments.plural.sh - -package v1alpha1 diff --git a/controller/api/v1alpha1/git_repository.go b/controller/api/v1alpha1/gitrepository_types.go similarity index 100% rename from controller/api/v1alpha1/git_repository.go rename to controller/api/v1alpha1/gitrepository_types.go diff --git a/controller/api/v1alpha1/provider.go b/controller/api/v1alpha1/provider_types.go similarity index 100% rename from controller/api/v1alpha1/provider.go rename to controller/api/v1alpha1/provider_types.go diff --git a/controller/api/v1alpha1/register.go b/controller/api/v1alpha1/register.go deleted file mode 100644 index 2dfd631eb..000000000 --- a/controller/api/v1alpha1/register.go +++ /dev/null @@ -1,12 +0,0 @@ -package v1alpha1 - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// SchemeGroupVersion is group version used to register these objects. -var SchemeGroupVersion = GroupVersion - -func Resource(resource string) schema.GroupResource { - return SchemeGroupVersion.WithResource(resource).GroupResource() -} diff --git a/controller/api/v1alpha1/service.go b/controller/api/v1alpha1/servicedeployment_types.go.go similarity index 100% rename from controller/api/v1alpha1/service.go rename to controller/api/v1alpha1/servicedeployment_types.go.go From e40ab5fc1f71c39a1574a400bd3123e0272ae1de Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 13:58:38 +0100 Subject: [PATCH 149/198] update samples --- controller/config/crd/samples/provider_gcp.yaml | 14 -------------- .../config/{crd => }/samples/all_in_one_gcp.yaml | 0 .../config/{crd => }/samples/cluster_aws.yaml | 0 .../config/{crd => }/samples/cluster_byok.yaml | 7 +------ .../config/{crd => }/samples/cluster_existing.yaml | 0 .../config/{crd => }/samples/git_repository.yaml | 0 .../{crd => }/samples/provider_aws_readonly.yaml | 0 controller/config/samples/provider_gcp.yaml | 7 +++++++ .../{crd => }/samples/service_deployment.yaml | 0 9 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 controller/config/crd/samples/provider_gcp.yaml rename controller/config/{crd => }/samples/all_in_one_gcp.yaml (100%) rename controller/config/{crd => }/samples/cluster_aws.yaml (100%) rename controller/config/{crd => }/samples/cluster_byok.yaml (51%) rename controller/config/{crd => }/samples/cluster_existing.yaml (100%) rename controller/config/{crd => }/samples/git_repository.yaml (100%) rename controller/config/{crd => }/samples/provider_aws_readonly.yaml (100%) create mode 100644 controller/config/samples/provider_gcp.yaml rename controller/config/{crd => }/samples/service_deployment.yaml (100%) diff --git a/controller/config/crd/samples/provider_gcp.yaml b/controller/config/crd/samples/provider_gcp.yaml deleted file mode 100644 index 86c5d4e26..000000000 --- a/controller/config/crd/samples/provider_gcp.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: deployments.plural.sh/v1alpha1 -kind: Provider -metadata: - name: gcp -spec: - name: gcp - namespace: operator - cloud: gcp - cloudSettings: - gcp: - name: credentials - namespace: operator - diff --git a/controller/config/crd/samples/all_in_one_gcp.yaml b/controller/config/samples/all_in_one_gcp.yaml similarity index 100% rename from controller/config/crd/samples/all_in_one_gcp.yaml rename to controller/config/samples/all_in_one_gcp.yaml diff --git a/controller/config/crd/samples/cluster_aws.yaml b/controller/config/samples/cluster_aws.yaml similarity index 100% rename from controller/config/crd/samples/cluster_aws.yaml rename to controller/config/samples/cluster_aws.yaml diff --git a/controller/config/crd/samples/cluster_byok.yaml b/controller/config/samples/cluster_byok.yaml similarity index 51% rename from controller/config/crd/samples/cluster_byok.yaml rename to controller/config/samples/cluster_byok.yaml index ded3fe292..05fa838d4 100644 --- a/controller/config/crd/samples/cluster_byok.yaml +++ b/controller/config/samples/cluster_byok.yaml @@ -3,9 +3,4 @@ kind: Cluster metadata: name: byok namespace: default -spec: - handle: byok - cloud: byok - protect: false - tags: - managed-by: plural-controller + diff --git a/controller/config/crd/samples/cluster_existing.yaml b/controller/config/samples/cluster_existing.yaml similarity index 100% rename from controller/config/crd/samples/cluster_existing.yaml rename to controller/config/samples/cluster_existing.yaml diff --git a/controller/config/crd/samples/git_repository.yaml b/controller/config/samples/git_repository.yaml similarity index 100% rename from controller/config/crd/samples/git_repository.yaml rename to controller/config/samples/git_repository.yaml diff --git a/controller/config/crd/samples/provider_aws_readonly.yaml b/controller/config/samples/provider_aws_readonly.yaml similarity index 100% rename from controller/config/crd/samples/provider_aws_readonly.yaml rename to controller/config/samples/provider_aws_readonly.yaml diff --git a/controller/config/samples/provider_gcp.yaml b/controller/config/samples/provider_gcp.yaml new file mode 100644 index 000000000..7c02c445e --- /dev/null +++ b/controller/config/samples/provider_gcp.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: Provider +metadata: + name: gcp + + diff --git a/controller/config/crd/samples/service_deployment.yaml b/controller/config/samples/service_deployment.yaml similarity index 100% rename from controller/config/crd/samples/service_deployment.yaml rename to controller/config/samples/service_deployment.yaml From 33b9445fd46c15a8f3866b8f641db499e41618a3 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 14:13:49 +0100 Subject: [PATCH 150/198] regenerate cluster with v4 plugin and v3 kubebuilder --- controller/PROJECT | 11 ++++- controller/api/v1alpha1/cluster_types.go | 2 +- controller/{ => cmd}/main.go | 0 controller/config/crd/kustomization.yaml | 5 +++ .../config/rbac/cluster_editor_role.yaml | 43 +++++++++++-------- .../config/rbac/cluster_viewer_role.yaml | 35 +++++++++------ controller/config/rbac/role.yaml | 6 +++ .../samples/deployments_v1alpha1_cluster.yaml | 6 +++ .../deployments_v1alpha1_gitrepository.yaml | 6 --- .../deployments_v1alpha1_provider.yaml | 6 --- ...eployments_v1alpha1_servicedeployment.yaml | 6 --- controller/config/samples/kustomization.yaml | 4 ++ .../cluster_controller.go | 5 +-- .../cluster_controller_test.go | 2 +- .../cluster_scope.go | 2 +- .../{controllers => controller}/common.go | 2 +- .../gitrepository_controller.go | 2 +- .../gitrepository_controller_test.go | 2 +- .../gitrepository_scope.go | 2 +- .../provider_controller.go | 2 +- .../provider_controller_attributes.go | 2 +- .../provider_controller_test.go | 2 +- .../provider_scope.go | 2 +- .../servicedeployment_controller.go | 2 +- .../servicedeployment_controller_test.go | 2 +- .../servicedeployment_scope.go | 2 +- .../{controllers => controller}/suite_test.go | 18 ++++++-- controller/internal/types/reconciler.go | 11 +++-- 28 files changed, 112 insertions(+), 78 deletions(-) rename controller/{ => cmd}/main.go (100%) delete mode 100644 controller/config/samples/deployments_v1alpha1_gitrepository.yaml delete mode 100644 controller/config/samples/deployments_v1alpha1_provider.yaml delete mode 100644 controller/config/samples/deployments_v1alpha1_servicedeployment.yaml create mode 100644 controller/config/samples/kustomization.yaml rename controller/internal/{controllers => controller}/cluster_controller.go (98%) rename controller/internal/{controllers => controller}/cluster_controller_test.go (99%) rename controller/internal/{controllers => controller}/cluster_scope.go (97%) rename controller/internal/{controllers => controller}/common.go (92%) rename controller/internal/{controllers => controller}/gitrepository_controller.go (99%) rename controller/internal/{controllers => controller}/gitrepository_controller_test.go (99%) rename controller/internal/{controllers => controller}/gitrepository_scope.go (98%) rename controller/internal/{controllers => controller}/provider_controller.go (99%) rename controller/internal/{controllers => controller}/provider_controller_attributes.go (99%) rename controller/internal/{controllers => controller}/provider_controller_test.go (99%) rename controller/internal/{controllers => controller}/provider_scope.go (97%) rename controller/internal/{controllers => controller}/servicedeployment_controller.go (99%) rename controller/internal/{controllers => controller}/servicedeployment_controller_test.go (99%) rename controller/internal/{controllers => controller}/servicedeployment_scope.go (97%) rename controller/internal/{controllers => controller}/suite_test.go (73%) diff --git a/controller/PROJECT b/controller/PROJECT index 93b45fae8..9cef0774b 100644 --- a/controller/PROJECT +++ b/controller/PROJECT @@ -17,4 +17,13 @@ resources: - group: deployments kind: GitRepository version: v1alpha1 -version: "2" +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: plural.sh + group: deployments + kind: Cluster2 + path: github.com/pluralsh/console/controller/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/controller/api/v1alpha1/cluster_types.go b/controller/api/v1alpha1/cluster_types.go index 6278260c1..213fab4b4 100644 --- a/controller/api/v1alpha1/cluster_types.go +++ b/controller/api/v1alpha1/cluster_types.go @@ -17,7 +17,7 @@ func init() { SchemeBuilder.Register(&Cluster{}, &ClusterList{}) } -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true type ClusterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` diff --git a/controller/main.go b/controller/cmd/main.go similarity index 100% rename from controller/main.go rename to controller/cmd/main.go diff --git a/controller/config/crd/kustomization.yaml b/controller/config/crd/kustomization.yaml index ab00c0151..358c03d72 100644 --- a/controller/config/crd/kustomization.yaml +++ b/controller/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/deployments.plural.sh_providers.yaml - bases/deployments.plural.sh_gitrepositories.yaml - bases/deployments.plural.sh_servicedeployments.yaml +- bases/deployments.plural.sh_cluster2s.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -15,6 +16,8 @@ patchesStrategicMerge: #- patches/webhook_in_providers.yaml #- patches/webhook_in_gitrepositories.yaml #- patches/webhook_in_servicedeployments.yaml +#- path: patches/webhook_in_clusters.yaml +#- path: patches/webhook_in_cluster2s.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -23,6 +26,8 @@ patchesStrategicMerge: #- patches/cainjection_in_providers.yaml #- patches/cainjection_in_gitrepositories.yaml #- patches/cainjection_in_servicedeployments.yaml +#- path: patches/cainjection_in_clusters.yaml +#- path: patches/cainjection_in_cluster2s.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/controller/config/rbac/cluster_editor_role.yaml b/controller/config/rbac/cluster_editor_role.yaml index 7f5916279..29a3cbde1 100644 --- a/controller/config/rbac/cluster_editor_role.yaml +++ b/controller/config/rbac/cluster_editor_role.yaml @@ -2,23 +2,30 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: cluster-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize name: cluster-editor-role rules: -- apiGroups: - - deployments.plural.sh - resources: - - clusters - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - deployments.plural.sh - resources: - - clusters/status - verbs: - - get + - apiGroups: + - deployments.plural.sh + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - deployments.plural.sh + resources: + - clusters/status + verbs: + - get diff --git a/controller/config/rbac/cluster_viewer_role.yaml b/controller/config/rbac/cluster_viewer_role.yaml index 7f3f22251..89822fd4a 100644 --- a/controller/config/rbac/cluster_viewer_role.yaml +++ b/controller/config/rbac/cluster_viewer_role.yaml @@ -2,19 +2,26 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: cluster-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize name: cluster-viewer-role rules: -- apiGroups: - - deployments.plural.sh - resources: - - clusters - verbs: - - get - - list - - watch -- apiGroups: - - deployments.plural.sh - resources: - - clusters/status - verbs: - - get + - apiGroups: + - deployments.plural.sh + resources: + - clusters + verbs: + - get + - list + - watch + - apiGroups: + - deployments.plural.sh + resources: + - clusters/status + verbs: + - get diff --git a/controller/config/rbac/role.yaml b/controller/config/rbac/role.yaml index ff92f6066..2c901c947 100644 --- a/controller/config/rbac/role.yaml +++ b/controller/config/rbac/role.yaml @@ -16,6 +16,12 @@ rules: - patch - update - watch +- apiGroups: + - deployments.plural.sh + resources: + - clusters/finalizers + verbs: + - update - apiGroups: - deployments.plural.sh resources: diff --git a/controller/config/samples/deployments_v1alpha1_cluster.yaml b/controller/config/samples/deployments_v1alpha1_cluster.yaml index 179b2e59f..eba41d9fe 100644 --- a/controller/config/samples/deployments_v1alpha1_cluster.yaml +++ b/controller/config/samples/deployments_v1alpha1_cluster.yaml @@ -1,6 +1,12 @@ apiVersion: deployments.plural.sh/v1alpha1 kind: Cluster metadata: + labels: + app.kubernetes.io/name: cluster + app.kubernetes.io/instance: cluster-sample + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: name: cluster-sample spec: # TODO(user): Add fields here diff --git a/controller/config/samples/deployments_v1alpha1_gitrepository.yaml b/controller/config/samples/deployments_v1alpha1_gitrepository.yaml deleted file mode 100644 index e1fa2ca5c..000000000 --- a/controller/config/samples/deployments_v1alpha1_gitrepository.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: deployments.plural.sh/v1alpha1 -kind: GitRepository -metadata: - name: gitrepository-sample -spec: - # TODO(user): Add fields here diff --git a/controller/config/samples/deployments_v1alpha1_provider.yaml b/controller/config/samples/deployments_v1alpha1_provider.yaml deleted file mode 100644 index 33538183d..000000000 --- a/controller/config/samples/deployments_v1alpha1_provider.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: deployments.plural.sh/v1alpha1 -kind: Provider -metadata: - name: provider-sample -spec: - # TODO(user): Add fields here diff --git a/controller/config/samples/deployments_v1alpha1_servicedeployment.yaml b/controller/config/samples/deployments_v1alpha1_servicedeployment.yaml deleted file mode 100644 index 0af76eddf..000000000 --- a/controller/config/samples/deployments_v1alpha1_servicedeployment.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: deployments.plural.sh/v1alpha1 -kind: ServiceDeployment -metadata: - name: servicedeployment-sample -spec: - # TODO(user): Add fields here diff --git a/controller/config/samples/kustomization.yaml b/controller/config/samples/kustomization.yaml new file mode 100644 index 000000000..263935233 --- /dev/null +++ b/controller/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- deployments_v1alpha1_cluster.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controller/internal/controllers/cluster_controller.go b/controller/internal/controller/cluster_controller.go similarity index 98% rename from controller/internal/controllers/cluster_controller.go rename to controller/internal/controller/cluster_controller.go index 074382143..2ac1406f9 100644 --- a/controller/internal/controllers/cluster_controller.go +++ b/controller/internal/controller/cluster_controller.go @@ -1,10 +1,9 @@ -package controllers +package controller import ( "context" "fmt" - "github.com/go-logr/logr" console "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/api/v1alpha1" consoleclient "github.com/pluralsh/console/controller/internal/client" @@ -28,7 +27,6 @@ const ( type ClusterReconciler struct { client.Client ConsoleClient consoleclient.ConsoleClient - Log logr.Logger Scheme *runtime.Scheme } @@ -41,6 +39,7 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { //+kubebuilder:rbac:groups=deployments.plural.sh,resources=clusters,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=deployments.plural.sh,resources=clusters/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=clusters/finalizers,verbs=update func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { logger := log.FromContext(ctx) diff --git a/controller/internal/controllers/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go similarity index 99% rename from controller/internal/controllers/cluster_controller_test.go rename to controller/internal/controller/cluster_controller_test.go index 937ba0eeb..a90ce072e 100644 --- a/controller/internal/controllers/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -1,4 +1,4 @@ -package controllers_test +package controller_test import ( "context" diff --git a/controller/internal/controllers/cluster_scope.go b/controller/internal/controller/cluster_scope.go similarity index 97% rename from controller/internal/controllers/cluster_scope.go rename to controller/internal/controller/cluster_scope.go index 893594027..3e7d907c4 100644 --- a/controller/internal/controllers/cluster_scope.go +++ b/controller/internal/controller/cluster_scope.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/common.go b/controller/internal/controller/common.go similarity index 92% rename from controller/internal/controllers/common.go rename to controller/internal/controller/common.go index e58704f80..7e3c0c967 100644 --- a/controller/internal/controllers/common.go +++ b/controller/internal/controller/common.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "time" diff --git a/controller/internal/controllers/gitrepository_controller.go b/controller/internal/controller/gitrepository_controller.go similarity index 99% rename from controller/internal/controllers/gitrepository_controller.go rename to controller/internal/controller/gitrepository_controller.go index 08060ce96..2e0da315a 100644 --- a/controller/internal/controllers/gitrepository_controller.go +++ b/controller/internal/controller/gitrepository_controller.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/gitrepository_controller_test.go b/controller/internal/controller/gitrepository_controller_test.go similarity index 99% rename from controller/internal/controllers/gitrepository_controller_test.go rename to controller/internal/controller/gitrepository_controller_test.go index f0f0fb50f..1fa125076 100644 --- a/controller/internal/controllers/gitrepository_controller_test.go +++ b/controller/internal/controller/gitrepository_controller_test.go @@ -1,4 +1,4 @@ -package controllers_test +package controller_test import ( "context" diff --git a/controller/internal/controllers/gitrepository_scope.go b/controller/internal/controller/gitrepository_scope.go similarity index 98% rename from controller/internal/controllers/gitrepository_scope.go rename to controller/internal/controller/gitrepository_scope.go index 80d794628..7133550c5 100644 --- a/controller/internal/controllers/gitrepository_scope.go +++ b/controller/internal/controller/gitrepository_scope.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/provider_controller.go b/controller/internal/controller/provider_controller.go similarity index 99% rename from controller/internal/controllers/provider_controller.go rename to controller/internal/controller/provider_controller.go index 829fc1c60..525266e67 100644 --- a/controller/internal/controllers/provider_controller.go +++ b/controller/internal/controller/provider_controller.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/provider_controller_attributes.go b/controller/internal/controller/provider_controller_attributes.go similarity index 99% rename from controller/internal/controllers/provider_controller_attributes.go rename to controller/internal/controller/provider_controller_attributes.go index c5c186dcb..13b549d76 100644 --- a/controller/internal/controllers/provider_controller_attributes.go +++ b/controller/internal/controller/provider_controller_attributes.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/provider_controller_test.go b/controller/internal/controller/provider_controller_test.go similarity index 99% rename from controller/internal/controllers/provider_controller_test.go rename to controller/internal/controller/provider_controller_test.go index 07dfd098b..03c1e53f6 100644 --- a/controller/internal/controllers/provider_controller_test.go +++ b/controller/internal/controller/provider_controller_test.go @@ -1,4 +1,4 @@ -package controllers_test +package controller_test import ( "context" diff --git a/controller/internal/controllers/provider_scope.go b/controller/internal/controller/provider_scope.go similarity index 97% rename from controller/internal/controllers/provider_scope.go rename to controller/internal/controller/provider_scope.go index e9b27989b..3fab085f0 100644 --- a/controller/internal/controllers/provider_scope.go +++ b/controller/internal/controller/provider_scope.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/servicedeployment_controller.go b/controller/internal/controller/servicedeployment_controller.go similarity index 99% rename from controller/internal/controllers/servicedeployment_controller.go rename to controller/internal/controller/servicedeployment_controller.go index 751a20d49..9b5f48223 100644 --- a/controller/internal/controllers/servicedeployment_controller.go +++ b/controller/internal/controller/servicedeployment_controller.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/servicedeployment_controller_test.go b/controller/internal/controller/servicedeployment_controller_test.go similarity index 99% rename from controller/internal/controllers/servicedeployment_controller_test.go rename to controller/internal/controller/servicedeployment_controller_test.go index decbd8730..cd48e2ab1 100644 --- a/controller/internal/controllers/servicedeployment_controller_test.go +++ b/controller/internal/controller/servicedeployment_controller_test.go @@ -1,4 +1,4 @@ -package controllers_test +package controller_test import ( "context" diff --git a/controller/internal/controllers/servicedeployment_scope.go b/controller/internal/controller/servicedeployment_scope.go similarity index 97% rename from controller/internal/controllers/servicedeployment_scope.go rename to controller/internal/controller/servicedeployment_scope.go index 14337fc50..4d4ddb4ef 100644 --- a/controller/internal/controllers/servicedeployment_scope.go +++ b/controller/internal/controller/servicedeployment_scope.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "context" diff --git a/controller/internal/controllers/suite_test.go b/controller/internal/controller/suite_test.go similarity index 73% rename from controller/internal/controllers/suite_test.go rename to controller/internal/controller/suite_test.go index ea5f8444e..5f4d17e2d 100644 --- a/controller/internal/controllers/suite_test.go +++ b/controller/internal/controller/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Kubernetes authors. +Copyright 2023. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( + "fmt" "path/filepath" + "runtime" "testing" . "github.com/onsi/ginkgo/v2" @@ -41,7 +43,7 @@ var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment -func TestAPIs(t *testing.T) { +func TestControllers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Controller Suite") @@ -52,8 +54,16 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error diff --git a/controller/internal/types/reconciler.go b/controller/internal/types/reconciler.go index 152490ae9..81c9dcdb8 100644 --- a/controller/internal/types/reconciler.go +++ b/controller/internal/types/reconciler.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/pluralsh/console/controller/internal/client" - "github.com/pluralsh/console/controller/internal/controllers" + "github.com/pluralsh/console/controller/internal/controller" ctrl "sigs.k8s.io/controller-runtime" ) @@ -38,26 +38,25 @@ func ToReconciler(reconciler string) (Reconciler, error) { func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.ConsoleClient) (Controller, error) { switch sc { case GitRepositoryReconciler: - return &controllers.GitRepositoryReconciler{ + return &controller.GitRepositoryReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ServiceDeploymentReconciler: - return &controllers.ServiceReconciler{ + return &controller.ServiceReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), }, nil case ClusterReconciler: - return &controllers.ClusterReconciler{ + return &controller.ClusterReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, - Log: mgr.GetLogger(), Scheme: mgr.GetScheme(), }, nil case ProviderReconciler: - return &controllers.ProviderReconciler{ + return &controller.ProviderReconciler{ Client: mgr.GetClient(), ConsoleClient: consoleClient, Scheme: mgr.GetScheme(), From 4f9eda9c9bd8fcd123de30fa3c186f562001c6be Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 14:21:58 +0100 Subject: [PATCH 151/198] update tests --- controller/Makefile | 2 +- controller/api/v1alpha1/zz_generated.deepcopy.go | 2 +- controller/hack/boilerplate.go.txt | 2 +- controller/hack/gen-client-mocks.sh | 2 +- .../internal/controller/cluster_controller_test.go | 12 ++++-------- .../controller/gitrepository_controller_test.go | 8 ++++---- .../internal/controller/provider_controller_test.go | 8 ++++---- .../controller/servicedeployment_controller_test.go | 10 +++++----- 8 files changed, 21 insertions(+), 25 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 6d5f2ccdf..63446a0ae 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -44,7 +44,7 @@ install-kubebuilder: ## install kubebuilder .PHONY: build build: manifests generate ## build - go build -o bin/deployment-controller main.go + go build -o bin/deployment-controller cmd/main.go .PHONY: release release: manifests generate ## builds release version of the app. Requires GoReleaser to work. diff --git a/controller/api/v1alpha1/zz_generated.deepcopy.go b/controller/api/v1alpha1/zz_generated.deepcopy.go index 55ab21d71..244afba83 100644 --- a/controller/api/v1alpha1/zz_generated.deepcopy.go +++ b/controller/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2023. +Copyright 2023 The Kubernetes authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/controller/hack/boilerplate.go.txt b/controller/hack/boilerplate.go.txt index 65b862271..8c36d1245 100644 --- a/controller/hack/boilerplate.go.txt +++ b/controller/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2023. +Copyright 2023 The Kubernetes authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/controller/hack/gen-client-mocks.sh b/controller/hack/gen-client-mocks.sh index 76965cc77..511a982df 100755 --- a/controller/hack/gen-client-mocks.sh +++ b/controller/hack/gen-client-mocks.sh @@ -9,4 +9,4 @@ source hack/lib.sh CONTAINERIZE_IMAGE=golang:1.21.1 containerize ./hack/gen-client-mocks.sh -go run github.com/vektra/mockery/v2@latest --dir=pkg/client --name=ConsoleClient --output=pkg/test/mocks \ No newline at end of file +go run github.com/vektra/mockery/v2@latest --dir=internal/client --name=ConsoleClient --output=internal/test/mocks \ No newline at end of file diff --git a/controller/internal/controller/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go index a90ce072e..012979755 100644 --- a/controller/internal/controller/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -6,7 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/internal/controllers" + "github.com/pluralsh/console/controller/internal/controller" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -17,7 +17,6 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -149,9 +148,8 @@ func TestCreateNewCluster(t *testing.T) { ctx := context.Background() - target := &controllers.ClusterReconciler{ + target := &controller.ClusterReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -305,9 +303,8 @@ func TestUpdateCluster(t *testing.T) { ctx := context.Background() - target := &controllers.ClusterReconciler{ + target := &controller.ClusterReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } @@ -415,9 +412,8 @@ func TestAdoptExistingCluster(t *testing.T) { ctx := context.Background() - target := &controllers.ClusterReconciler{ + target := &controller.ClusterReconciler{ Client: fakeClient, - Log: ctrl.Log.WithName("reconcilers").WithName("ClusterReconciler"), Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, } diff --git a/controller/internal/controller/gitrepository_controller_test.go b/controller/internal/controller/gitrepository_controller_test.go index 1fa125076..0ec2ed437 100644 --- a/controller/internal/controller/gitrepository_controller_test.go +++ b/controller/internal/controller/gitrepository_controller_test.go @@ -6,7 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/internal/controllers" + "github.com/pluralsh/console/controller/internal/controller" "github.com/samber/lo" "github.com/stretchr/testify/mock" @@ -100,7 +100,7 @@ func TestCreateNewRepository(t *testing.T) { // act ctx := context.Background() - target := &controllers.GitRepositoryReconciler{ + target := &controller.GitRepositoryReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -197,7 +197,7 @@ func TestUpdateRepository(t *testing.T) { // act ctx := context.Background() - target := &controllers.GitRepositoryReconciler{ + target := &controller.GitRepositoryReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -280,7 +280,7 @@ func TestImportRepository(t *testing.T) { // act ctx := context.Background() - target := &controllers.GitRepositoryReconciler{ + target := &controller.GitRepositoryReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, diff --git a/controller/internal/controller/provider_controller_test.go b/controller/internal/controller/provider_controller_test.go index 03c1e53f6..a4b69c701 100644 --- a/controller/internal/controller/provider_controller_test.go +++ b/controller/internal/controller/provider_controller_test.go @@ -6,7 +6,7 @@ import ( "testing" gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/internal/controllers" + "github.com/pluralsh/console/controller/internal/controller" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -112,7 +112,7 @@ func TestCreateNewProvider(t *testing.T) { // act ctx := context.Background() - providerReconciler := &controllers.ProviderReconciler{ + providerReconciler := &controller.ProviderReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -207,7 +207,7 @@ func TestAdoptProvider(t *testing.T) { // act ctx := context.Background() - providerReconciler := &controllers.ProviderReconciler{ + providerReconciler := &controller.ProviderReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -311,7 +311,7 @@ func TestUpdateProvider(t *testing.T) { // act ctx := context.Background() - providerReconciler := &controllers.ProviderReconciler{ + providerReconciler := &controller.ProviderReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, diff --git a/controller/internal/controller/servicedeployment_controller_test.go b/controller/internal/controller/servicedeployment_controller_test.go index cd48e2ab1..06b523732 100644 --- a/controller/internal/controller/servicedeployment_controller_test.go +++ b/controller/internal/controller/servicedeployment_controller_test.go @@ -8,7 +8,7 @@ import ( gqlclient "github.com/pluralsh/console-client-go" "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/controllers" + "github.com/pluralsh/console/controller/internal/controller" "github.com/pluralsh/console/controller/internal/test/mocks" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -103,7 +103,7 @@ func TestCreateNewService(t *testing.T) { ctx := context.Background() - target := &controllers.ServiceReconciler{ + target := &controller.ServiceReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -165,7 +165,7 @@ func TestDeleteService(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: serviceName, DeletionTimestamp: &metav1.Time{Time: time.Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, - Finalizers: []string{controllers.ServiceFinalizer}, + Finalizers: []string{controller.ServiceFinalizer}, }, Spec: v1alpha1.ServiceSpec{ Version: "1.24", @@ -205,7 +205,7 @@ func TestDeleteService(t *testing.T) { fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() ctx := context.Background() - target := &controllers.ServiceReconciler{ + target := &controller.ServiceReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, @@ -299,7 +299,7 @@ func TestUpdateService(t *testing.T) { ctx := context.Background() - target := &controllers.ServiceReconciler{ + target := &controller.ServiceReconciler{ Client: fakeClient, Scheme: scheme.Scheme, ConsoleClient: fakeConsoleClient, From 2022cd2f2f382f3193318bd04c8b7955113a1dda Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 14:43:18 +0100 Subject: [PATCH 152/198] add labels to rbac resources --- controller/config/rbac/gitrepository_editor_role.yaml | 7 +++++++ controller/config/rbac/gitrepository_viewer_role.yaml | 7 +++++++ controller/config/rbac/provider_editor_role.yaml | 7 +++++++ controller/config/rbac/provider_viewer_role.yaml | 7 +++++++ controller/config/rbac/servicedeployment_editor_role.yaml | 7 +++++++ controller/config/rbac/servicedeployment_viewer_role.yaml | 7 +++++++ 6 files changed, 42 insertions(+) diff --git a/controller/config/rbac/gitrepository_editor_role.yaml b/controller/config/rbac/gitrepository_editor_role.yaml index ebccc3ab0..ee7bbef90 100644 --- a/controller/config/rbac/gitrepository_editor_role.yaml +++ b/controller/config/rbac/gitrepository_editor_role.yaml @@ -3,6 +3,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: gitrepository-editor-role + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: gitrepository-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize rules: - apiGroups: - deployments.plural.sh diff --git a/controller/config/rbac/gitrepository_viewer_role.yaml b/controller/config/rbac/gitrepository_viewer_role.yaml index 192bf7621..fc78ff554 100644 --- a/controller/config/rbac/gitrepository_viewer_role.yaml +++ b/controller/config/rbac/gitrepository_viewer_role.yaml @@ -3,6 +3,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: gitrepository-viewer-role + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: gitrepository-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize rules: - apiGroups: - deployments.plural.sh diff --git a/controller/config/rbac/provider_editor_role.yaml b/controller/config/rbac/provider_editor_role.yaml index f457dca83..2e0c1b5fb 100644 --- a/controller/config/rbac/provider_editor_role.yaml +++ b/controller/config/rbac/provider_editor_role.yaml @@ -3,6 +3,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: provider-editor-role + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: provider-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize rules: - apiGroups: - deployments.plural.sh diff --git a/controller/config/rbac/provider_viewer_role.yaml b/controller/config/rbac/provider_viewer_role.yaml index 1bc0bb101..b01774c84 100644 --- a/controller/config/rbac/provider_viewer_role.yaml +++ b/controller/config/rbac/provider_viewer_role.yaml @@ -3,6 +3,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: provider-viewer-role + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: provider-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize rules: - apiGroups: - deployments.plural.sh diff --git a/controller/config/rbac/servicedeployment_editor_role.yaml b/controller/config/rbac/servicedeployment_editor_role.yaml index 35764d50c..0250569fa 100644 --- a/controller/config/rbac/servicedeployment_editor_role.yaml +++ b/controller/config/rbac/servicedeployment_editor_role.yaml @@ -3,6 +3,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: servicedeployment-editor-role + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: servicedeployment-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize rules: - apiGroups: - deployments.plural.sh diff --git a/controller/config/rbac/servicedeployment_viewer_role.yaml b/controller/config/rbac/servicedeployment_viewer_role.yaml index 1ae398a72..f8972401a 100644 --- a/controller/config/rbac/servicedeployment_viewer_role.yaml +++ b/controller/config/rbac/servicedeployment_viewer_role.yaml @@ -3,6 +3,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: servicedeployment-viewer-role + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: servicedeployment-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: + app.kubernetes.io/part-of: + app.kubernetes.io/managed-by: kustomize rules: - apiGroups: - deployments.plural.sh From 525297f759579062c58d22f3ead3e193d8ddaab2 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 14:45:12 +0100 Subject: [PATCH 153/198] update PROJECT file --- controller/PROJECT | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/controller/PROJECT b/controller/PROJECT index 9cef0774b..49c1dbd78 100644 --- a/controller/PROJECT +++ b/controller/PROJECT @@ -5,17 +5,32 @@ domain: plural.sh repo: github.com/pluralsh/console/controller resources: -- group: deployments +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: plural.sh + group: deployments kind: Cluster + path: github.com/pluralsh/console/controller/api/v1alpha1 version: v1alpha1 -- group: deployments +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: plural.sh + group: deployments kind: Provider + path: github.com/pluralsh/console/controller/api/v1alpha1 version: v1alpha1 -- group: deployments +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: plural.sh + group: deployments kind: ServiceDeployment - version: v1alpha1 -- group: deployments - kind: GitRepository + path: github.com/pluralsh/console/controller/api/v1alpha1 version: v1alpha1 - api: crdVersion: v1 @@ -23,7 +38,7 @@ resources: controller: true domain: plural.sh group: deployments - kind: Cluster2 + kind: GitRepository path: github.com/pluralsh/console/controller/api/v1alpha1 version: v1alpha1 version: "3" From 0a1aa2b55dec1d1c848b5130e578c027f51edeb7 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 14:58:11 +0100 Subject: [PATCH 154/198] fix byok cluster sample --- controller/config/crd/kustomization.yaml | 3 --- controller/config/samples/cluster_byok.yaml | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/controller/config/crd/kustomization.yaml b/controller/config/crd/kustomization.yaml index 358c03d72..29cf4e730 100644 --- a/controller/config/crd/kustomization.yaml +++ b/controller/config/crd/kustomization.yaml @@ -6,7 +6,6 @@ resources: - bases/deployments.plural.sh_providers.yaml - bases/deployments.plural.sh_gitrepositories.yaml - bases/deployments.plural.sh_servicedeployments.yaml -- bases/deployments.plural.sh_cluster2s.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -17,7 +16,6 @@ patchesStrategicMerge: #- patches/webhook_in_gitrepositories.yaml #- patches/webhook_in_servicedeployments.yaml #- path: patches/webhook_in_clusters.yaml -#- path: patches/webhook_in_cluster2s.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -27,7 +25,6 @@ patchesStrategicMerge: #- patches/cainjection_in_gitrepositories.yaml #- patches/cainjection_in_servicedeployments.yaml #- path: patches/cainjection_in_clusters.yaml -#- path: patches/cainjection_in_cluster2s.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/controller/config/samples/cluster_byok.yaml b/controller/config/samples/cluster_byok.yaml index 05fa838d4..cc6ae3bc3 100644 --- a/controller/config/samples/cluster_byok.yaml +++ b/controller/config/samples/cluster_byok.yaml @@ -3,4 +3,5 @@ kind: Cluster metadata: name: byok namespace: default - +spec: + cloud: "byok" From f7a84867c31aeb875a59dbbea9832076e446d7d3 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Mon, 18 Dec 2023 14:58:53 +0100 Subject: [PATCH 155/198] update makefile --- controller/Makefile | 119 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 6d5f2ccdf..d38f08c05 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -2,10 +2,24 @@ ROOT_DIRECTORY := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) include $(ROOT_DIRECTORY)/hack/include/build.mk -IMG ?= deployment-controller:latest # Image URL to use all building/pushing image targets -CONTROLLER_GEN = $(shell which controller-gen) -CRD_OPTIONS ?= "crd" +# Image URL to use all building/pushing image targets +IMG ?= deployment-controller:latest + +# Tool binaries +KUBECTL ?= $(shell which kubectl) +KUSTOMIZE ?= $(shell which kustomize) +CONTROLLER_GEN ?= $(shell which controller-gen) +ENVTEST ?= $(shell which setup-envtest) +GOLANGCI_LINT ?= $(shell which golangci-lint) + +# Tool versions KUBEBUILDER_VERSION := 3.11.1 +ENVTEST_VERSION := 1.28.3 + +# 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 ifndef GOPATH $(error $$GOPATH environment variable not set) @@ -30,16 +44,6 @@ update-dependencies: ## update dependencies go get -u ./... go mod tidy -.PHONY: install-tools -install-tools: ## install required tools - @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI {} go install {} - -.PHONY: install-kubebuilder -install-kubebuilder: ## install kubebuilder - @curl -L -O --output-dir bin/ "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_${OS}_${ARCH}" - @chmod +x bin/kubebuilder_${OS}_${ARCH} - @mv bin/kubebuilder_${OS}_${ARCH} ${GOPATH}/bin/kubebuilder - ##@ Build .PHONY: build @@ -59,11 +63,11 @@ docker-push: ## push Docker image with the driver ##@ Codegen .PHONY: manifests -manifests: install-tools ## generate Kubernetes manifests - $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases +manifests: controller-gen ## generate Kubernetes manifests + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate -generate: install-tools ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations +generate: controller-gen ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations go generate ./pkg/... $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/... @@ -73,19 +77,84 @@ genmock: ## generates mocks before running tests ##@ Tests +.PHONY: fmt +fmt: ## Run go fmt against code. + @go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + @go vet ./... + .PHONY: test -test: genmock ## run tests - go test ./... -v +test: manifests generate genmock fmt vet envtest ## run tests + @go test $$(go list ./... | grep -v /e2e) -v .PHONY: lint -lint: install-tools ## run linters - golangci-lint run ./... +lint: golangci-lint ## run linters + @$(GOLANGCI_LINT) run ./... .PHONY: fix -fix: install-tools ## fix issues found by linters - golangci-lint run --fix ./... +fix: golangci-lint ## fix issues found by linters + @$(GOLANGCI_LINT) run --fix ./... + +##@ 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 - -##@ Utils +.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 - -apply-crds: ## applies CRDs - kubectl apply -f config/crd/bases +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +###@ Utils +# +#apply-crds: ## applies CRDs +# kubectl apply -f config/crd/bases + +##@ Build Dependencies + +.PHONY: tools +tools: ## install required tools +tools: --tool + +.PHONY: --tool +%--tool: TOOL = .* +--tool: # INTERNAL: installs tool with name provided via $(TOOL) variable or all tools. + @cat tools.go | grep _ | awk -F'"' '$$2 ~ /$(TOOL)/ {print $$2}' | xargs -I {} go install {} + +.PHONY: controller-gen +controller-gen: TOOL = controller-gen +controller-gen: --tool ## Download and install controller-gen in the $GOPATH/bin + +.PHONY: kustomize +kustomize: TOOL = kustomize +kustomize: --tool ## Download and install kustomize in the $GOPATH/bin + +.PHONY: golangci-lint +golangci-lint: TOOL = golangci-lint +golangci-lint: --tool ## Download and install golangci-lint in the $GOPATH/bin + +.PHONY: envtest +envtest: TOOL = setup-envtest +envtest: --tool ## Download and install setup-envtest in $GOPATH/bin and configure envtest + @$(ENVTEST) use $(ENVTEST_VERSION) --bin-dir ${GOPATH}/bin + +.PHONY: kubebuilder +kubebuilder: ## install kubebuilder + @curl -L -O --output-dir bin/ "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_${OS}_${ARCH}" + @chmod +x bin/kubebuilder_${OS}_${ARCH} + @mv bin/kubebuilder_${OS}_${ARCH} ${GOPATH}/bin/kubebuilder From 70f4385018a51218025fd3d73138b7df61be1e4f Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 15:34:38 +0100 Subject: [PATCH 156/198] update go.mod --- controller/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/go.mod b/controller/go.mod index fe3355860..2499529da 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -7,7 +7,6 @@ toolchain go1.21.1 // Dependencies require ( github.com/Yamashou/gqlgenc v0.16.0 - github.com/go-logr/logr v1.3.0 github.com/pluralsh/console-client-go v0.0.59 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.39.0 @@ -81,6 +80,7 @@ require ( github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.2.3 // indirect github.com/go-critic/go-critic v0.9.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect From 57babba25561653698c158ce683b0f4279d3b867 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 16:06:21 +0100 Subject: [PATCH 157/198] kubebuilder init --- controller/.dockerignore | 3 + controller/.gitignore | 26 +++- controller/.golangci.yml | 36 +++-- controller/Dockerfile | 33 ++++ controller/PROJECT | 3 + controller/config/default/kustomization.yaml | 142 ++++++++++++++++++ .../default/manager_auth_proxy_patch.yaml | 39 +++++ .../config/default/manager_config_patch.yaml | 10 ++ controller/config/manager/kustomization.yaml | 2 + controller/config/manager/manager.yaml | 102 +++++++++++++ .../config/prometheus/kustomization.yaml | 2 + controller/config/prometheus/monitor.yaml | 25 +++ .../rbac/auth_proxy_client_clusterrole.yaml | 16 ++ controller/config/rbac/auth_proxy_role.yaml | 24 +++ .../config/rbac/auth_proxy_role_binding.yaml | 19 +++ .../config/rbac/auth_proxy_service.yaml | 21 +++ controller/config/rbac/kustomization.yaml | 18 +++ .../config/rbac/leader_election_role.yaml | 44 ++++++ .../rbac/leader_election_role_binding.yaml | 19 +++ controller/config/rbac/role.yaml | 11 +- controller/config/rbac/role_binding.yaml | 19 +++ controller/config/rbac/service_account.yaml | 12 ++ controller/hack/boilerplate.go.txt | 2 +- 23 files changed, 614 insertions(+), 14 deletions(-) create mode 100644 controller/.dockerignore create mode 100644 controller/Dockerfile create mode 100644 controller/config/default/kustomization.yaml create mode 100644 controller/config/default/manager_auth_proxy_patch.yaml create mode 100644 controller/config/default/manager_config_patch.yaml create mode 100644 controller/config/manager/kustomization.yaml create mode 100644 controller/config/manager/manager.yaml create mode 100644 controller/config/prometheus/kustomization.yaml create mode 100644 controller/config/prometheus/monitor.yaml create mode 100644 controller/config/rbac/auth_proxy_client_clusterrole.yaml create mode 100644 controller/config/rbac/auth_proxy_role.yaml create mode 100644 controller/config/rbac/auth_proxy_role_binding.yaml create mode 100644 controller/config/rbac/auth_proxy_service.yaml create mode 100644 controller/config/rbac/kustomization.yaml create mode 100644 controller/config/rbac/leader_election_role.yaml create mode 100644 controller/config/rbac/leader_election_role_binding.yaml create mode 100644 controller/config/rbac/role_binding.yaml create mode 100644 controller/config/rbac/service_account.yaml diff --git a/controller/.dockerignore b/controller/.dockerignore new file mode 100644 index 000000000..a3aab7af7 --- /dev/null +++ b/controller/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/controller/.gitignore b/controller/.gitignore index a5d8f7237..d17bf9235 100644 --- a/controller/.gitignore +++ b/controller/.gitignore @@ -1,2 +1,24 @@ -bin/ -dist/ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/controller/.golangci.yml b/controller/.golangci.yml index 49df66d6d..aed8644d1 100644 --- a/controller/.golangci.yml +++ b/controller/.golangci.yml @@ -1,24 +1,40 @@ +run: + deadline: 5m + allow-parallel-runners: true + issues: - max-per-linter: 0 - max-same-issues: 0 + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll linters: disable-all: true enable: - - durationcheck + - dupl - errcheck - exportloopref - - forcetypeassert - - godot + - goconst + - gocyclo - gofmt + - goimports - gosimple + - govet - ineffassign - - makezero + - lll - misspell - - nilerr - - predeclared + - nakedret + - prealloc - staticcheck - - tenv + - typecheck - unconvert - unparam - unused - - vet diff --git a/controller/Dockerfile b/controller/Dockerfile new file mode 100644 index 000000000..c389c0981 --- /dev/null +++ b/controller/Dockerfile @@ -0,0 +1,33 @@ +# Build the manager binary +FROM golang:1.20 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/controller/ internal/controller/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/controller/PROJECT b/controller/PROJECT index 49c1dbd78..dbd6014f0 100644 --- a/controller/PROJECT +++ b/controller/PROJECT @@ -3,6 +3,9 @@ # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html domain: plural.sh +layout: + - go.kubebuilder.io/v4 +projectName: controller repo: github.com/pluralsh/console/controller resources: - api: diff --git a/controller/config/default/kustomization.yaml b/controller/config/default/kustomization.yaml new file mode 100644 index 000000000..7fd4e0b0d --- /dev/null +++ b/controller/config/default/kustomization.yaml @@ -0,0 +1,142 @@ +# Adds namespace to all resources. +namespace: test-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: test- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +#- ../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 + +patches: +# 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. +- path: manager_auth_proxy_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/controller/config/default/manager_auth_proxy_patch.yaml b/controller/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000..70c3437f4 --- /dev/null +++ b/controller/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,39 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 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/controller/config/default/manager_config_patch.yaml b/controller/config/default/manager_config_patch.yaml new file mode 100644 index 000000000..f6f589169 --- /dev/null +++ b/controller/config/default/manager_config_patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/controller/config/manager/kustomization.yaml b/controller/config/manager/kustomization.yaml new file mode 100644 index 000000000..5c5f0b84c --- /dev/null +++ b/controller/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/controller/config/manager/manager.yaml b/controller/config/manager/manager.yaml new file mode 100644 index 000000000..917cdfc65 --- /dev/null +++ b/controller/config/manager/manager.yaml @@ -0,0 +1,102 @@ +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: test + app.kubernetes.io/part-of: test + 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: test + app.kubernetes.io/part-of: test + 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/controller/config/prometheus/kustomization.yaml b/controller/config/prometheus/kustomization.yaml new file mode 100644 index 000000000..ed137168a --- /dev/null +++ b/controller/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/controller/config/prometheus/monitor.yaml b/controller/config/prometheus/monitor.yaml new file mode 100644 index 000000000..90e793f1e --- /dev/null +++ b/controller/config/prometheus/monitor.yaml @@ -0,0 +1,25 @@ +# 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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/auth_proxy_client_clusterrole.yaml b/controller/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000..8948dc18f --- /dev/null +++ b/controller/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,16 @@ +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: test + app.kubernetes.io/part-of: test + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controller/config/rbac/auth_proxy_role.yaml b/controller/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000..3e000420e --- /dev/null +++ b/controller/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,24 @@ +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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/auth_proxy_role_binding.yaml b/controller/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000..f7cf051f2 --- /dev/null +++ b/controller/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,19 @@ +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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/auth_proxy_service.yaml b/controller/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000..e776faf8f --- /dev/null +++ b/controller/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,21 @@ +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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/kustomization.yaml b/controller/config/rbac/kustomization.yaml new file mode 100644 index 000000000..731832a6a --- /dev/null +++ b/controller/config/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +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/controller/config/rbac/leader_election_role.yaml b/controller/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000..16c73fff6 --- /dev/null +++ b/controller/config/rbac/leader_election_role.yaml @@ -0,0 +1,44 @@ +# 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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/leader_election_role_binding.yaml b/controller/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000..93af1aa31 --- /dev/null +++ b/controller/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,19 @@ +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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/role.yaml b/controller/config/rbac/role.yaml index 2c901c947..f4d0446ca 100644 --- a/controller/config/rbac/role.yaml +++ b/controller/config/rbac/role.yaml @@ -1,9 +1,18 @@ ---- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: manager-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: test + app.kubernetes.io/part-of: test + app.kubernetes.io/managed-by: kustomize name: manager-role rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] - apiGroups: - deployments.plural.sh resources: diff --git a/controller/config/rbac/role_binding.yaml b/controller/config/rbac/role_binding.yaml new file mode 100644 index 000000000..8cd4c5868 --- /dev/null +++ b/controller/config/rbac/role_binding.yaml @@ -0,0 +1,19 @@ +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: test + app.kubernetes.io/part-of: test + 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/controller/config/rbac/service_account.yaml b/controller/config/rbac/service_account.yaml new file mode 100644 index 000000000..afc3eb17e --- /dev/null +++ b/controller/config/rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: test + app.kubernetes.io/part-of: test + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/controller/hack/boilerplate.go.txt b/controller/hack/boilerplate.go.txt index 8c36d1245..65b862271 100644 --- a/controller/hack/boilerplate.go.txt +++ b/controller/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2023 The Kubernetes authors. +Copyright 2023. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From c0f928998c9676b0c50ae4abff795c3b54c196f2 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 16:22:53 +0100 Subject: [PATCH 158/198] kubebuilder init changes --- controller/Makefile | 8 ++-- controller/README.md | 91 ++++++++++++++++++++++++++++++++++++++++++ controller/cmd/main.go | 69 ++++++++++++++++++++++++++------ 3 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 controller/README.md diff --git a/controller/Makefile b/controller/Makefile index 80df3847a..e573c39bc 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -63,13 +63,13 @@ docker-push: ## push Docker image with the driver ##@ Codegen .PHONY: manifests -manifests: controller-gen ## generate Kubernetes 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 go generate ./pkg/... - $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/... + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: genmock genmock: ## generates mocks before running tests @@ -78,11 +78,11 @@ genmock: ## generates mocks before running tests ##@ Tests .PHONY: fmt -fmt: ## Run go fmt against code. +fmt: ## run go fmt against code @go fmt ./... .PHONY: vet -vet: ## Run go vet against code. +vet: ## run go vet against code @go vet ./... .PHONY: test diff --git a/controller/README.md b/controller/README.md new file mode 100644 index 000000000..15472310e --- /dev/null +++ b/controller/README.md @@ -0,0 +1,91 @@ +# test +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started + +### Prerequisites +- go version v1.20.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. + +### To Deploy on the cluster +**Build and push your image to the location specified by `IMG`:** + +```sh +make docker-build docker-push IMG=/test:tag +``` + +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. + +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/test:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +>**NOTE**: Ensure that the samples has default values to test it out. + +### To Uninstall +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 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. + diff --git a/controller/cmd/main.go b/controller/cmd/main.go index 6f0dd625d..8b4dc6e76 100644 --- a/controller/cmd/main.go +++ b/controller/cmd/main.go @@ -1,3 +1,19 @@ +/* +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 main import ( @@ -6,6 +22,13 @@ import ( "os" "strings" + // 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" + + deploymentsv1alpha "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -13,15 +36,13 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" - - deploymentsv1alpha "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/client" - "github.com/pluralsh/console/controller/internal/types" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + //+kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("Setup") + setupLog = ctrl.Log.WithName("setup") // version is managed by GoReleaser, see: https://goreleaser.com/cookbooks/using-main.version/ version = "dev" // commit is managed by GoReleaser, see: https://goreleaser.com/cookbooks/using-main.version/ @@ -29,8 +50,11 @@ var ( ) func init() { - utilruntime.Must(deploymentsv1alpha.AddToScheme(scheme)) utilruntime.Must(corev1.AddToScheme(scheme)) + + utilruntime.Must(deploymentsv1alpha.AddToScheme(scheme)) + + //+kubebuilder:scaffold:scheme } type controllerRunOptions struct { @@ -53,7 +77,7 @@ func main() { } opts.BindFlags(flag.CommandLine) flag.StringVar(&opt.metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") - flag.StringVar(&opt.probeAddr, "health-probe-bind-address", ":9001", "The address the probe endpoint binds to.") + flag.StringVar(&opt.probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&opt.enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -78,16 +102,35 @@ func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, - LeaderElection: opt.enableLeaderElection, - LeaderElectionID: "dep344ab8.plural.sh", + Metrics: metricsserver.Options{BindAddress: opt.metricsAddr}, HealthProbeBindAddress: opt.probeAddr, + LeaderElection: opt.enableLeaderElection, + LeaderElectionID: "144e1fda.plural.sh", + // 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 create manager") + setupLog.Error(err, "unable to start manager") + 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.AddHealthzCheck("ping", healthz.Ping); err != nil { - setupLog.Error(err, "unable to create health check") + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") os.Exit(1) } @@ -131,7 +174,7 @@ func runOrDie(controllers []types.Controller, mgr ctrl.Manager) { ctx := ctrl.SetupSignalHandler() setupLog.Info("starting manager") if err := mgr.Start(ctx); err != nil { - setupLog.Error(err, "error running manager") + setupLog.Error(err, "problem running manager") os.Exit(1) } } From 09c48c1c019ef1023de66aa62bd128ffd2f72c75 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 16:41:14 +0100 Subject: [PATCH 159/198] update makefile --- controller/Makefile | 15 +++++++-------- controller/tools.go | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index e573c39bc..340c980f9 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -47,11 +47,15 @@ update-dependencies: ## update dependencies ##@ Build .PHONY: build -build: manifests generate ## build - go build -o bin/deployment-controller cmd/main.go +build: manifests generate fmt vet ## build manager binary + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## run a controller from your host + go run ./cmd/main.go .PHONY: release -release: manifests generate ## builds release version of the app. Requires GoReleaser to work. +release: manifests generate fmt vet ## builds release version of the app. Requires GoReleaser to work. goreleaser build --clean --single-target --snapshot docker-build: ## build Docker image with the driver @@ -120,11 +124,6 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - -###@ Utils -# -#apply-crds: ## applies CRDs -# kubectl apply -f config/crd/bases - ##@ Build Dependencies .PHONY: tools diff --git a/controller/tools.go b/controller/tools.go index b28dc825b..62b301501 100644 --- a/controller/tools.go +++ b/controller/tools.go @@ -1,6 +1,6 @@ //go:build tools -package main +package tools import ( _ "github.com/golangci/golangci-lint/cmd/golangci-lint" From ff204a9e9d90537270a71e64d694db3c431ba31f Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Mon, 18 Dec 2023 16:42:02 +0100 Subject: [PATCH 160/198] update mockery config and start rewriting test to use ginkgo/gomega --- controller/.mockery.yaml | 8 + controller/Makefile | 26 +- controller/go.mod | 8 +- controller/go.sum | 19 +- .../internal/controller/cluster_controller.go | 9 +- .../cluster_controller_ginkgo_test.go | 146 ++ .../controller/cluster_controller_test.go | 866 +++++----- .../gitrepository_controller_test.go | 621 +++---- .../controller/provider_controller_test.go | 667 +++---- .../servicedeployment_controller_test.go | 639 +++---- .../internal/test/e2e/e2e_suite_test.go | 16 + .../internal/test/mocks/ConsoleClient.go | 740 -------- .../internal/test/mocks/ConsoleClient_mock.go | 1532 +++++++++++++++++ .../internal/test/mocks/testing_mock.go | 35 + controller/tools.go | 1 + 15 files changed, 3183 insertions(+), 2150 deletions(-) create mode 100644 controller/.mockery.yaml create mode 100644 controller/internal/controller/cluster_controller_ginkgo_test.go create mode 100644 controller/internal/test/e2e/e2e_suite_test.go delete mode 100644 controller/internal/test/mocks/ConsoleClient.go create mode 100644 controller/internal/test/mocks/ConsoleClient_mock.go create mode 100644 controller/internal/test/mocks/testing_mock.go diff --git a/controller/.mockery.yaml b/controller/.mockery.yaml new file mode 100644 index 000000000..a3c032e99 --- /dev/null +++ b/controller/.mockery.yaml @@ -0,0 +1,8 @@ +filename: "{{.InterfaceName}}_mock.go" +dir: "internal/test/mocks" +mockname: "{{.InterfaceName}}Mock" +outpkg: "mocks" +packages: + github.com/pluralsh/console/controller/internal/client: + interfaces: + ConsoleClient: diff --git a/controller/Makefile b/controller/Makefile index 80df3847a..ba6c66a07 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -11,10 +11,11 @@ KUSTOMIZE ?= $(shell which kustomize) CONTROLLER_GEN ?= $(shell which controller-gen) ENVTEST ?= $(shell which setup-envtest) GOLANGCI_LINT ?= $(shell which golangci-lint) +MOCKERY ?= $(shell which mockery) # Tool versions KUBEBUILDER_VERSION := 3.11.1 -ENVTEST_VERSION := 1.28.3 +ENVTEST_K8S_VERSION := 1.28.3 # 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. @@ -72,8 +73,8 @@ generate: controller-gen ## generate code containing DeepCopy, DeepCopyInto, and $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/... .PHONY: genmock -genmock: ## generates mocks before running tests - hack/gen-client-mocks.sh +genmock: mockery ## generates mocks before running tests + @$(MOCKERY) ##@ Tests @@ -85,10 +86,6 @@ fmt: ## Run go fmt against code. vet: ## Run go vet against code. @go vet ./... -.PHONY: test -test: manifests generate genmock fmt vet envtest ## run tests - @go test $$(go list ./... | grep -v /e2e) -v - .PHONY: lint lint: golangci-lint ## run linters @$(GOLANGCI_LINT) run ./... @@ -97,6 +94,14 @@ lint: golangci-lint ## run linters fix: golangci-lint ## fix issues found by linters @$(GOLANGCI_LINT) run --fix ./... +.PHONY: test +test: manifests generate genmock fmt vet envtest ## run tests + @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(GOPATH)/bin -p path)" go test $$(go list ./... | grep -v /e2e) -v + +.PHONY: e2e +e2e: ## run e2e tests + @go test ./internal/test/e2e/ -v -ginkgo.v + ##@ Deployment ifndef ignore-not-found @@ -150,8 +155,11 @@ golangci-lint: --tool ## Download and install golangci-lint in the $GOPATH/bin .PHONY: envtest envtest: TOOL = setup-envtest -envtest: --tool ## Download and install setup-envtest in $GOPATH/bin and configure envtest - @$(ENVTEST) use $(ENVTEST_VERSION) --bin-dir ${GOPATH}/bin +envtest: --tool ## Download and install setup-envtest in the $GOPATH/bin + +.PHONY: mockery +mockery: TOOL = mockery +mockery: --tool .PHONY: kubebuilder kubebuilder: ## install kubebuilder diff --git a/controller/go.mod b/controller/go.mod index fe3355860..380b81ead 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.1 // Dependencies require ( github.com/Yamashou/gqlgenc v0.16.0 - github.com/go-logr/logr v1.3.0 + github.com/go-logr/logr v1.3.0 // indirect github.com/pluralsh/console-client-go v0.0.59 github.com/pluralsh/polly v0.1.4 github.com/samber/lo v1.39.0 @@ -25,6 +25,7 @@ require ( // Tools require ( github.com/golangci/golangci-lint v1.55.2 + github.com/vektra/mockery/v2 v2.38.0 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231215020716-1b80b9629af8 sigs.k8s.io/controller-tools v0.13.0 sigs.k8s.io/kustomize/kustomize/v5 v5.3.0 @@ -259,11 +260,16 @@ require ( require github.com/onsi/ginkgo/v2 v2.13.1 require ( + github.com/chigopher/pathlib v0.15.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect + github.com/jinzhu/copier v0.3.5 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/rs/zerolog v1.29.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect gopkg.in/evanphx/json-patch.v5 v5.6.0 // indirect diff --git a/controller/go.sum b/controller/go.sum index 96b2a519d..2c66d7e2a 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -135,6 +135,8 @@ github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iy github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/chigopher/pathlib v0.15.0 h1:1pg96WL3iC1/YyWV4UJSl3E0GBf4B+h5amBtsbAAieY= +github.com/chigopher/pathlib v0.15.0/go.mod h1:3+YPPV21mU9vyw8Mjp+F33CyCfE6iOzinpiqBcccv7I= 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= @@ -146,6 +148,7 @@ github.com/coredns/caddy v1.1.0 h1:ezvsPrT/tA/7pYDBZxu0cT0VmWk75AfIaf6GSYCNMf0= github.com/coredns/caddy v1.1.0/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= github.com/coredns/corefile-migration v1.0.21 h1:W/DCETrHDiFo0Wj03EyMkaQ9fwsmSgqTCQDHpceaSsE= github.com/coredns/corefile-migration v1.0.21/go.mod h1:XnhgULOEouimnzgn0t4WPuFDN2/PJQcTxdWKC5eXNGE= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= @@ -242,6 +245,7 @@ github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -373,8 +377,10 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= @@ -385,6 +391,8 @@ github.com/jgautheron/goconst v1.6.0 h1:gbMLWKRMkzAc6kYsQL6/TxaoBUg3Jm9LSF/Ih1AD github.com/jgautheron/goconst v1.6.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -449,6 +457,7 @@ github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2 github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -574,6 +583,9 @@ github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= @@ -680,6 +692,8 @@ github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvni github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +github.com/vektra/mockery/v2 v2.38.0 h1:I0LBuUzZHqAU4d1DknW0DTFBPO6n8TaD38WL2KJf3yI= +github.com/vektra/mockery/v2 v2.38.0/go.mod h1:diB13hxXG6QrTR0ol2Rk8s2dRMftzvExSvPDKr+IYKk= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= @@ -901,6 +915,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc 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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/controller/internal/controller/cluster_controller.go b/controller/internal/controller/cluster_controller.go index 2ac1406f9..e92fc0d8b 100644 --- a/controller/internal/controller/cluster_controller.go +++ b/controller/internal/controller/cluster_controller.go @@ -5,9 +5,6 @@ import ( "fmt" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/internal/client" - "github.com/pluralsh/console/controller/internal/utils" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -17,6 +14,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/console/controller/api/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/utils" ) const ( @@ -83,7 +84,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request // Get Provider ID from the reference if it is set and ensure that controller reference is set properly. providerId, result, err := r.getProviderIdAndSetControllerRef(ctx, cluster) if result != nil { - utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + utils.MarkCondition(cluster.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, fmt.Sprintf("%s", err)) return *result, err } diff --git a/controller/internal/controller/cluster_controller_ginkgo_test.go b/controller/internal/controller/cluster_controller_ginkgo_test.go new file mode 100644 index 000000000..854f82b97 --- /dev/null +++ b/controller/internal/controller/cluster_controller_ginkgo_test.go @@ -0,0 +1,146 @@ +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/test/mocks" +) + +var _ = Describe("Cluster Controller", func() { + Context("When reconciling a resource", func() { + const ( + clusterName = "test-cluster" + clusterConsoleID = "12345-67890" + providerName = "test-provider" + providerNamespace = "test-provider" + providerConsoleID = "12345-67890" + ) + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: clusterName, + Namespace: "default", + } + + cluster := &v1alpha1.Cluster{} + provider := &v1alpha1.Provider{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Cluster") + err := k8sClient.Get(ctx, typeNamespacedName, cluster) + if err != nil && errors.IsNotFound(err) { + resource := &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Version: lo.ToPtr("1.24"), + Cloud: "aws", + ProviderRef: &corev1.ObjectReference{Name: providerName, Namespace: providerNamespace}, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + + By("creating the custom resource for the Kind Provider") + err = k8sClient.Get(ctx, typeNamespacedName, provider) + if err != nil && errors.IsNotFound(err) { + resource := &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{Name: providerName}, + Spec: v1alpha1.ProviderSpec{ + Cloud: "aws", + Name: providerName, + Namespace: providerNamespace, + }, + Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Status().Update(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + resource := &v1alpha1.Cluster{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Cluster") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + test := struct { + returnGetClusterByHandle *gqlclient.ClusterFragment + returnErrorGetClusterByHandle error + returnIsClusterExisting bool + returnCreateCluster *gqlclient.ClusterFragment + returnErrorCreateCluster error + expectedStatus v1alpha1.ClusterStatus + }{ + returnGetClusterByHandle: nil, + returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), + returnIsClusterExisting: false, + returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, + expectedStatus: v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }, + } + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, typeNamespacedName, cluster) + + Expect(err).NotTo(HaveOccurred()) + Expect(cluster.Status).To(Equal(test.expectedStatus)) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/controller/internal/controller/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go index 012979755..9a80725cd 100644 --- a/controller/internal/controller/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -1,434 +1,436 @@ package controller_test -import ( - "context" - "encoding/json" - "testing" - - gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/internal/controller" - "github.com/samber/lo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes/scheme" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/test/mocks" -) - -func init() { - utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -} - -func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { - for i := range status.Conditions { - status.Conditions[i].LastTransitionTime = metav1.Time{} - status.Conditions[i].ObservedGeneration = 0 - } - - return status -} - -func TestCreateNewCluster(t *testing.T) { - const ( - clusterName = "test-cluster" - clusterConsoleID = "12345-67890" - providerName = "test-provider" - providerNamespace = "test-provider" - providerConsoleID = "12345-67890" - ) - - tests := []struct { - name string - cluster string - returnGetClusterByHandle *gqlclient.ClusterFragment - returnErrorGetClusterByHandle error - returnIsClusterExisting bool - returnCreateCluster *gqlclient.ClusterFragment - returnErrorCreateCluster error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ClusterStatus - }{ - { - name: "scenario 1: create a new AWS cluster", - cluster: clusterName, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetClusterByHandle: nil, - returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), - returnIsClusterExisting: false, - returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName}, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Version: lo.ToPtr("1.24"), - Cloud: "aws", - ProviderRef: &corev1.ObjectReference{Name: providerName}, - }, - }, - &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{Name: providerName}, - Spec: v1alpha1.ProviderSpec{ - Cloud: "aws", - Name: providerName, - Namespace: providerNamespace, - }, - Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, - }, - }, - }, - { - name: "scenario 2: create a new BYOK cluster", - cluster: clusterName, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetClusterByHandle: nil, - returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), - returnIsClusterExisting: false, - returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName}, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Cloud: "byok", - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) - fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) - - ctx := context.Background() - - target := &controller.ClusterReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) - assert.NoError(t, err) - - existingCluster := &v1alpha1.Cluster{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) - existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) - expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) - - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} - -func TestUpdateCluster(t *testing.T) { - const ( - clusterName = "test-cluster" - clusterConsoleID = "12345-67890" - providerName = "test-provider" - providerNamespace = "test-provider" - providerConsoleID = "12345-67890" - ) - - tests := []struct { - name string - cluster string - returnIsClusterExisting bool - returnUpdateCluster *gqlclient.ClusterFragment - returnErrorUpdateCluster error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ClusterStatus - }{ - { - name: "scenario 1: update AWS cluster", - cluster: clusterName, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnIsClusterExisting: true, - returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName}, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Version: lo.ToPtr("1.24"), - Cloud: "aws", - ProviderRef: &corev1.ObjectReference{Name: providerName}, - }, - Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - }, - &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{Name: providerName}, - Spec: v1alpha1.ProviderSpec{ - Cloud: "aws", - Name: providerName, - Namespace: providerNamespace, - }, - Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, - }, - }, - }, - { - name: "scenario 2: update BYOK cluster", - cluster: clusterName, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnIsClusterExisting: true, - returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName}, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Cloud: "byok", - }, - Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) - fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return(test.returnUpdateCluster, test.returnErrorUpdateCluster) - - ctx := context.Background() - - target := &controller.ClusterReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) - assert.NoError(t, err) - - existingCluster := &v1alpha1.Cluster{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) - - existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) - expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) - - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} - -func TestAdoptExistingCluster(t *testing.T) { - const ( - clusterName = "test-cluster" - clusterConsoleID = "12345-67890" - ) - - tests := []struct { - name string - cluster string - returnGetClusterByHandle *gqlclient.ClusterFragment - returnErrorGetClusterByHandle error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ClusterStatus - }{ - { - name: "scenario 1: adopt existing AWS cluster", - cluster: clusterName, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: v1alpha1.ReadonlyTrueConditionMessage.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, - returnErrorGetClusterByHandle: nil, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName}, - Spec: v1alpha1.ClusterSpec{Handle: lo.ToPtr(clusterName)}, - }, - }, - }, - { - name: "scenario 2: adopt existing BYOK cluster", - cluster: clusterName, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - CurrentVersion: lo.ToPtr("1.24.11"), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: v1alpha1.ReadonlyTrueConditionMessage.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetClusterByHandle: &gqlclient.ClusterFragment{ - ID: clusterConsoleID, - CurrentVersion: lo.ToPtr("1.24.11"), - }, - returnErrorGetClusterByHandle: nil, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName}, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Cloud: "byok", - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) - - ctx := context.Background() - - target := &controller.ClusterReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) - assert.NoError(t, err) - - existingCluster := &v1alpha1.Cluster{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) - - existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) - expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) - - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} +// +//import ( +// "context" +// "encoding/json" +// "testing" +// +// gqlclient "github.com/pluralsh/console-client-go" +// "github.com/samber/lo" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/mock" +// corev1 "k8s.io/api/core/v1" +// "k8s.io/apimachinery/pkg/api/errors" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/runtime/schema" +// "k8s.io/apimachinery/pkg/types" +// utilruntime "k8s.io/apimachinery/pkg/util/runtime" +// "k8s.io/client-go/kubernetes/scheme" +// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/controller-runtime/pkg/client/fake" +// "sigs.k8s.io/controller-runtime/pkg/reconcile" +// +// "github.com/pluralsh/console/controller/internal/controller" +// +// "github.com/pluralsh/console/controller/api/v1alpha1" +// "github.com/pluralsh/console/controller/internal/test/mocks" +//) +// +//func init() { +// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +//} +// +//func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { +// for i := range status.Conditions { +// status.Conditions[i].LastTransitionTime = metav1.Time{} +// status.Conditions[i].ObservedGeneration = 0 +// } +// +// return status +//} +// +//func TestCreateNewCluster(t *testing.T) { +// const ( +// clusterName = "test-cluster" +// clusterConsoleID = "12345-67890" +// providerName = "test-provider" +// providerNamespace = "test-provider" +// providerConsoleID = "12345-67890" +// ) +// +// tests := []struct { +// name string +// cluster string +// returnGetClusterByHandle *gqlclient.ClusterFragment +// returnErrorGetClusterByHandle error +// returnIsClusterExisting bool +// returnCreateCluster *gqlclient.ClusterFragment +// returnErrorCreateCluster error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ClusterStatus +// }{ +// { +// name: "scenario 1: create a new AWS cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetClusterByHandle: nil, +// returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), +// returnIsClusterExisting: false, +// returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Version: lo.ToPtr("1.24"), +// Cloud: "aws", +// ProviderRef: &corev1.ObjectReference{Name: providerName}, +// }, +// }, +// &v1alpha1.Provider{ +// ObjectMeta: metav1.ObjectMeta{Name: providerName}, +// Spec: v1alpha1.ProviderSpec{ +// Cloud: "aws", +// Name: providerName, +// Namespace: providerNamespace, +// }, +// Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, +// }, +// }, +// }, +// { +// name: "scenario 2: create a new BYOK cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetClusterByHandle: nil, +// returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), +// returnIsClusterExisting: false, +// returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Cloud: "byok", +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) +// fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) +// fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) +// +// ctx := context.Background() +// +// target := &controller.ClusterReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) +// assert.NoError(t, err) +// +// existingCluster := &v1alpha1.Cluster{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) +// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) +// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) +// +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} +// +//func TestUpdateCluster(t *testing.T) { +// const ( +// clusterName = "test-cluster" +// clusterConsoleID = "12345-67890" +// providerName = "test-provider" +// providerNamespace = "test-provider" +// providerConsoleID = "12345-67890" +// ) +// +// tests := []struct { +// name string +// cluster string +// returnIsClusterExisting bool +// returnUpdateCluster *gqlclient.ClusterFragment +// returnErrorUpdateCluster error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ClusterStatus +// }{ +// { +// name: "scenario 1: update AWS cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnIsClusterExisting: true, +// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Version: lo.ToPtr("1.24"), +// Cloud: "aws", +// ProviderRef: &corev1.ObjectReference{Name: providerName}, +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// }, +// &v1alpha1.Provider{ +// ObjectMeta: metav1.ObjectMeta{Name: providerName}, +// Spec: v1alpha1.ProviderSpec{ +// Cloud: "aws", +// Name: providerName, +// Namespace: providerNamespace, +// }, +// Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, +// }, +// }, +// }, +// { +// name: "scenario 2: update BYOK cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnIsClusterExisting: true, +// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Cloud: "byok", +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) +// fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return(test.returnUpdateCluster, test.returnErrorUpdateCluster) +// +// ctx := context.Background() +// +// target := &controller.ClusterReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) +// assert.NoError(t, err) +// +// existingCluster := &v1alpha1.Cluster{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) +// +// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) +// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) +// +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} +// +//func TestAdoptExistingCluster(t *testing.T) { +// const ( +// clusterName = "test-cluster" +// clusterConsoleID = "12345-67890" +// ) +// +// tests := []struct { +// name string +// cluster string +// returnGetClusterByHandle *gqlclient.ClusterFragment +// returnErrorGetClusterByHandle error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ClusterStatus +// }{ +// { +// name: "scenario 1: adopt existing AWS cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// returnErrorGetClusterByHandle: nil, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{Handle: lo.ToPtr(clusterName)}, +// }, +// }, +// }, +// { +// name: "scenario 2: adopt existing BYOK cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// CurrentVersion: lo.ToPtr("1.24.11"), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetClusterByHandle: &gqlclient.ClusterFragment{ +// ID: clusterConsoleID, +// CurrentVersion: lo.ToPtr("1.24.11"), +// }, +// returnErrorGetClusterByHandle: nil, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Cloud: "byok", +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) +// +// ctx := context.Background() +// +// target := &controller.ClusterReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) +// assert.NoError(t, err) +// +// existingCluster := &v1alpha1.Cluster{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) +// +// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) +// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) +// +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} diff --git a/controller/internal/controller/gitrepository_controller_test.go b/controller/internal/controller/gitrepository_controller_test.go index 0ec2ed437..590d271cc 100644 --- a/controller/internal/controller/gitrepository_controller_test.go +++ b/controller/internal/controller/gitrepository_controller_test.go @@ -1,312 +1,313 @@ package controller_test -import ( - "context" - "encoding/json" - "testing" - - gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/internal/controller" - "github.com/samber/lo" - "github.com/stretchr/testify/mock" - - "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/test/mocks" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes/scheme" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -func init() { - utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -} - -func TestCreateNewRepository(t *testing.T) { - tests := []struct { - name string - repository string - returnGetRepository *gqlclient.GetGitRepository - returnErrorGetRepository error - returnCreateRepository *gqlclient.CreateGitRepository - returnErrorCreateRepository error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.GitRepositoryStatus - }{ - { - name: "scenario 1: create a new repository", - repository: "test", - expectedStatus: v1alpha1.GitRepositoryStatus{ - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetRepository: &gqlclient.GetGitRepository{ - GitRepository: nil, - }, - returnCreateRepository: &gqlclient.CreateGitRepository{ - CreateGitRepository: &gqlclient.GitRepositoryFragment{ - ID: "123", - }, - }, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - Spec: v1alpha1.GitRepositorySpec{ - Url: "https://test", - CredentialsRef: &corev1.SecretReference{ - Name: "testsecret", - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testsecret", - }, - Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // setup the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - target := &controller.GitRepositoryReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) - fakeConsoleClient.On("CreateRepository", mock.AnythingOfType("string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.returnCreateRepository, test.returnErrorCreateRepository) - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) - assert.NoError(t, err) - existingRepo := &v1alpha1.GitRepository{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) - assert.NoError(t, err) - existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) - assert.NoError(t, err) - expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} - -func TestUpdateRepository(t *testing.T) { - tests := []struct { - name string - repository string - returnGetRepository *gqlclient.GetGitRepository - returnErrorGetRepository error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.GitRepositoryStatus - }{ - { - name: "scenario 1: update credentials", - repository: "test", - expectedStatus: v1alpha1.GitRepositoryStatus{ - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetRepository: &gqlclient.GetGitRepository{ - GitRepository: &gqlclient.GitRepositoryFragment{ - ID: "123", - }, - }, - - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - Spec: v1alpha1.GitRepositorySpec{ - Url: "https://test", - CredentialsRef: &corev1.SecretReference{ - Name: "testsecret", - }, - }, - Status: v1alpha1.GitRepositoryStatus{ - Health: "", - Message: nil, - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("ABC"), - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testsecret", - }, - Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // setup the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - target := &controller.GitRepositoryReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) - fakeConsoleClient.On("UpdateRepository", mock.Anything, mock.Anything).Return(&gqlclient.UpdateGitRepository{}, nil) - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) - assert.NoError(t, err) - existingRepo := &v1alpha1.GitRepository{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) - assert.NoError(t, err) - existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) - assert.NoError(t, err) - expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) - assert.NoError(t, err) - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} - -func TestImportRepository(t *testing.T) { - tests := []struct { - name string - repository string - returnGetRepository *gqlclient.GetGitRepository - returnErrorGetRepository error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.GitRepositoryStatus - }{ - { - name: "scenario 1: update credentials", - repository: "test", - expectedStatus: v1alpha1.GitRepositoryStatus{ - ID: lo.ToPtr("123"), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: v1alpha1.ReadonlyTrueConditionMessage.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetRepository: &gqlclient.GetGitRepository{ - GitRepository: &gqlclient.GitRepositoryFragment{ - ID: "123", - }, - }, - - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - Spec: v1alpha1.GitRepositorySpec{ - Url: "https://test", - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // setup the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - target := &controller.GitRepositoryReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) - assert.NoError(t, err) - existingRepo := &v1alpha1.GitRepository{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) - assert.NoError(t, err) - existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) - assert.NoError(t, err) - expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) - assert.NoError(t, err) - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} - -func sanitizeRepoConditions(status v1alpha1.GitRepositoryStatus) v1alpha1.GitRepositoryStatus { - for i := range status.Conditions { - status.Conditions[i].LastTransitionTime = metav1.Time{} - status.Conditions[i].ObservedGeneration = 0 - } - - return status -} +// +//import ( +// "context" +// "encoding/json" +// "testing" +// +// gqlclient "github.com/pluralsh/console-client-go" +// "github.com/pluralsh/console/controller/internal/controller" +// "github.com/samber/lo" +// "github.com/stretchr/testify/mock" +// +// "github.com/pluralsh/console/controller/api/v1alpha1" +// "github.com/pluralsh/console/controller/internal/test/mocks" +// "github.com/stretchr/testify/assert" +// corev1 "k8s.io/api/core/v1" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/types" +// utilruntime "k8s.io/apimachinery/pkg/util/runtime" +// "k8s.io/client-go/kubernetes/scheme" +// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/controller-runtime/pkg/client/fake" +// "sigs.k8s.io/controller-runtime/pkg/reconcile" +//) +// +//func init() { +// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +//} +// +//func TestCreateNewRepository(t *testing.T) { +// tests := []struct { +// name string +// repository string +// returnGetRepository *gqlclient.GetGitRepository +// returnErrorGetRepository error +// returnCreateRepository *gqlclient.CreateGitRepository +// returnErrorCreateRepository error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.GitRepositoryStatus +// }{ +// { +// name: "scenario 1: create a new repository", +// repository: "test", +// expectedStatus: v1alpha1.GitRepositoryStatus{ +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetRepository: &gqlclient.GetGitRepository{ +// GitRepository: nil, +// }, +// returnCreateRepository: &gqlclient.CreateGitRepository{ +// CreateGitRepository: &gqlclient.GitRepositoryFragment{ +// ID: "123", +// }, +// }, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.GitRepository{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// }, +// Spec: v1alpha1.GitRepositorySpec{ +// Url: "https://test", +// CredentialsRef: &corev1.SecretReference{ +// Name: "testsecret", +// }, +// }, +// }, +// &corev1.Secret{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "testsecret", +// }, +// Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// // setup the test scenario +// fakeClient := fake. +// NewClientBuilder(). +// WithScheme(scheme.Scheme). +// WithObjects(test.existingObjects...). +// Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// +// // act +// ctx := context.Background() +// target := &controller.GitRepositoryReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) +// fakeConsoleClient.On("CreateRepository", mock.AnythingOfType("string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.returnCreateRepository, test.returnErrorCreateRepository) +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) +// assert.NoError(t, err) +// existingRepo := &v1alpha1.GitRepository{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) +// assert.NoError(t, err) +// existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) +// assert.NoError(t, err) +// expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} +// +//func TestUpdateRepository(t *testing.T) { +// tests := []struct { +// name string +// repository string +// returnGetRepository *gqlclient.GetGitRepository +// returnErrorGetRepository error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.GitRepositoryStatus +// }{ +// { +// name: "scenario 1: update credentials", +// repository: "test", +// expectedStatus: v1alpha1.GitRepositoryStatus{ +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetRepository: &gqlclient.GetGitRepository{ +// GitRepository: &gqlclient.GitRepositoryFragment{ +// ID: "123", +// }, +// }, +// +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.GitRepository{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// }, +// Spec: v1alpha1.GitRepositorySpec{ +// Url: "https://test", +// CredentialsRef: &corev1.SecretReference{ +// Name: "testsecret", +// }, +// }, +// Status: v1alpha1.GitRepositoryStatus{ +// Health: "", +// Message: nil, +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("ABC"), +// }, +// }, +// &corev1.Secret{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "testsecret", +// }, +// Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// // setup the test scenario +// fakeClient := fake. +// NewClientBuilder(). +// WithScheme(scheme.Scheme). +// WithObjects(test.existingObjects...). +// Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// +// // act +// ctx := context.Background() +// target := &controller.GitRepositoryReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) +// fakeConsoleClient.On("UpdateRepository", mock.Anything, mock.Anything).Return(&gqlclient.UpdateGitRepository{}, nil) +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) +// assert.NoError(t, err) +// existingRepo := &v1alpha1.GitRepository{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) +// assert.NoError(t, err) +// existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) +// assert.NoError(t, err) +// expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) +// assert.NoError(t, err) +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} +// +//func TestImportRepository(t *testing.T) { +// tests := []struct { +// name string +// repository string +// returnGetRepository *gqlclient.GetGitRepository +// returnErrorGetRepository error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.GitRepositoryStatus +// }{ +// { +// name: "scenario 1: update credentials", +// repository: "test", +// expectedStatus: v1alpha1.GitRepositoryStatus{ +// ID: lo.ToPtr("123"), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetRepository: &gqlclient.GetGitRepository{ +// GitRepository: &gqlclient.GitRepositoryFragment{ +// ID: "123", +// }, +// }, +// +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.GitRepository{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// }, +// Spec: v1alpha1.GitRepositorySpec{ +// Url: "https://test", +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// // setup the test scenario +// fakeClient := fake. +// NewClientBuilder(). +// WithScheme(scheme.Scheme). +// WithObjects(test.existingObjects...). +// Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// +// // act +// ctx := context.Background() +// target := &controller.GitRepositoryReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) +// assert.NoError(t, err) +// existingRepo := &v1alpha1.GitRepository{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) +// assert.NoError(t, err) +// existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) +// assert.NoError(t, err) +// expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) +// assert.NoError(t, err) +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} +// +//func sanitizeRepoConditions(status v1alpha1.GitRepositoryStatus) v1alpha1.GitRepositoryStatus { +// for i := range status.Conditions { +// status.Conditions[i].LastTransitionTime = metav1.Time{} +// status.Conditions[i].ObservedGeneration = 0 +// } +// +// return status +//} diff --git a/controller/internal/controller/provider_controller_test.go b/controller/internal/controller/provider_controller_test.go index a4b69c701..f5001bec2 100644 --- a/controller/internal/controller/provider_controller_test.go +++ b/controller/internal/controller/provider_controller_test.go @@ -1,335 +1,336 @@ package controller_test -import ( - "context" - "encoding/json" - "testing" - - gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/internal/controller" - "github.com/samber/lo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes/scheme" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/test/mocks" -) - -func init() { - utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -} - -func sanitizeConditions(status v1alpha1.ProviderStatus) v1alpha1.ProviderStatus { - for i := range status.Conditions { - status.Conditions[i].LastTransitionTime = metav1.Time{} - status.Conditions[i].ObservedGeneration = 0 - } - - return status -} - -func TestCreateNewProvider(t *testing.T) { - test := struct { - name string - providerName string - returnCreateProvider *gqlclient.ClusterProviderFragment - returnGetProviderByCloudError error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ProviderStatus - }{ - - name: "create a new provider", - providerName: "gcp-provider", - returnCreateProvider: &gqlclient.ClusterProviderFragment{ - ID: "1234", - Name: "gcp-provider", - Namespace: "gcp", - Cloud: "gcp", - }, - returnGetProviderByCloudError: errors.NewNotFound(schema.GroupResource{}, "gcp-provider"), - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gcp-provider", - }, - Spec: v1alpha1.ProviderSpec{ - Cloud: "gcp", - CloudSettings: &v1alpha1.CloudProviderSettings{ - GCP: &corev1.SecretReference{ - Name: "credentials", - }, - }, - Name: "gcp-provider", - Namespace: "gcp", - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "credentials", - }, - Data: map[string][]byte{ - "applicationCredentials": []byte("mock"), - }, - }, - }, - expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - } - - t.Run(test.name, func(t *testing.T) { - // set up the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - providerReconciler := &controller.ProviderReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, test.returnGetProviderByCloudError) - fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(false) - fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(test.returnCreateProvider, nil) - - _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) - assert.NoError(t, err) - - existingProvider := &v1alpha1.Provider{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - - existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) - expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) - - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) - }) -} - -func TestAdoptProvider(t *testing.T) { - test := struct { - name string - providerName string - returnGetProviderByCloud *gqlclient.ClusterProviderFragment - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ProviderStatus - }{ - name: "adopt existing provider", - providerName: "gcp-provider", - returnGetProviderByCloud: &gqlclient.ClusterProviderFragment{ - ID: "1234", - Name: "gcp-provider", - Namespace: "gcp", - Cloud: "gcp", - }, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gcp-provider", - }, - Spec: v1alpha1.ProviderSpec{ - Cloud: "gcp", - CloudSettings: &v1alpha1.CloudProviderSettings{ - GCP: &corev1.SecretReference{ - Name: "credentials", - }, - }, - Name: "gcp-provider", - Namespace: "gcp", - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "credentials", - }, - Data: map[string][]byte{ - "applicationCredentials": []byte("mock"), - }, - }, - }, - expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: v1alpha1.ReadonlyTrueConditionMessage.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - } - - t.Run(test.name, func(t *testing.T) { - // set up the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - providerReconciler := &controller.ProviderReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(test.returnGetProviderByCloud, nil) - - _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) - assert.NoError(t, err) - - existingProvider := &v1alpha1.Provider{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - - existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) - expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) - - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) - }) -} - -func TestUpdateProvider(t *testing.T) { - test := struct { - name string - providerName string - returnUpdateProvider *gqlclient.ClusterProviderFragment - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ProviderStatus - }{ - name: "update existing provider", - providerName: "gcp-provider", - returnUpdateProvider: &gqlclient.ClusterProviderFragment{ - ID: "1234", - Name: "gcp-provider", - Namespace: "gcp", - Cloud: "gcp", - }, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gcp-provider", - }, - Spec: v1alpha1.ProviderSpec{ - Cloud: "gcp", - CloudSettings: &v1alpha1.CloudProviderSettings{ - GCP: &corev1.SecretReference{ - Name: "credentials", - }, - }, - Name: "gcp-provider", - Namespace: "gcp", - }, - Status: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr(""), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "credentials", - }, - Data: map[string][]byte{ - "applicationCredentials": []byte("mock"), - }, - }, - }, - expectedStatus: v1alpha1.ProviderStatus{ - ID: lo.ToPtr("1234"), - SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - } - - t.Run(test.name, func(t *testing.T) { - // set up the test scenario - fakeClient := fake. - NewClientBuilder(). - WithScheme(scheme.Scheme). - WithObjects(test.existingObjects...). - Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - - // act - ctx := context.Background() - providerReconciler := &controller.ProviderReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(true, nil) - fakeConsoleClient.On("UpdateProvider", mock.Anything, mock.Anything, mock.Anything).Return(test.returnUpdateProvider, nil) - - _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) - assert.NoError(t, err) - - existingProvider := &v1alpha1.Provider{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) - - existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) - expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) - - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) - }) -} +// +//import ( +// "context" +// "encoding/json" +// "testing" +// +// gqlclient "github.com/pluralsh/console-client-go" +// "github.com/pluralsh/console/controller/internal/controller" +// "github.com/samber/lo" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/mock" +// corev1 "k8s.io/api/core/v1" +// "k8s.io/apimachinery/pkg/api/errors" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/runtime/schema" +// "k8s.io/apimachinery/pkg/types" +// utilruntime "k8s.io/apimachinery/pkg/util/runtime" +// "k8s.io/client-go/kubernetes/scheme" +// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/controller-runtime/pkg/client/fake" +// "sigs.k8s.io/controller-runtime/pkg/reconcile" +// +// "github.com/pluralsh/console/controller/api/v1alpha1" +// "github.com/pluralsh/console/controller/internal/test/mocks" +//) +// +//func init() { +// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +//} +// +//func sanitizeConditions(status v1alpha1.ProviderStatus) v1alpha1.ProviderStatus { +// for i := range status.Conditions { +// status.Conditions[i].LastTransitionTime = metav1.Time{} +// status.Conditions[i].ObservedGeneration = 0 +// } +// +// return status +//} +// +//func TestCreateNewProvider(t *testing.T) { +// test := struct { +// name string +// providerName string +// returnCreateProvider *gqlclient.ClusterProviderFragment +// returnGetProviderByCloudError error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ProviderStatus +// }{ +// +// name: "create a new provider", +// providerName: "gcp-provider", +// returnCreateProvider: &gqlclient.ClusterProviderFragment{ +// ID: "1234", +// Name: "gcp-provider", +// Namespace: "gcp", +// Cloud: "gcp", +// }, +// returnGetProviderByCloudError: errors.NewNotFound(schema.GroupResource{}, "gcp-provider"), +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Provider{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "gcp-provider", +// }, +// Spec: v1alpha1.ProviderSpec{ +// Cloud: "gcp", +// CloudSettings: &v1alpha1.CloudProviderSettings{ +// GCP: &corev1.SecretReference{ +// Name: "credentials", +// }, +// }, +// Name: "gcp-provider", +// Namespace: "gcp", +// }, +// }, +// &corev1.Secret{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "credentials", +// }, +// Data: map[string][]byte{ +// "applicationCredentials": []byte("mock"), +// }, +// }, +// }, +// expectedStatus: v1alpha1.ProviderStatus{ +// ID: lo.ToPtr("1234"), +// SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// } +// +// t.Run(test.name, func(t *testing.T) { +// // set up the test scenario +// fakeClient := fake. +// NewClientBuilder(). +// WithScheme(scheme.Scheme). +// WithObjects(test.existingObjects...). +// Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// +// // act +// ctx := context.Background() +// providerReconciler := &controller.ProviderReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, test.returnGetProviderByCloudError) +// fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(false) +// fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(test.returnCreateProvider, nil) +// +// _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) +// assert.NoError(t, err) +// +// existingProvider := &v1alpha1.Provider{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) +// +// existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) +// expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) +// +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) +// }) +//} +// +//func TestAdoptProvider(t *testing.T) { +// test := struct { +// name string +// providerName string +// returnGetProviderByCloud *gqlclient.ClusterProviderFragment +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ProviderStatus +// }{ +// name: "adopt existing provider", +// providerName: "gcp-provider", +// returnGetProviderByCloud: &gqlclient.ClusterProviderFragment{ +// ID: "1234", +// Name: "gcp-provider", +// Namespace: "gcp", +// Cloud: "gcp", +// }, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Provider{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "gcp-provider", +// }, +// Spec: v1alpha1.ProviderSpec{ +// Cloud: "gcp", +// CloudSettings: &v1alpha1.CloudProviderSettings{ +// GCP: &corev1.SecretReference{ +// Name: "credentials", +// }, +// }, +// Name: "gcp-provider", +// Namespace: "gcp", +// }, +// }, +// &corev1.Secret{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "credentials", +// }, +// Data: map[string][]byte{ +// "applicationCredentials": []byte("mock"), +// }, +// }, +// }, +// expectedStatus: v1alpha1.ProviderStatus{ +// ID: lo.ToPtr("1234"), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// } +// +// t.Run(test.name, func(t *testing.T) { +// // set up the test scenario +// fakeClient := fake. +// NewClientBuilder(). +// WithScheme(scheme.Scheme). +// WithObjects(test.existingObjects...). +// Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// +// // act +// ctx := context.Background() +// providerReconciler := &controller.ProviderReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(test.returnGetProviderByCloud, nil) +// +// _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) +// assert.NoError(t, err) +// +// existingProvider := &v1alpha1.Provider{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) +// +// existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) +// expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) +// +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) +// }) +//} +// +//func TestUpdateProvider(t *testing.T) { +// test := struct { +// name string +// providerName string +// returnUpdateProvider *gqlclient.ClusterProviderFragment +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ProviderStatus +// }{ +// name: "update existing provider", +// providerName: "gcp-provider", +// returnUpdateProvider: &gqlclient.ClusterProviderFragment{ +// ID: "1234", +// Name: "gcp-provider", +// Namespace: "gcp", +// Cloud: "gcp", +// }, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Provider{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "gcp-provider", +// }, +// Spec: v1alpha1.ProviderSpec{ +// Cloud: "gcp", +// CloudSettings: &v1alpha1.CloudProviderSettings{ +// GCP: &corev1.SecretReference{ +// Name: "credentials", +// }, +// }, +// Name: "gcp-provider", +// Namespace: "gcp", +// }, +// Status: v1alpha1.ProviderStatus{ +// ID: lo.ToPtr("1234"), +// SHA: lo.ToPtr(""), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// }, +// }, +// }, +// &corev1.Secret{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "credentials", +// }, +// Data: map[string][]byte{ +// "applicationCredentials": []byte("mock"), +// }, +// }, +// }, +// expectedStatus: v1alpha1.ProviderStatus{ +// ID: lo.ToPtr("1234"), +// SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// } +// +// t.Run(test.name, func(t *testing.T) { +// // set up the test scenario +// fakeClient := fake. +// NewClientBuilder(). +// WithScheme(scheme.Scheme). +// WithObjects(test.existingObjects...). +// Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// +// // act +// ctx := context.Background() +// providerReconciler := &controller.ProviderReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(true, nil) +// fakeConsoleClient.On("UpdateProvider", mock.Anything, mock.Anything, mock.Anything).Return(test.returnUpdateProvider, nil) +// +// _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) +// assert.NoError(t, err) +// +// existingProvider := &v1alpha1.Provider{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) +// +// existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) +// expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) +// +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) +// }) +//} diff --git a/controller/internal/controller/servicedeployment_controller_test.go b/controller/internal/controller/servicedeployment_controller_test.go index 06b523732..a52d36e40 100644 --- a/controller/internal/controller/servicedeployment_controller_test.go +++ b/controller/internal/controller/servicedeployment_controller_test.go @@ -1,321 +1,322 @@ package controller_test -import ( - "context" - "encoding/json" - "testing" - "time" - - gqlclient "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/controller" - "github.com/pluralsh/console/controller/internal/test/mocks" - "github.com/samber/lo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes/scheme" - ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -func init() { - utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -} - -func TestCreateNewService(t *testing.T) { - const ( - serviceName = "test" - clusterName = "testCluster" - repoName = "testRepo" - ) - tests := []struct { - name string - service string - returnGetService *gqlclient.ServiceDeploymentExtended - returnIsClusterExisting bool - returnCreateCluster *gqlclient.CreateServiceDeployment - returnErrorCreateCluster error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ServiceStatus - }{ - { - name: "scenario 1: create a new service", - service: "test", - expectedStatus: v1alpha1.ServiceStatus{ - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetService: &gqlclient.ServiceDeploymentExtended{ - ID: "123", - }, - returnIsClusterExisting: false, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.ServiceDeployment{ - ObjectMeta: metav1.ObjectMeta{Name: serviceName}, - Spec: v1alpha1.ServiceSpec{ - Version: "1.24", - ClusterRef: corev1.ObjectReference{Name: clusterName}, - RepositoryRef: corev1.ObjectReference{Name: repoName}, - }, - }, - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - }, - Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr("123"), - }, - }, - &v1alpha1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: repoName, - }, - Status: v1alpha1.GitRepositoryStatus{ - ID: lo.ToPtr("123"), - Health: v1alpha1.GitHealthPullable, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() - fakeConsoleClient.On("CreateService", mock.Anything, mock.Anything).Return(nil, nil) - fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) - - ctx := context.Background() - - target := &controller.ServiceReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) - assert.NoError(t, err) - - existingService := &v1alpha1.ServiceDeployment{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) - assert.NoError(t, err) - existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) - assert.NoError(t, err) - expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} - -func sanitizeServiceConditions(status v1alpha1.ServiceStatus) v1alpha1.ServiceStatus { - for i := range status.Conditions { - status.Conditions[i].LastTransitionTime = metav1.Time{} - status.Conditions[i].ObservedGeneration = 0 - } - - return status -} - -func TestDeleteService(t *testing.T) { - const ( - serviceName = "test" - clusterName = "testCluster" - repoName = "testRepo" - ) - tests := []struct { - name string - service string - returnGetService *gqlclient.ServiceDeploymentExtended - returnIsClusterExisting bool - returnCreateCluster *gqlclient.CreateServiceDeployment - returnErrorCreateCluster error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ServiceStatus - }{ - { - name: "scenario 1: delete service", - service: "test", - expectedStatus: v1alpha1.ServiceStatus{ - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), - }, - returnGetService: &gqlclient.ServiceDeploymentExtended{ - ID: "123", - }, - returnIsClusterExisting: false, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.ServiceDeployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceName, - DeletionTimestamp: &metav1.Time{Time: time.Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, - Finalizers: []string{controller.ServiceFinalizer}, - }, - Spec: v1alpha1.ServiceSpec{ - Version: "1.24", - ClusterRef: corev1.ObjectReference{Name: clusterName}, - RepositoryRef: corev1.ObjectReference{Name: repoName}, - }, - Status: v1alpha1.ServiceStatus{ - ID: lo.ToPtr("123"), - }, - }, - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - }, - Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr("123"), - }, - }, - &v1alpha1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: repoName, - }, - Status: v1alpha1.GitRepositoryStatus{ - ID: lo.ToPtr("123"), - Health: v1alpha1.GitHealthPullable, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() - ctx := context.Background() - - target := &controller.ServiceReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) - assert.NoError(t, err) - - existingService := &v1alpha1.ServiceDeployment{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) - assert.True(t, apierrors.IsNotFound(err)) - }) - } -} - -func TestUpdateService(t *testing.T) { - const ( - serviceName = "test" - clusterName = "testCluster" - repoName = "testRepo" - ) - tests := []struct { - name string - service string - returnGetService *gqlclient.ServiceDeploymentExtended - returnIsClusterExisting bool - returnCreateCluster *gqlclient.CreateServiceDeployment - returnErrorCreateCluster error - existingObjects []ctrlruntimeclient.Object - expectedStatus v1alpha1.ServiceStatus - }{ - { - name: "scenario 1: update service", - service: "test", - expectedStatus: v1alpha1.ServiceStatus{ - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }, - returnGetService: &gqlclient.ServiceDeploymentExtended{ - ID: "123", - }, - returnIsClusterExisting: false, - existingObjects: []ctrlruntimeclient.Object{ - &v1alpha1.ServiceDeployment{ - ObjectMeta: metav1.ObjectMeta{Name: serviceName}, - Spec: v1alpha1.ServiceSpec{ - Version: "1.24", - ClusterRef: corev1.ObjectReference{Name: clusterName}, - RepositoryRef: corev1.ObjectReference{Name: repoName}, - }, - Status: v1alpha1.ServiceStatus{ - ID: lo.ToPtr("123"), - SHA: lo.ToPtr("abc"), - }, - }, - &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - }, - Status: v1alpha1.ClusterStatus{ - ID: lo.ToPtr("123"), - }, - }, - &v1alpha1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: repoName, - }, - Status: v1alpha1.GitRepositoryStatus{ - ID: lo.ToPtr("123"), - Health: v1alpha1.GitHealthPullable, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() - - fakeConsoleClient := mocks.NewConsoleClient(t) - fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) - fakeConsoleClient.On("UpdateService", mock.Anything, mock.Anything).Return(nil) - - ctx := context.Background() - - target := &controller.ServiceReconciler{ - Client: fakeClient, - Scheme: scheme.Scheme, - ConsoleClient: fakeConsoleClient, - } - - _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) - assert.NoError(t, err) - - existingService := &v1alpha1.ServiceDeployment{} - err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) - assert.NoError(t, err) - existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) - assert.NoError(t, err) - expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) - assert.NoError(t, err) - assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) - }) - } -} +// +//import ( +// "context" +// "encoding/json" +// "testing" +// "time" +// +// gqlclient "github.com/pluralsh/console-client-go" +// "github.com/pluralsh/console/controller/api/v1alpha1" +// "github.com/pluralsh/console/controller/internal/controller" +// "github.com/pluralsh/console/controller/internal/test/mocks" +// "github.com/samber/lo" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/mock" +// corev1 "k8s.io/api/core/v1" +// apierrors "k8s.io/apimachinery/pkg/api/errors" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/types" +// utilruntime "k8s.io/apimachinery/pkg/util/runtime" +// "k8s.io/client-go/kubernetes/scheme" +// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/controller-runtime/pkg/client/fake" +// "sigs.k8s.io/controller-runtime/pkg/reconcile" +//) +// +//func init() { +// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) +//} +// +//func TestCreateNewService(t *testing.T) { +// const ( +// serviceName = "test" +// clusterName = "testCluster" +// repoName = "testRepo" +// ) +// tests := []struct { +// name string +// service string +// returnGetService *gqlclient.ServiceDeploymentExtended +// returnIsClusterExisting bool +// returnCreateCluster *gqlclient.CreateServiceDeployment +// returnErrorCreateCluster error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ServiceStatus +// }{ +// { +// name: "scenario 1: create a new service", +// service: "test", +// expectedStatus: v1alpha1.ServiceStatus{ +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetService: &gqlclient.ServiceDeploymentExtended{ +// ID: "123", +// }, +// returnIsClusterExisting: false, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.ServiceDeployment{ +// ObjectMeta: metav1.ObjectMeta{Name: serviceName}, +// Spec: v1alpha1.ServiceSpec{ +// Version: "1.24", +// ClusterRef: corev1.ObjectReference{Name: clusterName}, +// RepositoryRef: corev1.ObjectReference{Name: repoName}, +// }, +// }, +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: clusterName, +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr("123"), +// }, +// }, +// &v1alpha1.GitRepository{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: repoName, +// }, +// Status: v1alpha1.GitRepositoryStatus{ +// ID: lo.ToPtr("123"), +// Health: v1alpha1.GitHealthPullable, +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() +// fakeConsoleClient.On("CreateService", mock.Anything, mock.Anything).Return(nil, nil) +// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) +// +// ctx := context.Background() +// +// target := &controller.ServiceReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) +// assert.NoError(t, err) +// +// existingService := &v1alpha1.ServiceDeployment{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) +// assert.NoError(t, err) +// existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) +// assert.NoError(t, err) +// expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} +// +//func sanitizeServiceConditions(status v1alpha1.ServiceStatus) v1alpha1.ServiceStatus { +// for i := range status.Conditions { +// status.Conditions[i].LastTransitionTime = metav1.Time{} +// status.Conditions[i].ObservedGeneration = 0 +// } +// +// return status +//} +// +//func TestDeleteService(t *testing.T) { +// const ( +// serviceName = "test" +// clusterName = "testCluster" +// repoName = "testRepo" +// ) +// tests := []struct { +// name string +// service string +// returnGetService *gqlclient.ServiceDeploymentExtended +// returnIsClusterExisting bool +// returnCreateCluster *gqlclient.CreateServiceDeployment +// returnErrorCreateCluster error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ServiceStatus +// }{ +// { +// name: "scenario 1: delete service", +// service: "test", +// expectedStatus: v1alpha1.ServiceStatus{ +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), +// }, +// returnGetService: &gqlclient.ServiceDeploymentExtended{ +// ID: "123", +// }, +// returnIsClusterExisting: false, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.ServiceDeployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: serviceName, +// DeletionTimestamp: &metav1.Time{Time: time.Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, +// Finalizers: []string{controller.ServiceFinalizer}, +// }, +// Spec: v1alpha1.ServiceSpec{ +// Version: "1.24", +// ClusterRef: corev1.ObjectReference{Name: clusterName}, +// RepositoryRef: corev1.ObjectReference{Name: repoName}, +// }, +// Status: v1alpha1.ServiceStatus{ +// ID: lo.ToPtr("123"), +// }, +// }, +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: clusterName, +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr("123"), +// }, +// }, +// &v1alpha1.GitRepository{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: repoName, +// }, +// Status: v1alpha1.GitRepositoryStatus{ +// ID: lo.ToPtr("123"), +// Health: v1alpha1.GitHealthPullable, +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() +// ctx := context.Background() +// +// target := &controller.ServiceReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) +// assert.NoError(t, err) +// +// existingService := &v1alpha1.ServiceDeployment{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) +// assert.True(t, apierrors.IsNotFound(err)) +// }) +// } +//} +// +//func TestUpdateService(t *testing.T) { +// const ( +// serviceName = "test" +// clusterName = "testCluster" +// repoName = "testRepo" +// ) +// tests := []struct { +// name string +// service string +// returnGetService *gqlclient.ServiceDeploymentExtended +// returnIsClusterExisting bool +// returnCreateCluster *gqlclient.CreateServiceDeployment +// returnErrorCreateCluster error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ServiceStatus +// }{ +// { +// name: "scenario 1: update service", +// service: "test", +// expectedStatus: v1alpha1.ServiceStatus{ +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, +// }, +// }, +// returnGetService: &gqlclient.ServiceDeploymentExtended{ +// ID: "123", +// }, +// returnIsClusterExisting: false, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.ServiceDeployment{ +// ObjectMeta: metav1.ObjectMeta{Name: serviceName}, +// Spec: v1alpha1.ServiceSpec{ +// Version: "1.24", +// ClusterRef: corev1.ObjectReference{Name: clusterName}, +// RepositoryRef: corev1.ObjectReference{Name: repoName}, +// }, +// Status: v1alpha1.ServiceStatus{ +// ID: lo.ToPtr("123"), +// SHA: lo.ToPtr("abc"), +// }, +// }, +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: clusterName, +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr("123"), +// }, +// }, +// &v1alpha1.GitRepository{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: repoName, +// }, +// Status: v1alpha1.GitRepositoryStatus{ +// ID: lo.ToPtr("123"), +// Health: v1alpha1.GitHealthPullable, +// }, +// }, +// }, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) +// fakeConsoleClient.On("UpdateService", mock.Anything, mock.Anything).Return(nil) +// +// ctx := context.Background() +// +// target := &controller.ServiceReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } +// +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) +// assert.NoError(t, err) +// +// existingService := &v1alpha1.ServiceDeployment{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) +// assert.NoError(t, err) +// existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) +// assert.NoError(t, err) +// expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } +//} diff --git a/controller/internal/test/e2e/e2e_suite_test.go b/controller/internal/test/e2e/e2e_suite_test.go new file mode 100644 index 000000000..1008625dd --- /dev/null +++ b/controller/internal/test/e2e/e2e_suite_test.go @@ -0,0 +1,16 @@ +package e2e + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + fmt.Fprintf(GinkgoWriter, "Starting deployment-controller suite\n") + RunSpecs(t, "e2e suite") +} diff --git a/controller/internal/test/mocks/ConsoleClient.go b/controller/internal/test/mocks/ConsoleClient.go deleted file mode 100644 index 5ba4ab7b6..000000000 --- a/controller/internal/test/mocks/ConsoleClient.go +++ /dev/null @@ -1,740 +0,0 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - gqlgencclient "github.com/Yamashou/gqlgenc/client" - gqlclient "github.com/pluralsh/console-client-go" - - mock "github.com/stretchr/testify/mock" - - v1alpha1 "github.com/pluralsh/console/controller/api/v1alpha1" -) - -// ConsoleClient is an autogenerated mock type for the ConsoleClient type -type ConsoleClient struct { - mock.Mock -} - -// CreateCluster provides a mock function with given fields: attrs -func (_m *ConsoleClient) CreateCluster(attrs gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error) { - ret := _m.Called(attrs) - - if len(ret) == 0 { - panic("no return value specified for CreateCluster") - } - - var r0 *gqlclient.ClusterFragment - var r1 error - if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error)); ok { - return rf(attrs) - } - if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) *gqlclient.ClusterFragment); ok { - r0 = rf(attrs) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterFragment) - } - } - - if rf, ok := ret.Get(1).(func(gqlclient.ClusterAttributes) error); ok { - r1 = rf(attrs) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CreateProvider provides a mock function with given fields: ctx, attributes, options -func (_m *ConsoleClient) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, attributes) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for CreateProvider") - } - - var r0 *gqlclient.ClusterProviderFragment - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { - return rf(ctx, attributes, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { - r0 = rf(ctx, attributes, options...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) error); ok { - r1 = rf(ctx, attributes, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CreateRepository provides a mock function with given fields: url, privateKey, passphrase, username, password -func (_m *ConsoleClient) CreateRepository(url string, privateKey *string, passphrase *string, username *string, password *string) (*gqlclient.CreateGitRepository, error) { - ret := _m.Called(url, privateKey, passphrase, username, password) - - if len(ret) == 0 { - panic("no return value specified for CreateRepository") - } - - var r0 *gqlclient.CreateGitRepository - var r1 error - if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) (*gqlclient.CreateGitRepository, error)); ok { - return rf(url, privateKey, passphrase, username, password) - } - if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) *gqlclient.CreateGitRepository); ok { - r0 = rf(url, privateKey, passphrase, username, password) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.CreateGitRepository) - } - } - - if rf, ok := ret.Get(1).(func(string, *string, *string, *string, *string) error); ok { - r1 = rf(url, privateKey, passphrase, username, password) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CreateService provides a mock function with given fields: clusterId, attributes -func (_m *ConsoleClient) CreateService(clusterId *string, attributes gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error) { - ret := _m.Called(clusterId, attributes) - - if len(ret) == 0 { - panic("no return value specified for CreateService") - } - - var r0 *gqlclient.ServiceDeploymentFragment - var r1 error - if rf, ok := ret.Get(0).(func(*string, gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error)); ok { - return rf(clusterId, attributes) - } - if rf, ok := ret.Get(0).(func(*string, gqlclient.ServiceDeploymentAttributes) *gqlclient.ServiceDeploymentFragment); ok { - r0 = rf(clusterId, attributes) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ServiceDeploymentFragment) - } - } - - if rf, ok := ret.Get(1).(func(*string, gqlclient.ServiceDeploymentAttributes) error); ok { - r1 = rf(clusterId, attributes) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// DeleteCluster provides a mock function with given fields: id -func (_m *ConsoleClient) DeleteCluster(id string) (*gqlclient.ClusterFragment, error) { - ret := _m.Called(id) - - if len(ret) == 0 { - panic("no return value specified for DeleteCluster") - } - - var r0 *gqlclient.ClusterFragment - var r1 error - if rf, ok := ret.Get(0).(func(string) (*gqlclient.ClusterFragment, error)); ok { - return rf(id) - } - if rf, ok := ret.Get(0).(func(string) *gqlclient.ClusterFragment); ok { - r0 = rf(id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterFragment) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// DeleteProvider provides a mock function with given fields: ctx, id, options -func (_m *ConsoleClient) DeleteProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) error { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, id) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for DeleteProvider") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error); ok { - r0 = rf(ctx, id, options...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// DeleteRepository provides a mock function with given fields: id -func (_m *ConsoleClient) DeleteRepository(id string) error { - ret := _m.Called(id) - - if len(ret) == 0 { - panic("no return value specified for DeleteRepository") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// DeleteService provides a mock function with given fields: serviceId -func (_m *ConsoleClient) DeleteService(serviceId string) error { - ret := _m.Called(serviceId) - - if len(ret) == 0 { - panic("no return value specified for DeleteService") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(serviceId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// GetCluster provides a mock function with given fields: id -func (_m *ConsoleClient) GetCluster(id *string) (*gqlclient.ClusterFragment, error) { - ret := _m.Called(id) - - if len(ret) == 0 { - panic("no return value specified for GetCluster") - } - - var r0 *gqlclient.ClusterFragment - var r1 error - if rf, ok := ret.Get(0).(func(*string) (*gqlclient.ClusterFragment, error)); ok { - return rf(id) - } - if rf, ok := ret.Get(0).(func(*string) *gqlclient.ClusterFragment); ok { - r0 = rf(id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterFragment) - } - } - - if rf, ok := ret.Get(1).(func(*string) error); ok { - r1 = rf(id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetClusterByHandle provides a mock function with given fields: handle -func (_m *ConsoleClient) GetClusterByHandle(handle *string) (*gqlclient.ClusterFragment, error) { - ret := _m.Called(handle) - - if len(ret) == 0 { - panic("no return value specified for GetClusterByHandle") - } - - var r0 *gqlclient.ClusterFragment - var r1 error - if rf, ok := ret.Get(0).(func(*string) (*gqlclient.ClusterFragment, error)); ok { - return rf(handle) - } - if rf, ok := ret.Get(0).(func(*string) *gqlclient.ClusterFragment); ok { - r0 = rf(handle) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterFragment) - } - } - - if rf, ok := ret.Get(1).(func(*string) error); ok { - r1 = rf(handle) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetProvider provides a mock function with given fields: ctx, id, options -func (_m *ConsoleClient) GetProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, id) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for GetProvider") - } - - var r0 *gqlclient.ClusterProviderFragment - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { - return rf(ctx, id, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { - r0 = rf(ctx, id, options...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error); ok { - r1 = rf(ctx, id, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetProviderByCloud provides a mock function with given fields: ctx, cloud, options -func (_m *ConsoleClient) GetProviderByCloud(ctx context.Context, cloud v1alpha1.CloudProvider, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, cloud) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for GetProviderByCloud") - } - - var r0 *gqlclient.ClusterProviderFragment - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { - return rf(ctx, cloud, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { - r0 = rf(ctx, cloud, options...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) error); ok { - r1 = rf(ctx, cloud, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetRepository provides a mock function with given fields: url -func (_m *ConsoleClient) GetRepository(url *string) (*gqlclient.GetGitRepository, error) { - ret := _m.Called(url) - - if len(ret) == 0 { - panic("no return value specified for GetRepository") - } - - var r0 *gqlclient.GetGitRepository - var r1 error - if rf, ok := ret.Get(0).(func(*string) (*gqlclient.GetGitRepository, error)); ok { - return rf(url) - } - if rf, ok := ret.Get(0).(func(*string) *gqlclient.GetGitRepository); ok { - r0 = rf(url) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.GetGitRepository) - } - } - - if rf, ok := ret.Get(1).(func(*string) error); ok { - r1 = rf(url) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetService provides a mock function with given fields: clusterID, serviceName -func (_m *ConsoleClient) GetService(clusterID string, serviceName string) (*gqlclient.ServiceDeploymentExtended, error) { - ret := _m.Called(clusterID, serviceName) - - if len(ret) == 0 { - panic("no return value specified for GetService") - } - - var r0 *gqlclient.ServiceDeploymentExtended - var r1 error - if rf, ok := ret.Get(0).(func(string, string) (*gqlclient.ServiceDeploymentExtended, error)); ok { - return rf(clusterID, serviceName) - } - if rf, ok := ret.Get(0).(func(string, string) *gqlclient.ServiceDeploymentExtended); ok { - r0 = rf(clusterID, serviceName) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ServiceDeploymentExtended) - } - } - - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(clusterID, serviceName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetServices provides a mock function with given fields: -func (_m *ConsoleClient) GetServices() ([]*gqlclient.ServiceDeploymentBaseFragment, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetServices") - } - - var r0 []*gqlclient.ServiceDeploymentBaseFragment - var r1 error - if rf, ok := ret.Get(0).(func() ([]*gqlclient.ServiceDeploymentBaseFragment, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() []*gqlclient.ServiceDeploymentBaseFragment); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*gqlclient.ServiceDeploymentBaseFragment) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// IsClusterDeleting provides a mock function with given fields: id -func (_m *ConsoleClient) IsClusterDeleting(id *string) bool { - ret := _m.Called(id) - - if len(ret) == 0 { - panic("no return value specified for IsClusterDeleting") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(*string) bool); ok { - r0 = rf(id) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// IsClusterExisting provides a mock function with given fields: id -func (_m *ConsoleClient) IsClusterExisting(id *string) bool { - ret := _m.Called(id) - - if len(ret) == 0 { - panic("no return value specified for IsClusterExisting") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(*string) bool); ok { - r0 = rf(id) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// IsProviderDeleting provides a mock function with given fields: ctx, id -func (_m *ConsoleClient) IsProviderDeleting(ctx context.Context, id string) bool { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for IsProviderDeleting") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// IsProviderExists provides a mock function with given fields: ctx, id -func (_m *ConsoleClient) IsProviderExists(ctx context.Context, id string) bool { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for IsProviderExists") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// ListClusters provides a mock function with given fields: -func (_m *ConsoleClient) ListClusters() (*gqlclient.ListClusters, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ListClusters") - } - - var r0 *gqlclient.ListClusters - var r1 error - if rf, ok := ret.Get(0).(func() (*gqlclient.ListClusters, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() *gqlclient.ListClusters); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ListClusters) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListRepositories provides a mock function with given fields: -func (_m *ConsoleClient) ListRepositories() (*gqlclient.ListGitRepositories, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ListRepositories") - } - - var r0 *gqlclient.ListGitRepositories - var r1 error - if rf, ok := ret.Get(0).(func() (*gqlclient.ListGitRepositories, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() *gqlclient.ListGitRepositories); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ListGitRepositories) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UpdateCluster provides a mock function with given fields: id, attrs -func (_m *ConsoleClient) UpdateCluster(id string, attrs gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error) { - ret := _m.Called(id, attrs) - - if len(ret) == 0 { - panic("no return value specified for UpdateCluster") - } - - var r0 *gqlclient.ClusterFragment - var r1 error - if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error)); ok { - return rf(id, attrs) - } - if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) *gqlclient.ClusterFragment); ok { - r0 = rf(id, attrs) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterFragment) - } - } - - if rf, ok := ret.Get(1).(func(string, gqlclient.ClusterUpdateAttributes) error); ok { - r1 = rf(id, attrs) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UpdateComponents provides a mock function with given fields: id, components, errs -func (_m *ConsoleClient) UpdateComponents(id string, components []*gqlclient.ComponentAttributes, errs []*gqlclient.ServiceErrorAttributes) error { - ret := _m.Called(id, components, errs) - - if len(ret) == 0 { - panic("no return value specified for UpdateComponents") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, []*gqlclient.ComponentAttributes, []*gqlclient.ServiceErrorAttributes) error); ok { - r0 = rf(id, components, errs) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdateProvider provides a mock function with given fields: ctx, id, attributes, options -func (_m *ConsoleClient) UpdateProvider(ctx context.Context, id string, attributes gqlclient.ClusterProviderUpdateAttributes, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, id, attributes) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for UpdateProvider") - } - - var r0 *gqlclient.ClusterProviderFragment - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { - return rf(ctx, id, attributes, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { - r0 = rf(ctx, id, attributes, options...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) error); ok { - r1 = rf(ctx, id, attributes, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UpdateRepository provides a mock function with given fields: id, attrs -func (_m *ConsoleClient) UpdateRepository(id string, attrs gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error) { - ret := _m.Called(id, attrs) - - if len(ret) == 0 { - panic("no return value specified for UpdateRepository") - } - - var r0 *gqlclient.UpdateGitRepository - var r1 error - if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error)); ok { - return rf(id, attrs) - } - if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) *gqlclient.UpdateGitRepository); ok { - r0 = rf(id, attrs) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*gqlclient.UpdateGitRepository) - } - } - - if rf, ok := ret.Get(1).(func(string, gqlclient.GitAttributes) error); ok { - r1 = rf(id, attrs) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UpdateService provides a mock function with given fields: serviceId, attributes -func (_m *ConsoleClient) UpdateService(serviceId string, attributes gqlclient.ServiceUpdateAttributes) error { - ret := _m.Called(serviceId, attributes) - - if len(ret) == 0 { - panic("no return value specified for UpdateService") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, gqlclient.ServiceUpdateAttributes) error); ok { - r0 = rf(serviceId, attributes) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewConsoleClient creates a new instance of ConsoleClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewConsoleClient(t interface { - mock.TestingT - Cleanup(func()) -}) *ConsoleClient { - mock := &ConsoleClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/controller/internal/test/mocks/ConsoleClient_mock.go b/controller/internal/test/mocks/ConsoleClient_mock.go new file mode 100644 index 000000000..f19696764 --- /dev/null +++ b/controller/internal/test/mocks/ConsoleClient_mock.go @@ -0,0 +1,1532 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + gqlgencclient "github.com/Yamashou/gqlgenc/client" + gqlclient "github.com/pluralsh/console-client-go" + + mock "github.com/stretchr/testify/mock" + + v1alpha1 "github.com/pluralsh/console/controller/api/v1alpha1" +) + +// ConsoleClientMock is an autogenerated mock type for the ConsoleClient type +type ConsoleClientMock struct { + mock.Mock +} + +type ConsoleClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *ConsoleClientMock) EXPECT() *ConsoleClientMock_Expecter { + return &ConsoleClientMock_Expecter{mock: &_m.Mock} +} + +// CreateCluster provides a mock function with given fields: attrs +func (_m *ConsoleClientMock) CreateCluster(attrs gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(attrs) + + if len(ret) == 0 { + panic("no return value specified for CreateCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error)); ok { + return rf(attrs) + } + if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) *gqlclient.ClusterFragment); ok { + r0 = rf(attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(gqlclient.ClusterAttributes) error); ok { + r1 = rf(attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_CreateCluster_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateCluster' +type ConsoleClientMock_CreateCluster_Call struct { + *mock.Call +} + +// CreateCluster is a helper method to define mock.On call +// - attrs gqlclient.ClusterAttributes +func (_e *ConsoleClientMock_Expecter) CreateCluster(attrs interface{}) *ConsoleClientMock_CreateCluster_Call { + return &ConsoleClientMock_CreateCluster_Call{Call: _e.mock.On("CreateCluster", attrs)} +} + +func (_c *ConsoleClientMock_CreateCluster_Call) Run(run func(attrs gqlclient.ClusterAttributes)) *ConsoleClientMock_CreateCluster_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(gqlclient.ClusterAttributes)) + }) + return _c +} + +func (_c *ConsoleClientMock_CreateCluster_Call) Return(_a0 *gqlclient.ClusterFragment, _a1 error) *ConsoleClientMock_CreateCluster_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_CreateCluster_Call) RunAndReturn(run func(gqlclient.ClusterAttributes) (*gqlclient.ClusterFragment, error)) *ConsoleClientMock_CreateCluster_Call { + _c.Call.Return(run) + return _c +} + +// CreateProvider provides a mock function with given fields: ctx, attributes, options +func (_m *ConsoleClientMock) CreateProvider(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, attributes) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateProvider") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, attributes, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, attributes, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, attributes, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_CreateProvider_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateProvider' +type ConsoleClientMock_CreateProvider_Call struct { + *mock.Call +} + +// CreateProvider is a helper method to define mock.On call +// - ctx context.Context +// - attributes gqlclient.ClusterProviderAttributes +// - options ...gqlgencclient.HTTPRequestOption +func (_e *ConsoleClientMock_Expecter) CreateProvider(ctx interface{}, attributes interface{}, options ...interface{}) *ConsoleClientMock_CreateProvider_Call { + return &ConsoleClientMock_CreateProvider_Call{Call: _e.mock.On("CreateProvider", + append([]interface{}{ctx, attributes}, options...)...)} +} + +func (_c *ConsoleClientMock_CreateProvider_Call) Run(run func(ctx context.Context, attributes gqlclient.ClusterProviderAttributes, options ...gqlgencclient.HTTPRequestOption)) *ConsoleClientMock_CreateProvider_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]gqlgencclient.HTTPRequestOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(gqlgencclient.HTTPRequestOption) + } + } + run(args[0].(context.Context), args[1].(gqlclient.ClusterProviderAttributes), variadicArgs...) + }) + return _c +} + +func (_c *ConsoleClientMock_CreateProvider_Call) Return(_a0 *gqlclient.ClusterProviderFragment, _a1 error) *ConsoleClientMock_CreateProvider_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_CreateProvider_Call) RunAndReturn(run func(context.Context, gqlclient.ClusterProviderAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)) *ConsoleClientMock_CreateProvider_Call { + _c.Call.Return(run) + return _c +} + +// CreateRepository provides a mock function with given fields: url, privateKey, passphrase, username, password +func (_m *ConsoleClientMock) CreateRepository(url string, privateKey *string, passphrase *string, username *string, password *string) (*gqlclient.CreateGitRepository, error) { + ret := _m.Called(url, privateKey, passphrase, username, password) + + if len(ret) == 0 { + panic("no return value specified for CreateRepository") + } + + var r0 *gqlclient.CreateGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) (*gqlclient.CreateGitRepository, error)); ok { + return rf(url, privateKey, passphrase, username, password) + } + if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) *gqlclient.CreateGitRepository); ok { + r0 = rf(url, privateKey, passphrase, username, password) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.CreateGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(string, *string, *string, *string, *string) error); ok { + r1 = rf(url, privateKey, passphrase, username, password) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_CreateRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRepository' +type ConsoleClientMock_CreateRepository_Call struct { + *mock.Call +} + +// CreateRepository is a helper method to define mock.On call +// - url string +// - privateKey *string +// - passphrase *string +// - username *string +// - password *string +func (_e *ConsoleClientMock_Expecter) CreateRepository(url interface{}, privateKey interface{}, passphrase interface{}, username interface{}, password interface{}) *ConsoleClientMock_CreateRepository_Call { + return &ConsoleClientMock_CreateRepository_Call{Call: _e.mock.On("CreateRepository", url, privateKey, passphrase, username, password)} +} + +func (_c *ConsoleClientMock_CreateRepository_Call) Run(run func(url string, privateKey *string, passphrase *string, username *string, password *string)) *ConsoleClientMock_CreateRepository_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(*string), args[2].(*string), args[3].(*string), args[4].(*string)) + }) + return _c +} + +func (_c *ConsoleClientMock_CreateRepository_Call) Return(_a0 *gqlclient.CreateGitRepository, _a1 error) *ConsoleClientMock_CreateRepository_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_CreateRepository_Call) RunAndReturn(run func(string, *string, *string, *string, *string) (*gqlclient.CreateGitRepository, error)) *ConsoleClientMock_CreateRepository_Call { + _c.Call.Return(run) + return _c +} + +// CreateService provides a mock function with given fields: clusterId, attributes +func (_m *ConsoleClientMock) CreateService(clusterId *string, attributes gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + ret := _m.Called(clusterId, attributes) + + if len(ret) == 0 { + panic("no return value specified for CreateService") + } + + var r0 *gqlclient.ServiceDeploymentFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string, gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error)); ok { + return rf(clusterId, attributes) + } + if rf, ok := ret.Get(0).(func(*string, gqlclient.ServiceDeploymentAttributes) *gqlclient.ServiceDeploymentFragment); ok { + r0 = rf(clusterId, attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string, gqlclient.ServiceDeploymentAttributes) error); ok { + r1 = rf(clusterId, attributes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_CreateService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateService' +type ConsoleClientMock_CreateService_Call struct { + *mock.Call +} + +// CreateService is a helper method to define mock.On call +// - clusterId *string +// - attributes gqlclient.ServiceDeploymentAttributes +func (_e *ConsoleClientMock_Expecter) CreateService(clusterId interface{}, attributes interface{}) *ConsoleClientMock_CreateService_Call { + return &ConsoleClientMock_CreateService_Call{Call: _e.mock.On("CreateService", clusterId, attributes)} +} + +func (_c *ConsoleClientMock_CreateService_Call) Run(run func(clusterId *string, attributes gqlclient.ServiceDeploymentAttributes)) *ConsoleClientMock_CreateService_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*string), args[1].(gqlclient.ServiceDeploymentAttributes)) + }) + return _c +} + +func (_c *ConsoleClientMock_CreateService_Call) Return(_a0 *gqlclient.ServiceDeploymentFragment, _a1 error) *ConsoleClientMock_CreateService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_CreateService_Call) RunAndReturn(run func(*string, gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error)) *ConsoleClientMock_CreateService_Call { + _c.Call.Return(run) + return _c +} + +// DeleteCluster provides a mock function with given fields: id +func (_m *ConsoleClientMock) DeleteCluster(id string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for DeleteCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(string) (*gqlclient.ClusterFragment, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(string) *gqlclient.ClusterFragment); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_DeleteCluster_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteCluster' +type ConsoleClientMock_DeleteCluster_Call struct { + *mock.Call +} + +// DeleteCluster is a helper method to define mock.On call +// - id string +func (_e *ConsoleClientMock_Expecter) DeleteCluster(id interface{}) *ConsoleClientMock_DeleteCluster_Call { + return &ConsoleClientMock_DeleteCluster_Call{Call: _e.mock.On("DeleteCluster", id)} +} + +func (_c *ConsoleClientMock_DeleteCluster_Call) Run(run func(id string)) *ConsoleClientMock_DeleteCluster_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *ConsoleClientMock_DeleteCluster_Call) Return(_a0 *gqlclient.ClusterFragment, _a1 error) *ConsoleClientMock_DeleteCluster_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_DeleteCluster_Call) RunAndReturn(run func(string) (*gqlclient.ClusterFragment, error)) *ConsoleClientMock_DeleteCluster_Call { + _c.Call.Return(run) + return _c +} + +// DeleteProvider provides a mock function with given fields: ctx, id, options +func (_m *ConsoleClientMock) DeleteProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteProvider") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error); ok { + r0 = rf(ctx, id, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ConsoleClientMock_DeleteProvider_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteProvider' +type ConsoleClientMock_DeleteProvider_Call struct { + *mock.Call +} + +// DeleteProvider is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - options ...gqlgencclient.HTTPRequestOption +func (_e *ConsoleClientMock_Expecter) DeleteProvider(ctx interface{}, id interface{}, options ...interface{}) *ConsoleClientMock_DeleteProvider_Call { + return &ConsoleClientMock_DeleteProvider_Call{Call: _e.mock.On("DeleteProvider", + append([]interface{}{ctx, id}, options...)...)} +} + +func (_c *ConsoleClientMock_DeleteProvider_Call) Run(run func(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption)) *ConsoleClientMock_DeleteProvider_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]gqlgencclient.HTTPRequestOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(gqlgencclient.HTTPRequestOption) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *ConsoleClientMock_DeleteProvider_Call) Return(_a0 error) *ConsoleClientMock_DeleteProvider_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_DeleteProvider_Call) RunAndReturn(run func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error) *ConsoleClientMock_DeleteProvider_Call { + _c.Call.Return(run) + return _c +} + +// DeleteRepository provides a mock function with given fields: id +func (_m *ConsoleClientMock) DeleteRepository(id string) error { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for DeleteRepository") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ConsoleClientMock_DeleteRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRepository' +type ConsoleClientMock_DeleteRepository_Call struct { + *mock.Call +} + +// DeleteRepository is a helper method to define mock.On call +// - id string +func (_e *ConsoleClientMock_Expecter) DeleteRepository(id interface{}) *ConsoleClientMock_DeleteRepository_Call { + return &ConsoleClientMock_DeleteRepository_Call{Call: _e.mock.On("DeleteRepository", id)} +} + +func (_c *ConsoleClientMock_DeleteRepository_Call) Run(run func(id string)) *ConsoleClientMock_DeleteRepository_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *ConsoleClientMock_DeleteRepository_Call) Return(_a0 error) *ConsoleClientMock_DeleteRepository_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_DeleteRepository_Call) RunAndReturn(run func(string) error) *ConsoleClientMock_DeleteRepository_Call { + _c.Call.Return(run) + return _c +} + +// DeleteService provides a mock function with given fields: serviceId +func (_m *ConsoleClientMock) DeleteService(serviceId string) error { + ret := _m.Called(serviceId) + + if len(ret) == 0 { + panic("no return value specified for DeleteService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(serviceId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ConsoleClientMock_DeleteService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteService' +type ConsoleClientMock_DeleteService_Call struct { + *mock.Call +} + +// DeleteService is a helper method to define mock.On call +// - serviceId string +func (_e *ConsoleClientMock_Expecter) DeleteService(serviceId interface{}) *ConsoleClientMock_DeleteService_Call { + return &ConsoleClientMock_DeleteService_Call{Call: _e.mock.On("DeleteService", serviceId)} +} + +func (_c *ConsoleClientMock_DeleteService_Call) Run(run func(serviceId string)) *ConsoleClientMock_DeleteService_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *ConsoleClientMock_DeleteService_Call) Return(_a0 error) *ConsoleClientMock_DeleteService_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_DeleteService_Call) RunAndReturn(run func(string) error) *ConsoleClientMock_DeleteService_Call { + _c.Call.Return(run) + return _c +} + +// GetCluster provides a mock function with given fields: id +func (_m *ConsoleClientMock) GetCluster(id *string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for GetCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string) (*gqlclient.ClusterFragment, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(*string) *gqlclient.ClusterFragment); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetCluster_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCluster' +type ConsoleClientMock_GetCluster_Call struct { + *mock.Call +} + +// GetCluster is a helper method to define mock.On call +// - id *string +func (_e *ConsoleClientMock_Expecter) GetCluster(id interface{}) *ConsoleClientMock_GetCluster_Call { + return &ConsoleClientMock_GetCluster_Call{Call: _e.mock.On("GetCluster", id)} +} + +func (_c *ConsoleClientMock_GetCluster_Call) Run(run func(id *string)) *ConsoleClientMock_GetCluster_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*string)) + }) + return _c +} + +func (_c *ConsoleClientMock_GetCluster_Call) Return(_a0 *gqlclient.ClusterFragment, _a1 error) *ConsoleClientMock_GetCluster_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetCluster_Call) RunAndReturn(run func(*string) (*gqlclient.ClusterFragment, error)) *ConsoleClientMock_GetCluster_Call { + _c.Call.Return(run) + return _c +} + +// GetClusterByHandle provides a mock function with given fields: handle +func (_m *ConsoleClientMock) GetClusterByHandle(handle *string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(handle) + + if len(ret) == 0 { + panic("no return value specified for GetClusterByHandle") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string) (*gqlclient.ClusterFragment, error)); ok { + return rf(handle) + } + if rf, ok := ret.Get(0).(func(*string) *gqlclient.ClusterFragment); ok { + r0 = rf(handle) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string) error); ok { + r1 = rf(handle) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetClusterByHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClusterByHandle' +type ConsoleClientMock_GetClusterByHandle_Call struct { + *mock.Call +} + +// GetClusterByHandle is a helper method to define mock.On call +// - handle *string +func (_e *ConsoleClientMock_Expecter) GetClusterByHandle(handle interface{}) *ConsoleClientMock_GetClusterByHandle_Call { + return &ConsoleClientMock_GetClusterByHandle_Call{Call: _e.mock.On("GetClusterByHandle", handle)} +} + +func (_c *ConsoleClientMock_GetClusterByHandle_Call) Run(run func(handle *string)) *ConsoleClientMock_GetClusterByHandle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*string)) + }) + return _c +} + +func (_c *ConsoleClientMock_GetClusterByHandle_Call) Return(_a0 *gqlclient.ClusterFragment, _a1 error) *ConsoleClientMock_GetClusterByHandle_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetClusterByHandle_Call) RunAndReturn(run func(*string) (*gqlclient.ClusterFragment, error)) *ConsoleClientMock_GetClusterByHandle_Call { + _c.Call.Return(run) + return _c +} + +// GetProvider provides a mock function with given fields: ctx, id, options +func (_m *ConsoleClientMock) GetProvider(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetProvider") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, id, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, id, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, id, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetProvider_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProvider' +type ConsoleClientMock_GetProvider_Call struct { + *mock.Call +} + +// GetProvider is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - options ...gqlgencclient.HTTPRequestOption +func (_e *ConsoleClientMock_Expecter) GetProvider(ctx interface{}, id interface{}, options ...interface{}) *ConsoleClientMock_GetProvider_Call { + return &ConsoleClientMock_GetProvider_Call{Call: _e.mock.On("GetProvider", + append([]interface{}{ctx, id}, options...)...)} +} + +func (_c *ConsoleClientMock_GetProvider_Call) Run(run func(ctx context.Context, id string, options ...gqlgencclient.HTTPRequestOption)) *ConsoleClientMock_GetProvider_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]gqlgencclient.HTTPRequestOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(gqlgencclient.HTTPRequestOption) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *ConsoleClientMock_GetProvider_Call) Return(_a0 *gqlclient.ClusterProviderFragment, _a1 error) *ConsoleClientMock_GetProvider_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetProvider_Call) RunAndReturn(run func(context.Context, string, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)) *ConsoleClientMock_GetProvider_Call { + _c.Call.Return(run) + return _c +} + +// GetProviderByCloud provides a mock function with given fields: ctx, cloud, options +func (_m *ConsoleClientMock) GetProviderByCloud(ctx context.Context, cloud v1alpha1.CloudProvider, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, cloud) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetProviderByCloud") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, cloud, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, cloud, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, cloud, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetProviderByCloud_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProviderByCloud' +type ConsoleClientMock_GetProviderByCloud_Call struct { + *mock.Call +} + +// GetProviderByCloud is a helper method to define mock.On call +// - ctx context.Context +// - cloud v1alpha1.CloudProvider +// - options ...gqlgencclient.HTTPRequestOption +func (_e *ConsoleClientMock_Expecter) GetProviderByCloud(ctx interface{}, cloud interface{}, options ...interface{}) *ConsoleClientMock_GetProviderByCloud_Call { + return &ConsoleClientMock_GetProviderByCloud_Call{Call: _e.mock.On("GetProviderByCloud", + append([]interface{}{ctx, cloud}, options...)...)} +} + +func (_c *ConsoleClientMock_GetProviderByCloud_Call) Run(run func(ctx context.Context, cloud v1alpha1.CloudProvider, options ...gqlgencclient.HTTPRequestOption)) *ConsoleClientMock_GetProviderByCloud_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]gqlgencclient.HTTPRequestOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(gqlgencclient.HTTPRequestOption) + } + } + run(args[0].(context.Context), args[1].(v1alpha1.CloudProvider), variadicArgs...) + }) + return _c +} + +func (_c *ConsoleClientMock_GetProviderByCloud_Call) Return(_a0 *gqlclient.ClusterProviderFragment, _a1 error) *ConsoleClientMock_GetProviderByCloud_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetProviderByCloud_Call) RunAndReturn(run func(context.Context, v1alpha1.CloudProvider, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)) *ConsoleClientMock_GetProviderByCloud_Call { + _c.Call.Return(run) + return _c +} + +// GetRepository provides a mock function with given fields: url +func (_m *ConsoleClientMock) GetRepository(url *string) (*gqlclient.GetGitRepository, error) { + ret := _m.Called(url) + + if len(ret) == 0 { + panic("no return value specified for GetRepository") + } + + var r0 *gqlclient.GetGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(*string) (*gqlclient.GetGitRepository, error)); ok { + return rf(url) + } + if rf, ok := ret.Get(0).(func(*string) *gqlclient.GetGitRepository); ok { + r0 = rf(url) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.GetGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(*string) error); ok { + r1 = rf(url) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRepository' +type ConsoleClientMock_GetRepository_Call struct { + *mock.Call +} + +// GetRepository is a helper method to define mock.On call +// - url *string +func (_e *ConsoleClientMock_Expecter) GetRepository(url interface{}) *ConsoleClientMock_GetRepository_Call { + return &ConsoleClientMock_GetRepository_Call{Call: _e.mock.On("GetRepository", url)} +} + +func (_c *ConsoleClientMock_GetRepository_Call) Run(run func(url *string)) *ConsoleClientMock_GetRepository_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*string)) + }) + return _c +} + +func (_c *ConsoleClientMock_GetRepository_Call) Return(_a0 *gqlclient.GetGitRepository, _a1 error) *ConsoleClientMock_GetRepository_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetRepository_Call) RunAndReturn(run func(*string) (*gqlclient.GetGitRepository, error)) *ConsoleClientMock_GetRepository_Call { + _c.Call.Return(run) + return _c +} + +// GetService provides a mock function with given fields: clusterID, serviceName +func (_m *ConsoleClientMock) GetService(clusterID string, serviceName string) (*gqlclient.ServiceDeploymentExtended, error) { + ret := _m.Called(clusterID, serviceName) + + if len(ret) == 0 { + panic("no return value specified for GetService") + } + + var r0 *gqlclient.ServiceDeploymentExtended + var r1 error + if rf, ok := ret.Get(0).(func(string, string) (*gqlclient.ServiceDeploymentExtended, error)); ok { + return rf(clusterID, serviceName) + } + if rf, ok := ret.Get(0).(func(string, string) *gqlclient.ServiceDeploymentExtended); ok { + r0 = rf(clusterID, serviceName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentExtended) + } + } + + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(clusterID, serviceName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetService' +type ConsoleClientMock_GetService_Call struct { + *mock.Call +} + +// GetService is a helper method to define mock.On call +// - clusterID string +// - serviceName string +func (_e *ConsoleClientMock_Expecter) GetService(clusterID interface{}, serviceName interface{}) *ConsoleClientMock_GetService_Call { + return &ConsoleClientMock_GetService_Call{Call: _e.mock.On("GetService", clusterID, serviceName)} +} + +func (_c *ConsoleClientMock_GetService_Call) Run(run func(clusterID string, serviceName string)) *ConsoleClientMock_GetService_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *ConsoleClientMock_GetService_Call) Return(_a0 *gqlclient.ServiceDeploymentExtended, _a1 error) *ConsoleClientMock_GetService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetService_Call) RunAndReturn(run func(string, string) (*gqlclient.ServiceDeploymentExtended, error)) *ConsoleClientMock_GetService_Call { + _c.Call.Return(run) + return _c +} + +// GetServices provides a mock function with given fields: +func (_m *ConsoleClientMock) GetServices() ([]*gqlclient.ServiceDeploymentBaseFragment, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetServices") + } + + var r0 []*gqlclient.ServiceDeploymentBaseFragment + var r1 error + if rf, ok := ret.Get(0).(func() ([]*gqlclient.ServiceDeploymentBaseFragment, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []*gqlclient.ServiceDeploymentBaseFragment); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gqlclient.ServiceDeploymentBaseFragment) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' +type ConsoleClientMock_GetServices_Call struct { + *mock.Call +} + +// GetServices is a helper method to define mock.On call +func (_e *ConsoleClientMock_Expecter) GetServices() *ConsoleClientMock_GetServices_Call { + return &ConsoleClientMock_GetServices_Call{Call: _e.mock.On("GetServices")} +} + +func (_c *ConsoleClientMock_GetServices_Call) Run(run func()) *ConsoleClientMock_GetServices_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ConsoleClientMock_GetServices_Call) Return(_a0 []*gqlclient.ServiceDeploymentBaseFragment, _a1 error) *ConsoleClientMock_GetServices_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_GetServices_Call) RunAndReturn(run func() ([]*gqlclient.ServiceDeploymentBaseFragment, error)) *ConsoleClientMock_GetServices_Call { + _c.Call.Return(run) + return _c +} + +// IsClusterDeleting provides a mock function with given fields: id +func (_m *ConsoleClientMock) IsClusterDeleting(id *string) bool { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for IsClusterDeleting") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(*string) bool); ok { + r0 = rf(id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ConsoleClientMock_IsClusterDeleting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsClusterDeleting' +type ConsoleClientMock_IsClusterDeleting_Call struct { + *mock.Call +} + +// IsClusterDeleting is a helper method to define mock.On call +// - id *string +func (_e *ConsoleClientMock_Expecter) IsClusterDeleting(id interface{}) *ConsoleClientMock_IsClusterDeleting_Call { + return &ConsoleClientMock_IsClusterDeleting_Call{Call: _e.mock.On("IsClusterDeleting", id)} +} + +func (_c *ConsoleClientMock_IsClusterDeleting_Call) Run(run func(id *string)) *ConsoleClientMock_IsClusterDeleting_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*string)) + }) + return _c +} + +func (_c *ConsoleClientMock_IsClusterDeleting_Call) Return(_a0 bool) *ConsoleClientMock_IsClusterDeleting_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_IsClusterDeleting_Call) RunAndReturn(run func(*string) bool) *ConsoleClientMock_IsClusterDeleting_Call { + _c.Call.Return(run) + return _c +} + +// IsClusterExisting provides a mock function with given fields: id +func (_m *ConsoleClientMock) IsClusterExisting(id *string) bool { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for IsClusterExisting") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(*string) bool); ok { + r0 = rf(id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ConsoleClientMock_IsClusterExisting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsClusterExisting' +type ConsoleClientMock_IsClusterExisting_Call struct { + *mock.Call +} + +// IsClusterExisting is a helper method to define mock.On call +// - id *string +func (_e *ConsoleClientMock_Expecter) IsClusterExisting(id interface{}) *ConsoleClientMock_IsClusterExisting_Call { + return &ConsoleClientMock_IsClusterExisting_Call{Call: _e.mock.On("IsClusterExisting", id)} +} + +func (_c *ConsoleClientMock_IsClusterExisting_Call) Run(run func(id *string)) *ConsoleClientMock_IsClusterExisting_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*string)) + }) + return _c +} + +func (_c *ConsoleClientMock_IsClusterExisting_Call) Return(_a0 bool) *ConsoleClientMock_IsClusterExisting_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_IsClusterExisting_Call) RunAndReturn(run func(*string) bool) *ConsoleClientMock_IsClusterExisting_Call { + _c.Call.Return(run) + return _c +} + +// IsProviderDeleting provides a mock function with given fields: ctx, id +func (_m *ConsoleClientMock) IsProviderDeleting(ctx context.Context, id string) bool { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for IsProviderDeleting") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ConsoleClientMock_IsProviderDeleting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsProviderDeleting' +type ConsoleClientMock_IsProviderDeleting_Call struct { + *mock.Call +} + +// IsProviderDeleting is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *ConsoleClientMock_Expecter) IsProviderDeleting(ctx interface{}, id interface{}) *ConsoleClientMock_IsProviderDeleting_Call { + return &ConsoleClientMock_IsProviderDeleting_Call{Call: _e.mock.On("IsProviderDeleting", ctx, id)} +} + +func (_c *ConsoleClientMock_IsProviderDeleting_Call) Run(run func(ctx context.Context, id string)) *ConsoleClientMock_IsProviderDeleting_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *ConsoleClientMock_IsProviderDeleting_Call) Return(_a0 bool) *ConsoleClientMock_IsProviderDeleting_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_IsProviderDeleting_Call) RunAndReturn(run func(context.Context, string) bool) *ConsoleClientMock_IsProviderDeleting_Call { + _c.Call.Return(run) + return _c +} + +// IsProviderExists provides a mock function with given fields: ctx, id +func (_m *ConsoleClientMock) IsProviderExists(ctx context.Context, id string) bool { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for IsProviderExists") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ConsoleClientMock_IsProviderExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsProviderExists' +type ConsoleClientMock_IsProviderExists_Call struct { + *mock.Call +} + +// IsProviderExists is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *ConsoleClientMock_Expecter) IsProviderExists(ctx interface{}, id interface{}) *ConsoleClientMock_IsProviderExists_Call { + return &ConsoleClientMock_IsProviderExists_Call{Call: _e.mock.On("IsProviderExists", ctx, id)} +} + +func (_c *ConsoleClientMock_IsProviderExists_Call) Run(run func(ctx context.Context, id string)) *ConsoleClientMock_IsProviderExists_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *ConsoleClientMock_IsProviderExists_Call) Return(_a0 bool) *ConsoleClientMock_IsProviderExists_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_IsProviderExists_Call) RunAndReturn(run func(context.Context, string) bool) *ConsoleClientMock_IsProviderExists_Call { + _c.Call.Return(run) + return _c +} + +// ListClusters provides a mock function with given fields: +func (_m *ConsoleClientMock) ListClusters() (*gqlclient.ListClusters, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListClusters") + } + + var r0 *gqlclient.ListClusters + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListClusters, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListClusters); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListClusters) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_ListClusters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListClusters' +type ConsoleClientMock_ListClusters_Call struct { + *mock.Call +} + +// ListClusters is a helper method to define mock.On call +func (_e *ConsoleClientMock_Expecter) ListClusters() *ConsoleClientMock_ListClusters_Call { + return &ConsoleClientMock_ListClusters_Call{Call: _e.mock.On("ListClusters")} +} + +func (_c *ConsoleClientMock_ListClusters_Call) Run(run func()) *ConsoleClientMock_ListClusters_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ConsoleClientMock_ListClusters_Call) Return(_a0 *gqlclient.ListClusters, _a1 error) *ConsoleClientMock_ListClusters_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_ListClusters_Call) RunAndReturn(run func() (*gqlclient.ListClusters, error)) *ConsoleClientMock_ListClusters_Call { + _c.Call.Return(run) + return _c +} + +// ListRepositories provides a mock function with given fields: +func (_m *ConsoleClientMock) ListRepositories() (*gqlclient.ListGitRepositories, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListRepositories") + } + + var r0 *gqlclient.ListGitRepositories + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListGitRepositories, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListGitRepositories); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListGitRepositories) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_ListRepositories_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRepositories' +type ConsoleClientMock_ListRepositories_Call struct { + *mock.Call +} + +// ListRepositories is a helper method to define mock.On call +func (_e *ConsoleClientMock_Expecter) ListRepositories() *ConsoleClientMock_ListRepositories_Call { + return &ConsoleClientMock_ListRepositories_Call{Call: _e.mock.On("ListRepositories")} +} + +func (_c *ConsoleClientMock_ListRepositories_Call) Run(run func()) *ConsoleClientMock_ListRepositories_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ConsoleClientMock_ListRepositories_Call) Return(_a0 *gqlclient.ListGitRepositories, _a1 error) *ConsoleClientMock_ListRepositories_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_ListRepositories_Call) RunAndReturn(run func() (*gqlclient.ListGitRepositories, error)) *ConsoleClientMock_ListRepositories_Call { + _c.Call.Return(run) + return _c +} + +// UpdateCluster provides a mock function with given fields: id, attrs +func (_m *ConsoleClientMock) UpdateCluster(id string, attrs gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(id, attrs) + + if len(ret) == 0 { + panic("no return value specified for UpdateCluster") + } + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error)); ok { + return rf(id, attrs) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) *gqlclient.ClusterFragment); ok { + r0 = rf(id, attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.ClusterUpdateAttributes) error); ok { + r1 = rf(id, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_UpdateCluster_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCluster' +type ConsoleClientMock_UpdateCluster_Call struct { + *mock.Call +} + +// UpdateCluster is a helper method to define mock.On call +// - id string +// - attrs gqlclient.ClusterUpdateAttributes +func (_e *ConsoleClientMock_Expecter) UpdateCluster(id interface{}, attrs interface{}) *ConsoleClientMock_UpdateCluster_Call { + return &ConsoleClientMock_UpdateCluster_Call{Call: _e.mock.On("UpdateCluster", id, attrs)} +} + +func (_c *ConsoleClientMock_UpdateCluster_Call) Run(run func(id string, attrs gqlclient.ClusterUpdateAttributes)) *ConsoleClientMock_UpdateCluster_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(gqlclient.ClusterUpdateAttributes)) + }) + return _c +} + +func (_c *ConsoleClientMock_UpdateCluster_Call) Return(_a0 *gqlclient.ClusterFragment, _a1 error) *ConsoleClientMock_UpdateCluster_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_UpdateCluster_Call) RunAndReturn(run func(string, gqlclient.ClusterUpdateAttributes) (*gqlclient.ClusterFragment, error)) *ConsoleClientMock_UpdateCluster_Call { + _c.Call.Return(run) + return _c +} + +// UpdateComponents provides a mock function with given fields: id, components, errs +func (_m *ConsoleClientMock) UpdateComponents(id string, components []*gqlclient.ComponentAttributes, errs []*gqlclient.ServiceErrorAttributes) error { + ret := _m.Called(id, components, errs) + + if len(ret) == 0 { + panic("no return value specified for UpdateComponents") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, []*gqlclient.ComponentAttributes, []*gqlclient.ServiceErrorAttributes) error); ok { + r0 = rf(id, components, errs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ConsoleClientMock_UpdateComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateComponents' +type ConsoleClientMock_UpdateComponents_Call struct { + *mock.Call +} + +// UpdateComponents is a helper method to define mock.On call +// - id string +// - components []*gqlclient.ComponentAttributes +// - errs []*gqlclient.ServiceErrorAttributes +func (_e *ConsoleClientMock_Expecter) UpdateComponents(id interface{}, components interface{}, errs interface{}) *ConsoleClientMock_UpdateComponents_Call { + return &ConsoleClientMock_UpdateComponents_Call{Call: _e.mock.On("UpdateComponents", id, components, errs)} +} + +func (_c *ConsoleClientMock_UpdateComponents_Call) Run(run func(id string, components []*gqlclient.ComponentAttributes, errs []*gqlclient.ServiceErrorAttributes)) *ConsoleClientMock_UpdateComponents_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]*gqlclient.ComponentAttributes), args[2].([]*gqlclient.ServiceErrorAttributes)) + }) + return _c +} + +func (_c *ConsoleClientMock_UpdateComponents_Call) Return(_a0 error) *ConsoleClientMock_UpdateComponents_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_UpdateComponents_Call) RunAndReturn(run func(string, []*gqlclient.ComponentAttributes, []*gqlclient.ServiceErrorAttributes) error) *ConsoleClientMock_UpdateComponents_Call { + _c.Call.Return(run) + return _c +} + +// UpdateProvider provides a mock function with given fields: ctx, id, attributes, options +func (_m *ConsoleClientMock) UpdateProvider(ctx context.Context, id string, attributes gqlclient.ClusterProviderUpdateAttributes, options ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id, attributes) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for UpdateProvider") + } + + var r0 *gqlclient.ClusterProviderFragment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)); ok { + return rf(ctx, id, attributes, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) *gqlclient.ClusterProviderFragment); ok { + r0 = rf(ctx, id, attributes, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterProviderFragment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) error); ok { + r1 = rf(ctx, id, attributes, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_UpdateProvider_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProvider' +type ConsoleClientMock_UpdateProvider_Call struct { + *mock.Call +} + +// UpdateProvider is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - attributes gqlclient.ClusterProviderUpdateAttributes +// - options ...gqlgencclient.HTTPRequestOption +func (_e *ConsoleClientMock_Expecter) UpdateProvider(ctx interface{}, id interface{}, attributes interface{}, options ...interface{}) *ConsoleClientMock_UpdateProvider_Call { + return &ConsoleClientMock_UpdateProvider_Call{Call: _e.mock.On("UpdateProvider", + append([]interface{}{ctx, id, attributes}, options...)...)} +} + +func (_c *ConsoleClientMock_UpdateProvider_Call) Run(run func(ctx context.Context, id string, attributes gqlclient.ClusterProviderUpdateAttributes, options ...gqlgencclient.HTTPRequestOption)) *ConsoleClientMock_UpdateProvider_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]gqlgencclient.HTTPRequestOption, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(gqlgencclient.HTTPRequestOption) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(gqlclient.ClusterProviderUpdateAttributes), variadicArgs...) + }) + return _c +} + +func (_c *ConsoleClientMock_UpdateProvider_Call) Return(_a0 *gqlclient.ClusterProviderFragment, _a1 error) *ConsoleClientMock_UpdateProvider_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_UpdateProvider_Call) RunAndReturn(run func(context.Context, string, gqlclient.ClusterProviderUpdateAttributes, ...gqlgencclient.HTTPRequestOption) (*gqlclient.ClusterProviderFragment, error)) *ConsoleClientMock_UpdateProvider_Call { + _c.Call.Return(run) + return _c +} + +// UpdateRepository provides a mock function with given fields: id, attrs +func (_m *ConsoleClientMock) UpdateRepository(id string, attrs gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error) { + ret := _m.Called(id, attrs) + + if len(ret) == 0 { + panic("no return value specified for UpdateRepository") + } + + var r0 *gqlclient.UpdateGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error)); ok { + return rf(id, attrs) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) *gqlclient.UpdateGitRepository); ok { + r0 = rf(id, attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.UpdateGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.GitAttributes) error); ok { + r1 = rf(id, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsoleClientMock_UpdateRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRepository' +type ConsoleClientMock_UpdateRepository_Call struct { + *mock.Call +} + +// UpdateRepository is a helper method to define mock.On call +// - id string +// - attrs gqlclient.GitAttributes +func (_e *ConsoleClientMock_Expecter) UpdateRepository(id interface{}, attrs interface{}) *ConsoleClientMock_UpdateRepository_Call { + return &ConsoleClientMock_UpdateRepository_Call{Call: _e.mock.On("UpdateRepository", id, attrs)} +} + +func (_c *ConsoleClientMock_UpdateRepository_Call) Run(run func(id string, attrs gqlclient.GitAttributes)) *ConsoleClientMock_UpdateRepository_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(gqlclient.GitAttributes)) + }) + return _c +} + +func (_c *ConsoleClientMock_UpdateRepository_Call) Return(_a0 *gqlclient.UpdateGitRepository, _a1 error) *ConsoleClientMock_UpdateRepository_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConsoleClientMock_UpdateRepository_Call) RunAndReturn(run func(string, gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error)) *ConsoleClientMock_UpdateRepository_Call { + _c.Call.Return(run) + return _c +} + +// UpdateService provides a mock function with given fields: serviceId, attributes +func (_m *ConsoleClientMock) UpdateService(serviceId string, attributes gqlclient.ServiceUpdateAttributes) error { + ret := _m.Called(serviceId, attributes) + + if len(ret) == 0 { + panic("no return value specified for UpdateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, gqlclient.ServiceUpdateAttributes) error); ok { + r0 = rf(serviceId, attributes) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ConsoleClientMock_UpdateService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateService' +type ConsoleClientMock_UpdateService_Call struct { + *mock.Call +} + +// UpdateService is a helper method to define mock.On call +// - serviceId string +// - attributes gqlclient.ServiceUpdateAttributes +func (_e *ConsoleClientMock_Expecter) UpdateService(serviceId interface{}, attributes interface{}) *ConsoleClientMock_UpdateService_Call { + return &ConsoleClientMock_UpdateService_Call{Call: _e.mock.On("UpdateService", serviceId, attributes)} +} + +func (_c *ConsoleClientMock_UpdateService_Call) Run(run func(serviceId string, attributes gqlclient.ServiceUpdateAttributes)) *ConsoleClientMock_UpdateService_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(gqlclient.ServiceUpdateAttributes)) + }) + return _c +} + +func (_c *ConsoleClientMock_UpdateService_Call) Return(_a0 error) *ConsoleClientMock_UpdateService_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConsoleClientMock_UpdateService_Call) RunAndReturn(run func(string, gqlclient.ServiceUpdateAttributes) error) *ConsoleClientMock_UpdateService_Call { + _c.Call.Return(run) + return _c +} + +// NewConsoleClientMock creates a new instance of ConsoleClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewConsoleClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *ConsoleClientMock { + mock := &ConsoleClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/controller/internal/test/mocks/testing_mock.go b/controller/internal/test/mocks/testing_mock.go new file mode 100644 index 000000000..844195705 --- /dev/null +++ b/controller/internal/test/mocks/testing_mock.go @@ -0,0 +1,35 @@ +package mocks + +import ( + "fmt" +) + +var TestingT = &MockTestingT{} + +// MockTestingT mocks a test struct +type MockTestingT struct{} + +const mockTestingTFailNowCalled = "FailNow was called" + +func (m *MockTestingT) Logf(msg string, opts ...interface{}) { + fmt.Printf(msg, opts...) +} + +func (m *MockTestingT) Errorf(msg string, opts ...interface{}) { + fmt.Printf(msg, opts...) +} + +// FailNow mocks the FailNow call. +// It panics in order to mimic the FailNow behavior in the sense that +// the execution stops. +// When expecting this method, the call that invokes it should use the following code: +// +// assert.PanicsWithValue(t, mockTestingTFailNowCalled, func() {...}) +func (m *MockTestingT) FailNow() { + // this function should panic now to stop the execution as expected + panic(mockTestingTFailNowCalled) +} + +func (m *MockTestingT) Cleanup(f func()) { + f() +} diff --git a/controller/tools.go b/controller/tools.go index b28dc825b..53b50ad94 100644 --- a/controller/tools.go +++ b/controller/tools.go @@ -4,6 +4,7 @@ package main import ( _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "github.com/vektra/mockery/v2" _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" _ "sigs.k8s.io/controller-tools/cmd/controller-gen" _ "sigs.k8s.io/kustomize/kustomize/v5" From a032686f720d6ad36a4e0c1ca0270fcfcdd19f89 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 16:44:40 +0100 Subject: [PATCH 161/198] finish makefile --- controller/Makefile | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index af05a50f0..12acf90ae 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -30,6 +30,9 @@ ifeq (,$(findstring $(GOPATH)/bin,$(PATH))) $(error $$GOPATH/bin directory is not in your $$PATH) endif +.PHONY: all +all: build + ##@ General .PHONY: help @@ -59,12 +62,32 @@ run: manifests generate fmt vet ## run a controller from your host release: manifests generate fmt vet ## builds release version of the app. Requires GoReleaser to work. goreleaser build --clean --single-target --snapshot -docker-build: ## build Docker image with the driver +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +docker-build: ## build Docker image with the manager docker build --no-cache -t ${IMG} . -docker-push: ## push Docker image with the driver +docker-push: ## push docker image with the manager docker push ${IMG} +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - 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 + ##@ Codegen .PHONY: manifests From 0895654df895c590e624c0936fb2b69a6661408d Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Mon, 18 Dec 2023 16:47:34 +0100 Subject: [PATCH 162/198] go mod tidy --- controller/go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/controller/go.mod b/controller/go.mod index 75c66394a..15f2ffa46 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -72,7 +72,6 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.1.1 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/fatih/structtag v1.2.0 // indirect From 3c5cf1da3eb7ec5adb99326846476c33e3c6942f Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Mon, 18 Dec 2023 16:52:40 +0100 Subject: [PATCH 163/198] fix cluster test --- .../cluster_controller_ginkgo_test.go | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/controller/internal/controller/cluster_controller_ginkgo_test.go b/controller/internal/controller/cluster_controller_ginkgo_test.go index 854f82b97..d82b447c6 100644 --- a/controller/internal/controller/cluster_controller_ginkgo_test.go +++ b/controller/internal/controller/cluster_controller_ginkgo_test.go @@ -20,13 +20,21 @@ import ( "github.com/pluralsh/console/controller/internal/test/mocks" ) +func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + var _ = Describe("Cluster Controller", func() { Context("When reconciling a resource", func() { const ( clusterName = "test-cluster" clusterConsoleID = "12345-67890" providerName = "test-provider" - providerNamespace = "test-provider" providerConsoleID = "12345-67890" ) @@ -53,26 +61,27 @@ var _ = Describe("Cluster Controller", func() { Handle: lo.ToPtr(clusterName), Version: lo.ToPtr("1.24"), Cloud: "aws", - ProviderRef: &corev1.ObjectReference{Name: providerName, Namespace: providerNamespace}, + ProviderRef: &corev1.ObjectReference{Name: providerName}, }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } By("creating the custom resource for the Kind Provider") - err = k8sClient.Get(ctx, typeNamespacedName, provider) + err = k8sClient.Get(ctx, types.NamespacedName{Name: providerName}, provider) if err != nil && errors.IsNotFound(err) { resource := &v1alpha1.Provider{ ObjectMeta: metav1.ObjectMeta{Name: providerName}, Spec: v1alpha1.ProviderSpec{ - Cloud: "aws", - Name: providerName, - Namespace: providerNamespace, + Cloud: "aws", + Name: providerName, }, Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Status().Update(ctx, resource)).To(Succeed()) + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: providerName}, provider)).To(Succeed()) + provider.Status = v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)} + Expect(k8sClient.Status().Update(ctx, provider)).To(Succeed()) } }) @@ -138,9 +147,7 @@ var _ = Describe("Cluster Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, cluster) Expect(err).NotTo(HaveOccurred()) - Expect(cluster.Status).To(Equal(test.expectedStatus)) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(test.expectedStatus))) }) }) }) From b4c1d9f0c6a83f5c79f3733e85120ec76b02b81d Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 19 Dec 2023 10:13:27 +0100 Subject: [PATCH 164/198] update rbac config for provider controller --- controller/Makefile | 2 +- .../api/v1alpha1/zz_generated.deepcopy.go | 2 +- controller/config/rbac/role.yaml | 23 ++++---- .../cluster_controller_ginkgo_test.go | 57 +++++++------------ .../controller/provider_controller.go | 13 +++-- controller/internal/test/utils/kubernetes.go | 33 +++++++++++ 6 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 controller/internal/test/utils/kubernetes.go diff --git a/controller/Makefile b/controller/Makefile index 12acf90ae..304daa6db 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -123,7 +123,7 @@ fix: golangci-lint ## fix issues found by linters .PHONY: test test: manifests generate genmock fmt vet envtest ## run tests - @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(GOPATH)/bin -p path)" go test $$(go list ./... | grep -v /e2e) -v + @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(GOPATH)/bin -p path)" go test $$(go list ./... | grep -v /e2e) .PHONY: e2e e2e: ## run e2e tests diff --git a/controller/api/v1alpha1/zz_generated.deepcopy.go b/controller/api/v1alpha1/zz_generated.deepcopy.go index 244afba83..55ab21d71 100644 --- a/controller/api/v1alpha1/zz_generated.deepcopy.go +++ b/controller/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2023 The Kubernetes authors. +Copyright 2023. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/controller/config/rbac/role.yaml b/controller/config/rbac/role.yaml index f4d0446ca..09078d1bd 100644 --- a/controller/config/rbac/role.yaml +++ b/controller/config/rbac/role.yaml @@ -1,18 +1,15 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: manager-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize name: manager-role rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] +- apiGroups: + - "" + resources: + - secrets + verbs: + - get - apiGroups: - deployments.plural.sh resources: @@ -71,6 +68,12 @@ rules: - patch - update - watch +- apiGroups: + - deployments.plural.sh + resources: + - providers/finalizers + verbs: + - update - apiGroups: - deployments.plural.sh resources: diff --git a/controller/internal/controller/cluster_controller_ginkgo_test.go b/controller/internal/controller/cluster_controller_ginkgo_test.go index d82b447c6..2052df957 100644 --- a/controller/internal/controller/cluster_controller_ginkgo_test.go +++ b/controller/internal/controller/cluster_controller_ginkgo_test.go @@ -18,6 +18,7 @@ import ( "github.com/pluralsh/console/controller/api/v1alpha1" "github.com/pluralsh/console/controller/internal/test/mocks" + "github.com/pluralsh/console/controller/internal/test/utils" ) func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { @@ -39,50 +40,36 @@ var _ = Describe("Cluster Controller", func() { ) ctx := context.Background() - typeNamespacedName := types.NamespacedName{ Name: clusterName, Namespace: "default", } - cluster := &v1alpha1.Cluster{} - provider := &v1alpha1.Provider{} - BeforeEach(func() { By("creating the custom resource for the Kind Cluster") - err := k8sClient.Get(ctx, typeNamespacedName, cluster) - if err != nil && errors.IsNotFound(err) { - resource := &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - Namespace: "default", - }, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), - Version: lo.ToPtr("1.24"), - Cloud: "aws", - ProviderRef: &corev1.ObjectReference{Name: providerName}, - }, - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - } + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(clusterName), + Version: lo.ToPtr("1.24"), + Cloud: "aws", + ProviderRef: &corev1.ObjectReference{Name: providerName}, + }, + }, nil)).To(Succeed()) By("creating the custom resource for the Kind Provider") - err = k8sClient.Get(ctx, types.NamespacedName{Name: providerName}, provider) - if err != nil && errors.IsNotFound(err) { - resource := &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{Name: providerName}, - Spec: v1alpha1.ProviderSpec{ - Cloud: "aws", - Name: providerName, - }, - Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: providerName}, provider)).To(Succeed()) - provider.Status = v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)} - Expect(k8sClient.Status().Update(ctx, provider)).To(Succeed()) - } + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{Name: providerName}, + Spec: v1alpha1.ProviderSpec{ + Cloud: "aws", + Name: providerName, + }, + }, func(p *v1alpha1.Provider) { + p.Status.ID = lo.ToPtr(providerConsoleID) + })).To(Succeed()) }) AfterEach(func() { diff --git a/controller/internal/controller/provider_controller.go b/controller/internal/controller/provider_controller.go index 525266e67..b11d03211 100644 --- a/controller/internal/controller/provider_controller.go +++ b/controller/internal/controller/provider_controller.go @@ -35,11 +35,14 @@ const ( ProviderProtectionFinalizerName = "providers.deployments.plural.sh/provider-protection" ) -//+kubebuilder:rbac:groups=deployments.plural.sh,resources=providers,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/status,verbs=get;update;patch - -// Reconcile ... -// TODO: Add kubebuilder rbac annotation +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the v1alpha1.Provider closer to the desired state +// and syncs it with the Console API state. func (r *ProviderReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) { log := log.FromContext(ctx) log.Info("Reconciling") diff --git a/controller/internal/test/utils/kubernetes.go b/controller/internal/test/utils/kubernetes.go new file mode 100644 index 000000000..271f54524 --- /dev/null +++ b/controller/internal/test/utils/kubernetes.go @@ -0,0 +1,33 @@ +package utils + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Patcher[PatchObject client.Object] func(object PatchObject) + +func MaybeCreate[O client.Object](c client.Client, object O, patch Patcher[O]) error { + ctx := context.Background() + original := object.DeepCopyObject().(O) + + err := c.Get(ctx, client.ObjectKey{Name: object.GetName(), Namespace: object.GetNamespace()}, object) + if err != nil && !errors.IsNotFound(err) { + return err + } + + err = c.Create(ctx, object) + if err != nil { + return err + } + + if patch == nil { + return nil + } + + patch(object) + + return c.Status().Patch(ctx, object, client.MergeFrom(original)) +} From 00a034d9a56acaf8c0c93bac60352e6a00022a2c Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 19 Dec 2023 10:15:17 +0100 Subject: [PATCH 165/198] run tests with verbose flag --- controller/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/Makefile b/controller/Makefile index 304daa6db..12acf90ae 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -123,7 +123,7 @@ fix: golangci-lint ## fix issues found by linters .PHONY: test test: manifests generate genmock fmt vet envtest ## run tests - @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(GOPATH)/bin -p path)" go test $$(go list ./... | grep -v /e2e) + @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(GOPATH)/bin -p path)" go test $$(go list ./... | grep -v /e2e) -v .PHONY: e2e e2e: ## run e2e tests From f55177df08cfc7ea3ccb579659b43a4adcb8a7dc Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 11:33:50 +0100 Subject: [PATCH 166/198] rewrite cluster tests --- controller/go.mod | 1 + .../cluster_controller_ginkgo_test.go | 166 ++++++++++------- .../controller/cluster_controller_test.go | 170 ------------------ 3 files changed, 100 insertions(+), 237 deletions(-) diff --git a/controller/go.mod b/controller/go.mod index 15f2ffa46..18437859a 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -260,6 +260,7 @@ require github.com/onsi/ginkgo/v2 v2.13.1 require ( github.com/chigopher/pathlib v0.15.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect diff --git a/controller/internal/controller/cluster_controller_ginkgo_test.go b/controller/internal/controller/cluster_controller_ginkgo_test.go index 2052df957..04b7a8fef 100644 --- a/controller/internal/controller/cluster_controller_ginkgo_test.go +++ b/controller/internal/controller/cluster_controller_ginkgo_test.go @@ -30,93 +30,114 @@ func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus return status } -var _ = Describe("Cluster Controller", func() { - Context("When reconciling a resource", func() { +var _ = Describe("Cluster Controller", Ordered, func() { + Context("When creating a resource", func() { const ( - clusterName = "test-cluster" - clusterConsoleID = "12345-67890" - providerName = "test-provider" + awsProviderName = "aws-test-provider" providerConsoleID = "12345-67890" + awsClusterName = "aws-test-cluster" + byokClusterName = "byok-test-cluster" + clusterConsoleID = "12345-67890" ) ctx := context.Background() - typeNamespacedName := types.NamespacedName{ - Name: clusterName, - Namespace: "default", - } + awsNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} + byokNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} + + BeforeAll(func() { + By("creating AWS provider") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{Name: awsProviderName}, + Spec: v1alpha1.ProviderSpec{ + Cloud: "aws", + Name: awsProviderName, + }, + }, func(p *v1alpha1.Provider) { + p.Status.ID = lo.ToPtr(providerConsoleID) + })).To(Succeed()) - BeforeEach(func() { - By("creating the custom resource for the Kind Cluster") + By("creating AWS cluster") Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, + Name: awsClusterName, Namespace: "default", }, Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(clusterName), + Handle: lo.ToPtr(awsClusterName), Version: lo.ToPtr("1.24"), Cloud: "aws", - ProviderRef: &corev1.ObjectReference{Name: providerName}, + ProviderRef: &corev1.ObjectReference{Name: awsProviderName}, }, }, nil)).To(Succeed()) - By("creating the custom resource for the Kind Provider") - Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{Name: providerName}, - Spec: v1alpha1.ProviderSpec{ - Cloud: "aws", - Name: providerName, + By("creating BYOK cluster") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: byokClusterName, + Namespace: "default", }, - }, func(p *v1alpha1.Provider) { - p.Status.ID = lo.ToPtr(providerConsoleID) - })).To(Succeed()) + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(byokClusterName), + Cloud: "byok", + }, + }, nil)).To(Succeed()) }) - AfterEach(func() { - resource := &v1alpha1.Cluster{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) + AfterAll(func() { + awsCluster := &v1alpha1.Cluster{} + err := k8sClient.Get(ctx, awsNamespacedName, awsCluster) Expect(err).NotTo(HaveOccurred()) - By("Cleanup the specific resource instance Cluster") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Expect(k8sClient.Delete(ctx, awsCluster)).To(Succeed()) + + byokCluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokNamespacedName, byokCluster) + Expect(err).NotTo(HaveOccurred()) + By("cleanup BYOK cluster") + Expect(k8sClient.Delete(ctx, byokCluster)).To(Succeed()) }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") - test := struct { - returnGetClusterByHandle *gqlclient.ClusterFragment - returnErrorGetClusterByHandle error - returnIsClusterExisting bool - returnCreateCluster *gqlclient.ClusterFragment - returnErrorCreateCluster error - expectedStatus v1alpha1.ClusterStatus - }{ - returnGetClusterByHandle: nil, - returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), - returnIsClusterExisting: false, - returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, - expectedStatus: v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, + It("should successfully reconcile AWS cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: clusterConsoleID}, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("CI5QLJIIR62PCOX2PVNBUEUUO2XXJ7SYZNQE2ZNY7N3F4ADISJNA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), }, }, - } + }))) + }) + It("should successfully reconcile BYOK cluster", func() { fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) - fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: clusterConsoleID}, nil) controllerReconciler := &ClusterReconciler{ Client: k8sClient, @@ -124,17 +145,28 @@ var _ = Describe("Cluster Controller", func() { ConsoleClient: fakeConsoleClient, } - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) - + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokNamespacedName}) Expect(err).NotTo(HaveOccurred()) cluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, typeNamespacedName, cluster) - + err = k8sClient.Get(ctx, awsNamespacedName, cluster) Expect(err).NotTo(HaveOccurred()) - Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(test.expectedStatus))) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(clusterConsoleID), + SHA: lo.ToPtr("CI5QLJIIR62PCOX2PVNBUEUUO2XXJ7SYZNQE2ZNY7N3F4ADISJNA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) }) }) }) diff --git a/controller/internal/controller/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go index 9a80725cd..5b15c7a3d 100644 --- a/controller/internal/controller/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -1,175 +1,5 @@ package controller_test -// -//import ( -// "context" -// "encoding/json" -// "testing" -// -// gqlclient "github.com/pluralsh/console-client-go" -// "github.com/samber/lo" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/mock" -// corev1 "k8s.io/api/core/v1" -// "k8s.io/apimachinery/pkg/api/errors" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/runtime/schema" -// "k8s.io/apimachinery/pkg/types" -// utilruntime "k8s.io/apimachinery/pkg/util/runtime" -// "k8s.io/client-go/kubernetes/scheme" -// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" -// "sigs.k8s.io/controller-runtime/pkg/client/fake" -// "sigs.k8s.io/controller-runtime/pkg/reconcile" -// -// "github.com/pluralsh/console/controller/internal/controller" -// -// "github.com/pluralsh/console/controller/api/v1alpha1" -// "github.com/pluralsh/console/controller/internal/test/mocks" -//) -// -//func init() { -// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -//} -// -//func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { -// for i := range status.Conditions { -// status.Conditions[i].LastTransitionTime = metav1.Time{} -// status.Conditions[i].ObservedGeneration = 0 -// } -// -// return status -//} -// -//func TestCreateNewCluster(t *testing.T) { -// const ( -// clusterName = "test-cluster" -// clusterConsoleID = "12345-67890" -// providerName = "test-provider" -// providerNamespace = "test-provider" -// providerConsoleID = "12345-67890" -// ) -// -// tests := []struct { -// name string -// cluster string -// returnGetClusterByHandle *gqlclient.ClusterFragment -// returnErrorGetClusterByHandle error -// returnIsClusterExisting bool -// returnCreateCluster *gqlclient.ClusterFragment -// returnErrorCreateCluster error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ClusterStatus -// }{ -// { -// name: "scenario 1: create a new AWS cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetClusterByHandle: nil, -// returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), -// returnIsClusterExisting: false, -// returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Version: lo.ToPtr("1.24"), -// Cloud: "aws", -// ProviderRef: &corev1.ObjectReference{Name: providerName}, -// }, -// }, -// &v1alpha1.Provider{ -// ObjectMeta: metav1.ObjectMeta{Name: providerName}, -// Spec: v1alpha1.ProviderSpec{ -// Cloud: "aws", -// Name: providerName, -// Namespace: providerNamespace, -// }, -// Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, -// }, -// }, -// }, -// { -// name: "scenario 2: create a new BYOK cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetClusterByHandle: nil, -// returnErrorGetClusterByHandle: errors.NewNotFound(schema.GroupResource{}, clusterName), -// returnIsClusterExisting: false, -// returnCreateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Cloud: "byok", -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) -// fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) -// fakeConsoleClient.On("CreateCluster", mock.Anything).Return(test.returnCreateCluster, test.returnErrorCreateCluster) -// -// ctx := context.Background() -// -// target := &controller.ClusterReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) -// assert.NoError(t, err) -// -// existingCluster := &v1alpha1.Cluster{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) -// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) -// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) -// -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} -// //func TestUpdateCluster(t *testing.T) { // const ( // clusterName = "test-cluster" From 5757f4d29c8029af62e04875ddf596fa1798b9c7 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 12:11:23 +0100 Subject: [PATCH 167/198] rewrite tests for adopting existing clusters --- .../cluster_controller_ginkgo_test.go | 153 ++++++++-- .../controller/cluster_controller_test.go | 289 ++++++++---------- 2 files changed, 270 insertions(+), 172 deletions(-) diff --git a/controller/internal/controller/cluster_controller_ginkgo_test.go b/controller/internal/controller/cluster_controller_ginkgo_test.go index 04b7a8fef..084a68f8e 100644 --- a/controller/internal/controller/cluster_controller_ginkgo_test.go +++ b/controller/internal/controller/cluster_controller_ginkgo_test.go @@ -33,19 +33,26 @@ func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus var _ = Describe("Cluster Controller", Ordered, func() { Context("When creating a resource", func() { const ( - awsProviderName = "aws-test-provider" - providerConsoleID = "12345-67890" - awsClusterName = "aws-test-cluster" - byokClusterName = "byok-test-cluster" - clusterConsoleID = "12345-67890" + awsProviderName = "aws-provider" + awsProviderConsoleID = "aws-provider-console-id" + awsClusterName = "aws-cluster" + awsClusterConsoleID = "aws-cluster-console-id" + byokClusterName = "byok-test-cluster" + byokClusterConsoleID = "byok-cluster-console-id" + awsReadonlyClusterName = "aws-readonly-cluster" + awsReadonlyClusterConsoleID = "aws-readonly-cluster-console-id" + byokReadonlyClusterName = "byok-readonly-cluster" + byokReadonlyClusterConsoleID = "byok-readonly-cluster-console-id" ) ctx := context.Background() awsNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} byokNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} + awsReadonlyNamespacedName := types.NamespacedName{Name: awsReadonlyClusterName, Namespace: "default"} + byokReadonlyNamespacedName := types.NamespacedName{Name: byokReadonlyClusterName, Namespace: "default"} BeforeAll(func() { - By("creating AWS provider") + By("Creating AWS provider") Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ ObjectMeta: metav1.ObjectMeta{Name: awsProviderName}, Spec: v1alpha1.ProviderSpec{ @@ -53,10 +60,10 @@ var _ = Describe("Cluster Controller", Ordered, func() { Name: awsProviderName, }, }, func(p *v1alpha1.Provider) { - p.Status.ID = lo.ToPtr(providerConsoleID) + p.Status.ID = lo.ToPtr(awsProviderConsoleID) })).To(Succeed()) - By("creating AWS cluster") + By("Creating AWS cluster") Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: awsClusterName, @@ -70,7 +77,7 @@ var _ = Describe("Cluster Controller", Ordered, func() { }, }, nil)).To(Succeed()) - By("creating BYOK cluster") + By("Creating BYOK cluster") Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: byokClusterName, @@ -81,27 +88,63 @@ var _ = Describe("Cluster Controller", Ordered, func() { Cloud: "byok", }, }, nil)).To(Succeed()) + + By("Creating AWS cluster that will adopt existing Console resource") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: awsReadonlyClusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(awsReadonlyClusterName), + Cloud: "aws", + }, + }, nil)).To(Succeed()) + + By("Creating BYOK cluster that will adopt existing Console resource") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: byokReadonlyClusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(byokReadonlyClusterName), + Cloud: "byok", + }, + }, nil)).To(Succeed()) }) AfterAll(func() { + By("Cleanup AWS cluster") awsCluster := &v1alpha1.Cluster{} err := k8sClient.Get(ctx, awsNamespacedName, awsCluster) Expect(err).NotTo(HaveOccurred()) - By("Cleanup the specific resource instance Cluster") Expect(k8sClient.Delete(ctx, awsCluster)).To(Succeed()) + By("Cleanup BYOK cluster") byokCluster := &v1alpha1.Cluster{} err = k8sClient.Get(ctx, byokNamespacedName, byokCluster) Expect(err).NotTo(HaveOccurred()) - By("cleanup BYOK cluster") Expect(k8sClient.Delete(ctx, byokCluster)).To(Succeed()) + + By("Cleanup AWS readonly cluster") + awsReadonlyCluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsReadonlyNamespacedName, awsReadonlyCluster) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, awsReadonlyCluster)).To(Succeed()) + + By("Cleanup BYOK readonly cluster") + byokReadonlyCluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokReadonlyNamespacedName, byokReadonlyCluster) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, byokReadonlyCluster)).To(Succeed()) }) It("should successfully reconcile AWS cluster", func() { fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: clusterConsoleID}, nil) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: awsClusterConsoleID}, nil) controllerReconciler := &ClusterReconciler{ Client: k8sClient, @@ -116,8 +159,8 @@ var _ = Describe("Cluster Controller", Ordered, func() { err = k8sClient.Get(ctx, awsNamespacedName, cluster) Expect(err).NotTo(HaveOccurred()) Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("CI5QLJIIR62PCOX2PVNBUEUUO2XXJ7SYZNQE2ZNY7N3F4ADISJNA===="), + ID: lo.ToPtr(awsClusterConsoleID), + SHA: lo.ToPtr("J7CMSICIXLWV7MCWNPBZUA6FEOI3HGTQMNVLYD6VZXX6Y66S6ETQ===="), Conditions: []metav1.Condition{ { Type: v1alpha1.ReadonlyConditionType.String(), @@ -137,7 +180,7 @@ var _ = Describe("Cluster Controller", Ordered, func() { fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: clusterConsoleID}, nil) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: byokClusterConsoleID}, nil) controllerReconciler := &ClusterReconciler{ Client: k8sClient, @@ -152,8 +195,8 @@ var _ = Describe("Cluster Controller", Ordered, func() { err = k8sClient.Get(ctx, awsNamespacedName, cluster) Expect(err).NotTo(HaveOccurred()) Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ - ID: lo.ToPtr(clusterConsoleID), - SHA: lo.ToPtr("CI5QLJIIR62PCOX2PVNBUEUUO2XXJ7SYZNQE2ZNY7N3F4ADISJNA===="), + ID: lo.ToPtr(byokClusterConsoleID), + SHA: lo.ToPtr("J7CMSICIXLWV7MCWNPBZUA6FEOI3HGTQMNVLYD6VZXX6Y66S6ETQ===="), Conditions: []metav1.Condition{ { Type: v1alpha1.ReadonlyConditionType.String(), @@ -168,5 +211,81 @@ var _ = Describe("Cluster Controller", Ordered, func() { }, }))) }) + + It("should successfully reconcile AWS readonly cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(&gqlclient.ClusterFragment{ + ID: awsReadonlyClusterConsoleID, + CurrentVersion: lo.ToPtr("1.24.11"), + }, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsReadonlyNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsReadonlyNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(awsReadonlyClusterConsoleID), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + CurrentVersion: lo.ToPtr("1.24.11"), + }))) + }) + + It("should successfully reconcile BYOK readonly cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(&gqlclient.ClusterFragment{ + ID: byokReadonlyClusterConsoleID, + CurrentVersion: lo.ToPtr("1.24.11"), + }, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokReadonlyNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokReadonlyNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(byokReadonlyClusterConsoleID), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + CurrentVersion: lo.ToPtr("1.24.11"), + }))) + }) }) }) diff --git a/controller/internal/controller/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go index 5b15c7a3d..3cf378e6e 100644 --- a/controller/internal/controller/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -1,161 +1,160 @@ package controller_test -//func TestUpdateCluster(t *testing.T) { -// const ( -// clusterName = "test-cluster" -// clusterConsoleID = "12345-67890" -// providerName = "test-provider" -// providerNamespace = "test-provider" -// providerConsoleID = "12345-67890" -// ) -// -// tests := []struct { -// name string -// cluster string -// returnIsClusterExisting bool -// returnUpdateCluster *gqlclient.ClusterFragment -// returnErrorUpdateCluster error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ClusterStatus -// }{ -// { -// name: "scenario 1: update AWS cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), +// func TestUpdateCluster(t *testing.T) { +// const ( +// clusterName = "test-cluster" +// clusterConsoleID = "12345-67890" +// providerName = "test-provider" +// providerNamespace = "test-provider" +// providerConsoleID = "12345-67890" +// ) +// +// tests := []struct { +// name string +// cluster string +// returnIsClusterExisting bool +// returnUpdateCluster *gqlclient.ClusterFragment +// returnErrorUpdateCluster error +// existingObjects []ctrlruntimeclient.Object +// expectedStatus v1alpha1.ClusterStatus +// }{ +// { +// name: "scenario 1: update AWS cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, // }, // }, -// }, -// returnIsClusterExisting: true, -// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Version: lo.ToPtr("1.24"), -// Cloud: "aws", -// ProviderRef: &corev1.ObjectReference{Name: providerName}, -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), +// returnIsClusterExisting: true, +// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Version: lo.ToPtr("1.24"), +// Cloud: "aws", +// ProviderRef: &corev1.ObjectReference{Name: providerName}, +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, // }, // }, // }, -// }, -// &v1alpha1.Provider{ -// ObjectMeta: metav1.ObjectMeta{Name: providerName}, -// Spec: v1alpha1.ProviderSpec{ -// Cloud: "aws", -// Name: providerName, -// Namespace: providerNamespace, +// &v1alpha1.Provider{ +// ObjectMeta: metav1.ObjectMeta{Name: providerName}, +// Spec: v1alpha1.ProviderSpec{ +// Cloud: "aws", +// Name: providerName, +// Namespace: providerNamespace, +// }, +// Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, // }, -// Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, // }, // }, -// }, -// { -// name: "scenario 2: update BYOK cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), +// { +// name: "scenario 2: update BYOK cluster", +// cluster: clusterName, +// expectedStatus: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, // }, // }, -// }, -// returnIsClusterExisting: true, -// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Cloud: "byok", -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), +// returnIsClusterExisting: true, +// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, +// existingObjects: []ctrlruntimeclient.Object{ +// &v1alpha1.Cluster{ +// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, +// Spec: v1alpha1.ClusterSpec{ +// Handle: lo.ToPtr(clusterName), +// Cloud: "byok", +// }, +// Status: v1alpha1.ClusterStatus{ +// ID: lo.ToPtr(clusterConsoleID), +// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), +// Conditions: []metav1.Condition{ +// { +// Type: v1alpha1.ReadonlyConditionType.String(), +// Status: metav1.ConditionFalse, +// Reason: v1alpha1.ReadonlyConditionReason.String(), +// }, +// { +// Type: v1alpha1.ReadyConditionType.String(), +// Status: metav1.ConditionTrue, +// Reason: v1alpha1.ReadyConditionReason.String(), +// }, // }, // }, // }, // }, // }, -// }, -// } +// } // -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() // -// fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) -// fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return(test.returnUpdateCluster, test.returnErrorUpdateCluster) +// fakeConsoleClient := mocks.NewConsoleClient(t) +// fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) +// fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return(test.returnUpdateCluster, test.returnErrorUpdateCluster) // -// ctx := context.Background() +// ctx := context.Background() // -// target := &controller.ClusterReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } +// target := &controller.ClusterReconciler{ +// Client: fakeClient, +// Scheme: scheme.Scheme, +// ConsoleClient: fakeConsoleClient, +// } // -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) -// assert.NoError(t, err) +// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) +// assert.NoError(t, err) // -// existingCluster := &v1alpha1.Cluster{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) +// existingCluster := &v1alpha1.Cluster{} +// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) // -// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) -// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) +// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) +// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) // -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) +// assert.NoError(t, err) +// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) +// }) +// } // } -//} -// //func TestAdoptExistingCluster(t *testing.T) { // const ( // clusterName = "test-cluster" @@ -171,24 +170,8 @@ package controller_test // expectedStatus v1alpha1.ClusterStatus // }{ // { -// name: "scenario 1: adopt existing AWS cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, +// name: "scenario 1: adopt existing AWS cluster", +// cluster: clusterName, // returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, // returnErrorGetClusterByHandle: nil, // existingObjects: []ctrlruntimeclient.Object{ @@ -218,10 +201,7 @@ package controller_test // }, // }, // }, -// returnGetClusterByHandle: &gqlclient.ClusterFragment{ -// ID: clusterConsoleID, -// CurrentVersion: lo.ToPtr("1.24.11"), -// }, +// returnGetClusterByHandle: , // returnErrorGetClusterByHandle: nil, // existingObjects: []ctrlruntimeclient.Object{ // &v1alpha1.Cluster{ @@ -240,7 +220,6 @@ package controller_test // fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() // // fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(test.returnGetClusterByHandle, test.returnErrorGetClusterByHandle) // // ctx := context.Background() // From 04f82dbdcf40f09c64b434afbbbdebc497c9bb5a Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 19 Dec 2023 12:11:39 +0100 Subject: [PATCH 168/198] add ginkgo tests for repo --- .../gitrepository_controller_test.go | 313 ----------------- .../controller/gitrepository_ginkgo_test.go | 319 ++++++++++++++++++ controller/internal/test/utils/kubernetes.go | 18 + 3 files changed, 337 insertions(+), 313 deletions(-) delete mode 100644 controller/internal/controller/gitrepository_controller_test.go create mode 100644 controller/internal/controller/gitrepository_ginkgo_test.go diff --git a/controller/internal/controller/gitrepository_controller_test.go b/controller/internal/controller/gitrepository_controller_test.go deleted file mode 100644 index 590d271cc..000000000 --- a/controller/internal/controller/gitrepository_controller_test.go +++ /dev/null @@ -1,313 +0,0 @@ -package controller_test - -// -//import ( -// "context" -// "encoding/json" -// "testing" -// -// gqlclient "github.com/pluralsh/console-client-go" -// "github.com/pluralsh/console/controller/internal/controller" -// "github.com/samber/lo" -// "github.com/stretchr/testify/mock" -// -// "github.com/pluralsh/console/controller/api/v1alpha1" -// "github.com/pluralsh/console/controller/internal/test/mocks" -// "github.com/stretchr/testify/assert" -// corev1 "k8s.io/api/core/v1" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/types" -// utilruntime "k8s.io/apimachinery/pkg/util/runtime" -// "k8s.io/client-go/kubernetes/scheme" -// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" -// "sigs.k8s.io/controller-runtime/pkg/client/fake" -// "sigs.k8s.io/controller-runtime/pkg/reconcile" -//) -// -//func init() { -// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -//} -// -//func TestCreateNewRepository(t *testing.T) { -// tests := []struct { -// name string -// repository string -// returnGetRepository *gqlclient.GetGitRepository -// returnErrorGetRepository error -// returnCreateRepository *gqlclient.CreateGitRepository -// returnErrorCreateRepository error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.GitRepositoryStatus -// }{ -// { -// name: "scenario 1: create a new repository", -// repository: "test", -// expectedStatus: v1alpha1.GitRepositoryStatus{ -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetRepository: &gqlclient.GetGitRepository{ -// GitRepository: nil, -// }, -// returnCreateRepository: &gqlclient.CreateGitRepository{ -// CreateGitRepository: &gqlclient.GitRepositoryFragment{ -// ID: "123", -// }, -// }, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.GitRepository{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "test", -// }, -// Spec: v1alpha1.GitRepositorySpec{ -// Url: "https://test", -// CredentialsRef: &corev1.SecretReference{ -// Name: "testsecret", -// }, -// }, -// }, -// &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "testsecret", -// }, -// Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// // setup the test scenario -// fakeClient := fake. -// NewClientBuilder(). -// WithScheme(scheme.Scheme). -// WithObjects(test.existingObjects...). -// Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// // act -// ctx := context.Background() -// target := &controller.GitRepositoryReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) -// fakeConsoleClient.On("CreateRepository", mock.AnythingOfType("string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.returnCreateRepository, test.returnErrorCreateRepository) -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) -// assert.NoError(t, err) -// existingRepo := &v1alpha1.GitRepository{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) -// assert.NoError(t, err) -// existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) -// assert.NoError(t, err) -// expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} -// -//func TestUpdateRepository(t *testing.T) { -// tests := []struct { -// name string -// repository string -// returnGetRepository *gqlclient.GetGitRepository -// returnErrorGetRepository error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.GitRepositoryStatus -// }{ -// { -// name: "scenario 1: update credentials", -// repository: "test", -// expectedStatus: v1alpha1.GitRepositoryStatus{ -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetRepository: &gqlclient.GetGitRepository{ -// GitRepository: &gqlclient.GitRepositoryFragment{ -// ID: "123", -// }, -// }, -// -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.GitRepository{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "test", -// }, -// Spec: v1alpha1.GitRepositorySpec{ -// Url: "https://test", -// CredentialsRef: &corev1.SecretReference{ -// Name: "testsecret", -// }, -// }, -// Status: v1alpha1.GitRepositoryStatus{ -// Health: "", -// Message: nil, -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("ABC"), -// }, -// }, -// &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "testsecret", -// }, -// Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// // setup the test scenario -// fakeClient := fake. -// NewClientBuilder(). -// WithScheme(scheme.Scheme). -// WithObjects(test.existingObjects...). -// Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// // act -// ctx := context.Background() -// target := &controller.GitRepositoryReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) -// fakeConsoleClient.On("UpdateRepository", mock.Anything, mock.Anything).Return(&gqlclient.UpdateGitRepository{}, nil) -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) -// assert.NoError(t, err) -// existingRepo := &v1alpha1.GitRepository{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) -// assert.NoError(t, err) -// existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) -// assert.NoError(t, err) -// expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) -// assert.NoError(t, err) -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} -// -//func TestImportRepository(t *testing.T) { -// tests := []struct { -// name string -// repository string -// returnGetRepository *gqlclient.GetGitRepository -// returnErrorGetRepository error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.GitRepositoryStatus -// }{ -// { -// name: "scenario 1: update credentials", -// repository: "test", -// expectedStatus: v1alpha1.GitRepositoryStatus{ -// ID: lo.ToPtr("123"), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetRepository: &gqlclient.GetGitRepository{ -// GitRepository: &gqlclient.GitRepositoryFragment{ -// ID: "123", -// }, -// }, -// -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.GitRepository{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "test", -// }, -// Spec: v1alpha1.GitRepositorySpec{ -// Url: "https://test", -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// // setup the test scenario -// fakeClient := fake. -// NewClientBuilder(). -// WithScheme(scheme.Scheme). -// WithObjects(test.existingObjects...). -// Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// // act -// ctx := context.Background() -// target := &controller.GitRepositoryReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.repository}}) -// assert.NoError(t, err) -// existingRepo := &v1alpha1.GitRepository{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.repository}, existingRepo) -// assert.NoError(t, err) -// existingStatusJson, err := json.Marshal(sanitizeRepoConditions(existingRepo.Status)) -// assert.NoError(t, err) -// expectedStatusJson, err := json.Marshal(sanitizeRepoConditions(test.expectedStatus)) -// assert.NoError(t, err) -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} -// -//func sanitizeRepoConditions(status v1alpha1.GitRepositoryStatus) v1alpha1.GitRepositoryStatus { -// for i := range status.Conditions { -// status.Conditions[i].LastTransitionTime = metav1.Time{} -// status.Conditions[i].ObservedGeneration = 0 -// } -// -// return status -//} diff --git a/controller/internal/controller/gitrepository_ginkgo_test.go b/controller/internal/controller/gitrepository_ginkgo_test.go new file mode 100644 index 000000000..fd94b0e6a --- /dev/null +++ b/controller/internal/controller/gitrepository_ginkgo_test.go @@ -0,0 +1,319 @@ +package controller + +import ( + "context" + + "github.com/pluralsh/console/controller/internal/test/utils" + + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/test/mocks" +) + +func sanitizeRepoConditions(status v1alpha1.GitRepositoryStatus) v1alpha1.GitRepositoryStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + +var _ = Describe("Repository Controller", Ordered, func() { + Context("When reconciling a resource", func() { + const ( + repoName = "test-repo" + repoUrl = "https://test" + namespace = "default" + repoID = "123" + ) + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: repoName, + Namespace: "default", + } + + repository := &v1alpha1.GitRepository{} + + BeforeAll(func() { + By("creating the custom resource for the Kind Repository") + err := k8sClient.Get(ctx, typeNamespacedName, repository) + if err != nil && errors.IsNotFound(err) { + resource := &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + Namespace: namespace, + }, + Spec: v1alpha1.GitRepositorySpec{ + Url: repoUrl, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterAll(func() { + resource := &v1alpha1.GitRepository{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Repository") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should successfully reconcile the import resource", func() { + By("Reconciling the import resource") + test := struct { + returnGetRepository *gqlclient.GetGitRepository + returnErrorGetRepository error + returnCreateRepository *gqlclient.CreateGitRepository + returnErrorCreateRepository error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.GitRepositoryStatus + }{ + expectedStatus: v1alpha1.GitRepositoryStatus{ + ID: lo.ToPtr("123"), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }, + returnGetRepository: &gqlclient.GetGitRepository{ + GitRepository: &gqlclient.GitRepositoryFragment{ + ID: repoID, + }, + }, + } + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) + + controllerReconciler := &GitRepositoryReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + + repository := &v1alpha1.GitRepository{} + err = k8sClient.Get(ctx, typeNamespacedName, repository) + + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeRepoConditions(repository.Status)).To(Equal(sanitizeRepoConditions(test.expectedStatus))) + }) + }) + + Context("When reconciling a resource", func() { + const ( + repoName = "test-repo" + repoUrl = "https://test" + secretName = "test-secret" + namespace = "default" + repoID = "123" + ) + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: repoName, + Namespace: "default", + } + + repository := &v1alpha1.GitRepository{} + secret := &corev1.Secret{} + + BeforeAll(func() { + By("creating the custom resource for the Kind Repository") + err := k8sClient.Get(ctx, typeNamespacedName, repository) + if err != nil && errors.IsNotFound(err) { + resource := &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + Namespace: namespace, + }, + Spec: v1alpha1.GitRepositorySpec{ + Url: repoUrl, + CredentialsRef: &corev1.SecretReference{ + Name: secretName, + Namespace: namespace, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + + By("creating the custom resource for the Kind Secret") + err = k8sClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, secret) + if err != nil && errors.IsNotFound(err) { + resource := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + Data: map[string][]byte{"z": {1, 2, 3}, "a": {4, 5, 6}}, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterAll(func() { + resource := &v1alpha1.GitRepository{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Repository") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + + err = k8sClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, secret) + By("Cleanup the specific resource instance Secret") + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + }) + + It("should successfully reconcile the creation resource", func() { + By("Reconciling the created resource") + test := struct { + returnGetRepository *gqlclient.GetGitRepository + returnErrorGetRepository error + returnCreateRepository *gqlclient.CreateGitRepository + returnErrorCreateRepository error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.GitRepositoryStatus + }{ + expectedStatus: v1alpha1.GitRepositoryStatus{ + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }, + returnGetRepository: &gqlclient.GetGitRepository{ + GitRepository: nil, + }, + returnCreateRepository: &gqlclient.CreateGitRepository{ + CreateGitRepository: &gqlclient.GitRepositoryFragment{ + ID: "123", + }, + }, + } + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) + fakeConsoleClient.On("CreateRepository", mock.AnythingOfType("string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.returnCreateRepository, test.returnErrorCreateRepository) + + controllerReconciler := &GitRepositoryReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + + repository := &v1alpha1.GitRepository{} + err = k8sClient.Get(ctx, typeNamespacedName, repository) + + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeRepoConditions(repository.Status)).To(Equal(sanitizeRepoConditions(test.expectedStatus))) + }) + + It("should successfully reconcile the update resource", func() { + By("Reconciling the updated resource") + test := struct { + returnGetRepository *gqlclient.GetGitRepository + returnErrorGetRepository error + existingObjects []ctrlruntimeclient.Object + expectedStatus v1alpha1.GitRepositoryStatus + }{ + expectedStatus: v1alpha1.GitRepositoryStatus{ + ID: lo.ToPtr(repoID), + SHA: lo.ToPtr("TEFHFGIB5PQMBLUWST2R6DXTY5QGH74WVGIKYQI7I3BY7BCSBDLA===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }, + returnGetRepository: &gqlclient.GetGitRepository{ + GitRepository: &gqlclient.GitRepositoryFragment{ + ID: repoID, + }, + }, + } + + Expect(utils.MaybePatch(k8sClient, &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{Name: repoName, Namespace: namespace}, + }, func(p *v1alpha1.GitRepository) { + p.Status.ID = lo.ToPtr(repoID) + p.Status.SHA = lo.ToPtr("ABC") + })).To(Succeed()) + + repository := &v1alpha1.GitRepository{} + err := k8sClient.Get(ctx, typeNamespacedName, repository) + Expect(err).NotTo(HaveOccurred()) + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetRepository", mock.AnythingOfType("*string")).Return(test.returnGetRepository, test.returnErrorGetRepository) + fakeConsoleClient.On("UpdateRepository", mock.Anything, mock.Anything).Return(&gqlclient.UpdateGitRepository{}, nil) + + controllerReconciler := &GitRepositoryReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, typeNamespacedName, repository) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeRepoConditions(repository.Status)).To(Equal(sanitizeRepoConditions(test.expectedStatus))) + }) + }) +}) diff --git a/controller/internal/test/utils/kubernetes.go b/controller/internal/test/utils/kubernetes.go index 271f54524..b4ceeb967 100644 --- a/controller/internal/test/utils/kubernetes.go +++ b/controller/internal/test/utils/kubernetes.go @@ -31,3 +31,21 @@ func MaybeCreate[O client.Object](c client.Client, object O, patch Patcher[O]) e return c.Status().Patch(ctx, object, client.MergeFrom(original)) } + +func MaybePatch[O client.Object](c client.Client, object O, patch Patcher[O]) error { + ctx := context.Background() + original := object.DeepCopyObject().(O) + + err := c.Get(ctx, client.ObjectKey{Name: object.GetName(), Namespace: object.GetNamespace()}, object) + if err != nil { + return err + } + + if patch == nil { + return nil + } + + patch(object) + + return c.Status().Patch(ctx, object, client.MergeFrom(original)) +} From 9f3190f22ca347fe8a8afe63a84c5b8df77f2642 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 12:48:14 +0100 Subject: [PATCH 169/198] finish rewriting cluster tests --- .../cluster_controller_ginkgo_test.go | 291 -------- .../controller/cluster_controller_test.go | 623 +++++++++++------- 2 files changed, 378 insertions(+), 536 deletions(-) delete mode 100644 controller/internal/controller/cluster_controller_ginkgo_test.go diff --git a/controller/internal/controller/cluster_controller_ginkgo_test.go b/controller/internal/controller/cluster_controller_ginkgo_test.go deleted file mode 100644 index 084a68f8e..000000000 --- a/controller/internal/controller/cluster_controller_ginkgo_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package controller - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - gqlclient "github.com/pluralsh/console-client-go" - "github.com/samber/lo" - "github.com/stretchr/testify/mock" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/test/mocks" - "github.com/pluralsh/console/controller/internal/test/utils" -) - -func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { - for i := range status.Conditions { - status.Conditions[i].LastTransitionTime = metav1.Time{} - status.Conditions[i].ObservedGeneration = 0 - } - - return status -} - -var _ = Describe("Cluster Controller", Ordered, func() { - Context("When creating a resource", func() { - const ( - awsProviderName = "aws-provider" - awsProviderConsoleID = "aws-provider-console-id" - awsClusterName = "aws-cluster" - awsClusterConsoleID = "aws-cluster-console-id" - byokClusterName = "byok-test-cluster" - byokClusterConsoleID = "byok-cluster-console-id" - awsReadonlyClusterName = "aws-readonly-cluster" - awsReadonlyClusterConsoleID = "aws-readonly-cluster-console-id" - byokReadonlyClusterName = "byok-readonly-cluster" - byokReadonlyClusterConsoleID = "byok-readonly-cluster-console-id" - ) - - ctx := context.Background() - awsNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} - byokNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} - awsReadonlyNamespacedName := types.NamespacedName{Name: awsReadonlyClusterName, Namespace: "default"} - byokReadonlyNamespacedName := types.NamespacedName{Name: byokReadonlyClusterName, Namespace: "default"} - - BeforeAll(func() { - By("Creating AWS provider") - Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ - ObjectMeta: metav1.ObjectMeta{Name: awsProviderName}, - Spec: v1alpha1.ProviderSpec{ - Cloud: "aws", - Name: awsProviderName, - }, - }, func(p *v1alpha1.Provider) { - p.Status.ID = lo.ToPtr(awsProviderConsoleID) - })).To(Succeed()) - - By("Creating AWS cluster") - Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: awsClusterName, - Namespace: "default", - }, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(awsClusterName), - Version: lo.ToPtr("1.24"), - Cloud: "aws", - ProviderRef: &corev1.ObjectReference{Name: awsProviderName}, - }, - }, nil)).To(Succeed()) - - By("Creating BYOK cluster") - Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: byokClusterName, - Namespace: "default", - }, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(byokClusterName), - Cloud: "byok", - }, - }, nil)).To(Succeed()) - - By("Creating AWS cluster that will adopt existing Console resource") - Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: awsReadonlyClusterName, - Namespace: "default", - }, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(awsReadonlyClusterName), - Cloud: "aws", - }, - }, nil)).To(Succeed()) - - By("Creating BYOK cluster that will adopt existing Console resource") - Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: byokReadonlyClusterName, - Namespace: "default", - }, - Spec: v1alpha1.ClusterSpec{ - Handle: lo.ToPtr(byokReadonlyClusterName), - Cloud: "byok", - }, - }, nil)).To(Succeed()) - }) - - AfterAll(func() { - By("Cleanup AWS cluster") - awsCluster := &v1alpha1.Cluster{} - err := k8sClient.Get(ctx, awsNamespacedName, awsCluster) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, awsCluster)).To(Succeed()) - - By("Cleanup BYOK cluster") - byokCluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, byokNamespacedName, byokCluster) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, byokCluster)).To(Succeed()) - - By("Cleanup AWS readonly cluster") - awsReadonlyCluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, awsReadonlyNamespacedName, awsReadonlyCluster) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, awsReadonlyCluster)).To(Succeed()) - - By("Cleanup BYOK readonly cluster") - byokReadonlyCluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, byokReadonlyNamespacedName, byokReadonlyCluster) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, byokReadonlyCluster)).To(Succeed()) - }) - - It("should successfully reconcile AWS cluster", func() { - fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) - fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: awsClusterConsoleID}, nil) - - controllerReconciler := &ClusterReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - ConsoleClient: fakeConsoleClient, - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsNamespacedName}) - Expect(err).NotTo(HaveOccurred()) - - cluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, awsNamespacedName, cluster) - Expect(err).NotTo(HaveOccurred()) - Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ - ID: lo.ToPtr(awsClusterConsoleID), - SHA: lo.ToPtr("J7CMSICIXLWV7MCWNPBZUA6FEOI3HGTQMNVLYD6VZXX6Y66S6ETQ===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }))) - }) - - It("should successfully reconcile BYOK cluster", func() { - fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) - fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) - fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: byokClusterConsoleID}, nil) - - controllerReconciler := &ClusterReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - ConsoleClient: fakeConsoleClient, - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokNamespacedName}) - Expect(err).NotTo(HaveOccurred()) - - cluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, awsNamespacedName, cluster) - Expect(err).NotTo(HaveOccurred()) - Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ - ID: lo.ToPtr(byokClusterConsoleID), - SHA: lo.ToPtr("J7CMSICIXLWV7MCWNPBZUA6FEOI3HGTQMNVLYD6VZXX6Y66S6ETQ===="), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionFalse, - Reason: v1alpha1.ReadonlyConditionReason.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - }))) - }) - - It("should successfully reconcile AWS readonly cluster", func() { - fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(&gqlclient.ClusterFragment{ - ID: awsReadonlyClusterConsoleID, - CurrentVersion: lo.ToPtr("1.24.11"), - }, nil) - - controllerReconciler := &ClusterReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - ConsoleClient: fakeConsoleClient, - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsReadonlyNamespacedName}) - Expect(err).NotTo(HaveOccurred()) - - cluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, awsReadonlyNamespacedName, cluster) - Expect(err).NotTo(HaveOccurred()) - Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ - ID: lo.ToPtr(awsReadonlyClusterConsoleID), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: v1alpha1.ReadonlyTrueConditionMessage.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - CurrentVersion: lo.ToPtr("1.24.11"), - }))) - }) - - It("should successfully reconcile BYOK readonly cluster", func() { - fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) - fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(&gqlclient.ClusterFragment{ - ID: byokReadonlyClusterConsoleID, - CurrentVersion: lo.ToPtr("1.24.11"), - }, nil) - - controllerReconciler := &ClusterReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - ConsoleClient: fakeConsoleClient, - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokReadonlyNamespacedName}) - Expect(err).NotTo(HaveOccurred()) - - cluster := &v1alpha1.Cluster{} - err = k8sClient.Get(ctx, byokReadonlyNamespacedName, cluster) - Expect(err).NotTo(HaveOccurred()) - Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ - ID: lo.ToPtr(byokReadonlyClusterConsoleID), - Conditions: []metav1.Condition{ - { - Type: v1alpha1.ReadonlyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadonlyConditionReason.String(), - Message: v1alpha1.ReadonlyTrueConditionMessage.String(), - }, - { - Type: v1alpha1.ReadyConditionType.String(), - Status: metav1.ConditionTrue, - Reason: v1alpha1.ReadyConditionReason.String(), - }, - }, - CurrentVersion: lo.ToPtr("1.24.11"), - }))) - }) - }) -}) diff --git a/controller/internal/controller/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go index 3cf378e6e..64435e860 100644 --- a/controller/internal/controller/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -1,245 +1,378 @@ -package controller_test - -// func TestUpdateCluster(t *testing.T) { -// const ( -// clusterName = "test-cluster" -// clusterConsoleID = "12345-67890" -// providerName = "test-provider" -// providerNamespace = "test-provider" -// providerConsoleID = "12345-67890" -// ) -// -// tests := []struct { -// name string -// cluster string -// returnIsClusterExisting bool -// returnUpdateCluster *gqlclient.ClusterFragment -// returnErrorUpdateCluster error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ClusterStatus -// }{ -// { -// name: "scenario 1: update AWS cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnIsClusterExisting: true, -// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Version: lo.ToPtr("1.24"), -// Cloud: "aws", -// ProviderRef: &corev1.ObjectReference{Name: providerName}, -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// }, -// &v1alpha1.Provider{ -// ObjectMeta: metav1.ObjectMeta{Name: providerName}, -// Spec: v1alpha1.ProviderSpec{ -// Cloud: "aws", -// Name: providerName, -// Namespace: providerNamespace, -// }, -// Status: v1alpha1.ProviderStatus{ID: lo.ToPtr(providerConsoleID)}, -// }, -// }, -// }, -// { -// name: "scenario 2: update BYOK cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("XGLLQCLXY5LEQV2UAQDUSOZ2MN24L67HDIGWRK2MA5STBBRNMVDA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnIsClusterExisting: true, -// returnUpdateCluster: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Cloud: "byok", -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// SHA: lo.ToPtr("DU5PTA62PGOS35CPPCNSRG6PGXUUIWTXVBK5BFXCCGCAAM2K6HYA===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(test.returnIsClusterExisting) -// fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return(test.returnUpdateCluster, test.returnErrorUpdateCluster) -// -// ctx := context.Background() -// -// target := &controller.ClusterReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) -// assert.NoError(t, err) -// -// existingCluster := &v1alpha1.Cluster{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) -// -// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) -// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) -// -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -// } -//func TestAdoptExistingCluster(t *testing.T) { -// const ( -// clusterName = "test-cluster" -// clusterConsoleID = "12345-67890" -// ) -// -// tests := []struct { -// name string -// cluster string -// returnGetClusterByHandle *gqlclient.ClusterFragment -// returnErrorGetClusterByHandle error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ClusterStatus -// }{ -// { -// name: "scenario 1: adopt existing AWS cluster", -// cluster: clusterName, -// returnGetClusterByHandle: &gqlclient.ClusterFragment{ID: clusterConsoleID}, -// returnErrorGetClusterByHandle: nil, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{Handle: lo.ToPtr(clusterName)}, -// }, -// }, -// }, -// { -// name: "scenario 2: adopt existing BYOK cluster", -// cluster: clusterName, -// expectedStatus: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr(clusterConsoleID), -// CurrentVersion: lo.ToPtr("1.24.11"), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetClusterByHandle: , -// returnErrorGetClusterByHandle: nil, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{Name: clusterName}, -// Spec: v1alpha1.ClusterSpec{ -// Handle: lo.ToPtr(clusterName), -// Cloud: "byok", -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// ctx := context.Background() -// -// target := &controller.ClusterReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.cluster}}) -// assert.NoError(t, err) -// -// existingCluster := &v1alpha1.Cluster{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.cluster}, existingCluster) -// -// existingStatusJson, _ := json.Marshal(sanitizeClusterStatus(existingCluster.Status)) -// expectedStatusJson, _ := json.Marshal(sanitizeClusterStatus(test.expectedStatus)) -// -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/test/mocks" + "github.com/pluralsh/console/controller/internal/test/utils" +) + +func sanitizeClusterStatus(status v1alpha1.ClusterStatus) v1alpha1.ClusterStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + +var _ = Describe("Cluster Controller", Ordered, func() { + Context("when reconciling resource", func() { + const ( + awsProviderName = "aws-provider" + awsProviderConsoleID = "aws-provider-console-id" + awsClusterName = "aws-cluster" + awsClusterConsoleID = "aws-cluster-console-id" + byokClusterName = "byok-cluster" + byokClusterConsoleID = "byok-cluster-console-id" + awsReadonlyClusterName = "aws-readonly-cluster" + awsReadonlyClusterConsoleID = "aws-readonly-cluster-console-id" + byokReadonlyClusterName = "byok-readonly-cluster" + byokReadonlyClusterConsoleID = "byok-readonly-cluster-console-id" + ) + + ctx := context.Background() + awsNamespacedName := types.NamespacedName{Name: awsClusterName, Namespace: "default"} + byokNamespacedName := types.NamespacedName{Name: byokClusterName, Namespace: "default"} + awsReadonlyNamespacedName := types.NamespacedName{Name: awsReadonlyClusterName, Namespace: "default"} + byokReadonlyNamespacedName := types.NamespacedName{Name: byokReadonlyClusterName, Namespace: "default"} + + BeforeAll(func() { + By("Creating AWS provider") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{Name: awsProviderName}, + Spec: v1alpha1.ProviderSpec{ + Cloud: "aws", + Name: awsProviderName, + }, + }, func(p *v1alpha1.Provider) { + p.Status.ID = lo.ToPtr(awsProviderConsoleID) + })).To(Succeed()) + + By("Creating AWS cluster") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: awsClusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(awsClusterName), + Version: lo.ToPtr("1.24"), + Cloud: "aws", + ProviderRef: &corev1.ObjectReference{Name: awsProviderName}, + }, + }, nil)).To(Succeed()) + + By("Creating BYOK cluster") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: byokClusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(byokClusterName), + Cloud: "byok", + }, + }, nil)).To(Succeed()) + + By("Creating AWS cluster that will adopt existing Console resource") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: awsReadonlyClusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(awsReadonlyClusterName), + Cloud: "aws", + }, + }, nil)).To(Succeed()) + + By("Creating BYOK cluster that will adopt existing Console resource") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: byokReadonlyClusterName, + Namespace: "default", + }, + Spec: v1alpha1.ClusterSpec{ + Handle: lo.ToPtr(byokReadonlyClusterName), + Cloud: "byok", + }, + }, nil)).To(Succeed()) + }) + + AfterAll(func() { + By("Cleanup AWS cluster") + awsCluster := &v1alpha1.Cluster{} + err := k8sClient.Get(ctx, awsNamespacedName, awsCluster) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, awsCluster)).To(Succeed()) + + By("Cleanup BYOK cluster") + byokCluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokNamespacedName, byokCluster) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, byokCluster)).To(Succeed()) + + By("Cleanup AWS readonly cluster") + awsReadonlyCluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsReadonlyNamespacedName, awsReadonlyCluster) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, awsReadonlyCluster)).To(Succeed()) + + By("Cleanup BYOK readonly cluster") + byokReadonlyCluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokReadonlyNamespacedName, byokReadonlyCluster) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, byokReadonlyCluster)).To(Succeed()) + }) + + It("should successfully reconcile AWS cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, awsClusterName)) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: awsClusterConsoleID}, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(awsClusterConsoleID), + SHA: lo.ToPtr("J7CMSICIXLWV7MCWNPBZUA6FEOI3HGTQMNVLYD6VZXX6Y66S6ETQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + + It("should successfully reconcile and update AWS cluster that was created in previous unit test", func() { + Expect(utils.MaybePatch(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: awsClusterName, Namespace: "default"}, + }, func(p *v1alpha1.Cluster) { + p.Status.SHA = lo.ToPtr("diff-sha") + })).To(Succeed()) + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(true) + fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return( + &gqlclient.ClusterFragment{ID: awsClusterConsoleID, CurrentVersion: lo.ToPtr("1.25.6")}, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(awsClusterConsoleID), + SHA: lo.ToPtr("J7CMSICIXLWV7MCWNPBZUA6FEOI3HGTQMNVLYD6VZXX6Y66S6ETQ===="), + CurrentVersion: lo.ToPtr("1.25.6"), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + + It("should successfully reconcile BYOK cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(nil, errors.NewNotFound(schema.GroupResource{}, byokClusterName)) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(false) + fakeConsoleClient.On("CreateCluster", mock.Anything).Return(&gqlclient.ClusterFragment{ID: byokClusterConsoleID}, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(byokClusterConsoleID), + SHA: lo.ToPtr("CPYLCGRGF2JWFBF3OGRHQQUSBDXW6Y4VMUDQDCQQDEA6G6CAZORQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + + It("should successfully reconcile and update BYOK cluster that was created in previous unit test", func() { + Expect(utils.MaybePatch(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: byokClusterName, Namespace: "default"}, + }, func(p *v1alpha1.Cluster) { + p.Status.SHA = lo.ToPtr("diff-sha") + })).To(Succeed()) + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("IsClusterExisting", mock.AnythingOfType("*string")).Return(true) + fakeConsoleClient.On("UpdateCluster", mock.AnythingOfType("string"), mock.Anything).Return( + &gqlclient.ClusterFragment{ID: byokClusterConsoleID, CurrentVersion: lo.ToPtr("1.25.6")}, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokNamespacedName, cluster) + + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(byokClusterConsoleID), + SHA: lo.ToPtr("CPYLCGRGF2JWFBF3OGRHQQUSBDXW6Y4VMUDQDCQQDEA6G6CAZORQ===="), + CurrentVersion: lo.ToPtr("1.25.6"), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + + It("should successfully reconcile AWS readonly cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(&gqlclient.ClusterFragment{ + ID: awsReadonlyClusterConsoleID, + CurrentVersion: lo.ToPtr("1.24.11"), + }, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awsReadonlyNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, awsReadonlyNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(awsReadonlyClusterConsoleID), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + CurrentVersion: lo.ToPtr("1.24.11"), + }))) + }) + + It("should successfully reconcile BYOK readonly cluster", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetClusterByHandle", mock.AnythingOfType("*string")).Return(&gqlclient.ClusterFragment{ + ID: byokReadonlyClusterConsoleID, + CurrentVersion: lo.ToPtr("1.24.11"), + }, nil) + + controllerReconciler := &ClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: byokReadonlyNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + cluster := &v1alpha1.Cluster{} + err = k8sClient.Get(ctx, byokReadonlyNamespacedName, cluster) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeClusterStatus(cluster.Status)).To(Equal(sanitizeClusterStatus(v1alpha1.ClusterStatus{ + ID: lo.ToPtr(byokReadonlyClusterConsoleID), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + CurrentVersion: lo.ToPtr("1.24.11"), + }))) + }) + }) +}) From 9f76e11a961ffe2b5c080812a0dbebdeb54c00b6 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 19 Dec 2023 12:53:31 +0100 Subject: [PATCH 170/198] update kustomize configuration and cleanup --- controller/Dockerfile | 4 +- controller/Makefile | 3 +- controller/cmd/main.go | 12 +- controller/config/crd/kustomization.yaml | 44 ++---- controller/config/crd/kustomizeconfig.yaml | 17 --- .../crd/patches/cainjection_in_clusters.yaml | 8 -- .../cainjection_in_gitrepositories.yaml | 8 -- .../crd/patches/cainjection_in_providers.yaml | 8 -- .../cainjection_in_servicedeployments.yaml | 8 -- .../crd/patches/webhook_in_clusters.yaml | 17 --- .../patches/webhook_in_gitrepositories.yaml | 17 --- .../crd/patches/webhook_in_providers.yaml | 17 --- .../webhook_in_servicedeployments.yaml | 17 --- controller/config/default/kustomization.yaml | 128 ++---------------- .../default/manager_auth_proxy_patch.yaml | 3 +- .../config/default/manager_config_patch.yaml | 4 +- controller/config/manager/kustomization.yaml | 6 + controller/config/manager/manager.yaml | 54 ++------ .../config/manager/service_account.yaml | 4 + controller/config/prometheus/monitor.yaml | 5 +- .../rbac/auth_proxy_client_clusterrole.yaml | 16 --- controller/config/rbac/auth_proxy_role.yaml | 24 ---- .../config/rbac/auth_proxy_role_binding.yaml | 19 --- .../config/rbac/auth_proxy_service.yaml | 21 --- controller/config/rbac/kustomization.yaml | 8 -- .../config/rbac/leader_election_role.yaml | 7 - .../rbac/leader_election_role_binding.yaml | 9 +- controller/config/rbac/role.yaml | 20 +++ controller/config/rbac/role_binding.yaml | 9 +- controller/config/rbac/service_account.yaml | 12 -- .../rbac/{ => user}/cluster_editor_role.yaml | 0 .../rbac/{ => user}/cluster_viewer_role.yaml | 0 .../{ => user}/gitrepository_editor_role.yaml | 0 .../{ => user}/gitrepository_viewer_role.yaml | 0 .../rbac/{ => user}/provider_editor_role.yaml | 0 .../rbac/{ => user}/provider_viewer_role.yaml | 0 .../servicedeployment_editor_role.yaml | 0 .../servicedeployment_viewer_role.yaml | 0 .../samples/deployments_v1alpha1_cluster.yaml | 12 -- controller/config/samples/kustomization.yaml | 6 +- controller/hack/gen-client-mocks.sh | 12 -- controller/hack/lib.sh | 67 --------- .../controller/gitrepository_controller.go | 15 +- .../controller/provider_controller.go | 2 +- .../servicedeployment_controller.go | 16 ++- controller/internal/controller/suite_test.go | 3 - 46 files changed, 99 insertions(+), 563 deletions(-) delete mode 100644 controller/config/crd/kustomizeconfig.yaml delete mode 100644 controller/config/crd/patches/cainjection_in_clusters.yaml delete mode 100644 controller/config/crd/patches/cainjection_in_gitrepositories.yaml delete mode 100644 controller/config/crd/patches/cainjection_in_providers.yaml delete mode 100644 controller/config/crd/patches/cainjection_in_servicedeployments.yaml delete mode 100644 controller/config/crd/patches/webhook_in_clusters.yaml delete mode 100644 controller/config/crd/patches/webhook_in_gitrepositories.yaml delete mode 100644 controller/config/crd/patches/webhook_in_providers.yaml delete mode 100644 controller/config/crd/patches/webhook_in_servicedeployments.yaml create mode 100644 controller/config/manager/service_account.yaml delete mode 100644 controller/config/rbac/auth_proxy_client_clusterrole.yaml delete mode 100644 controller/config/rbac/auth_proxy_role.yaml delete mode 100644 controller/config/rbac/auth_proxy_role_binding.yaml delete mode 100644 controller/config/rbac/auth_proxy_service.yaml delete mode 100644 controller/config/rbac/service_account.yaml rename controller/config/rbac/{ => user}/cluster_editor_role.yaml (100%) rename controller/config/rbac/{ => user}/cluster_viewer_role.yaml (100%) rename controller/config/rbac/{ => user}/gitrepository_editor_role.yaml (100%) rename controller/config/rbac/{ => user}/gitrepository_viewer_role.yaml (100%) rename controller/config/rbac/{ => user}/provider_editor_role.yaml (100%) rename controller/config/rbac/{ => user}/provider_viewer_role.yaml (100%) rename controller/config/rbac/{ => user}/servicedeployment_editor_role.yaml (100%) rename controller/config/rbac/{ => user}/servicedeployment_viewer_role.yaml (100%) delete mode 100644 controller/config/samples/deployments_v1alpha1_cluster.yaml delete mode 100755 controller/hack/gen-client-mocks.sh delete mode 100644 controller/hack/lib.sh diff --git a/controller/Dockerfile b/controller/Dockerfile index c389c0981..36e5cd88a 100644 --- a/controller/Dockerfile +++ b/controller/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.20 as builder +FROM golang:1.21 as builder ARG TARGETOS ARG TARGETARCH @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/controller/Makefile b/controller/Makefile index 12acf90ae..fd2b57207 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -145,8 +145,9 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + @echo asd > /tmp/config.env $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + @rm /tmp/config.env .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. diff --git a/controller/cmd/main.go b/controller/cmd/main.go index 8b4dc6e76..6fedf3e3d 100644 --- a/controller/cmd/main.go +++ b/controller/cmd/main.go @@ -26,9 +26,6 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" - deploymentsv1alpha "github.com/pluralsh/console/controller/api/v1alpha1" - "github.com/pluralsh/console/controller/internal/client" - "github.com/pluralsh/console/controller/internal/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -37,7 +34,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlruntimezap "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - //+kubebuilder:scaffold:imports + + deploymentsv1alpha "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/types" ) var ( @@ -53,8 +53,6 @@ func init() { utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(deploymentsv1alpha.AddToScheme(scheme)) - - //+kubebuilder:scaffold:scheme } type controllerRunOptions struct { @@ -123,8 +121,6 @@ func main() { 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) diff --git a/controller/config/crd/kustomization.yaml b/controller/config/crd/kustomization.yaml index 29cf4e730..44f433e8b 100644 --- a/controller/config/crd/kustomization.yaml +++ b/controller/config/crd/kustomization.yaml @@ -1,32 +1,16 @@ -# 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/deployments.plural.sh_clusters.yaml -- bases/deployments.plural.sh_providers.yaml -- bases/deployments.plural.sh_gitrepositories.yaml -- bases/deployments.plural.sh_servicedeployments.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_clusters.yaml -#- patches/webhook_in_providers.yaml -#- patches/webhook_in_gitrepositories.yaml -#- patches/webhook_in_servicedeployments.yaml -#- path: patches/webhook_in_clusters.yaml -#+kubebuilder:scaffold:crdkustomizewebhookpatch +# Adds namespace to all resources. +namespace: plural-deployment-controller -# [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_clusters.yaml -#- patches/cainjection_in_providers.yaml -#- patches/cainjection_in_gitrepositories.yaml -#- patches/cainjection_in_servicedeployments.yaml -#- path: patches/cainjection_in_clusters.yaml -#+kubebuilder:scaffold:crdkustomizecainjectionpatch +# Labels to add to all resources and selectors. +labels: + - includeSelectors: false + pairs: + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + app.kubernetes.io/managed-by: kustomize -# the following config is for teaching kustomize how to do kustomization for CRDs. -configurations: -- kustomizeconfig.yaml +resources: + - bases/deployments.plural.sh_providers.yaml + - bases/deployments.plural.sh_gitrepositories.yaml + - bases/deployments.plural.sh_clusters.yaml + - bases/deployments.plural.sh_servicedeployments.yaml diff --git a/controller/config/crd/kustomizeconfig.yaml b/controller/config/crd/kustomizeconfig.yaml deleted file mode 100644 index 6f83d9a94..000000000 --- a/controller/config/crd/kustomizeconfig.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# This file is for teaching kustomize how to substitute name and namespace reference in CRD -nameReference: -- kind: Service - version: v1 - fieldSpecs: - - kind: CustomResourceDefinition - group: apiextensions.k8s.io - path: spec/conversion/webhookClientConfig/service/name - -namespace: -- kind: CustomResourceDefinition - group: apiextensions.k8s.io - path: spec/conversion/webhookClientConfig/service/namespace - create: false - -varReference: -- path: metadata/annotations diff --git a/controller/config/crd/patches/cainjection_in_clusters.yaml b/controller/config/crd/patches/cainjection_in_clusters.yaml deleted file mode 100644 index 721215e87..000000000 --- a/controller/config/crd/patches/cainjection_in_clusters.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: clusters.deployments.plural.sh diff --git a/controller/config/crd/patches/cainjection_in_gitrepositories.yaml b/controller/config/crd/patches/cainjection_in_gitrepositories.yaml deleted file mode 100644 index e186bcee1..000000000 --- a/controller/config/crd/patches/cainjection_in_gitrepositories.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: gitrepositories.deployments.plural.sh diff --git a/controller/config/crd/patches/cainjection_in_providers.yaml b/controller/config/crd/patches/cainjection_in_providers.yaml deleted file mode 100644 index 0aebd5d03..000000000 --- a/controller/config/crd/patches/cainjection_in_providers.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: providers.deployments.plural.sh diff --git a/controller/config/crd/patches/cainjection_in_servicedeployments.yaml b/controller/config/crd/patches/cainjection_in_servicedeployments.yaml deleted file mode 100644 index c450a07d1..000000000 --- a/controller/config/crd/patches/cainjection_in_servicedeployments.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: servicedeployments.deployments.plural.sh diff --git a/controller/config/crd/patches/webhook_in_clusters.yaml b/controller/config/crd/patches/webhook_in_clusters.yaml deleted file mode 100644 index 7eed68f82..000000000 --- a/controller/config/crd/patches/webhook_in_clusters.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: clusters.deployments.plural.sh -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/controller/config/crd/patches/webhook_in_gitrepositories.yaml b/controller/config/crd/patches/webhook_in_gitrepositories.yaml deleted file mode 100644 index 389e275a0..000000000 --- a/controller/config/crd/patches/webhook_in_gitrepositories.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: gitrepositories.deployments.plural.sh -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/controller/config/crd/patches/webhook_in_providers.yaml b/controller/config/crd/patches/webhook_in_providers.yaml deleted file mode 100644 index d4f4e87d7..000000000 --- a/controller/config/crd/patches/webhook_in_providers.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: providers.deployments.plural.sh -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/controller/config/crd/patches/webhook_in_servicedeployments.yaml b/controller/config/crd/patches/webhook_in_servicedeployments.yaml deleted file mode 100644 index a4d02a673..000000000 --- a/controller/config/crd/patches/webhook_in_servicedeployments.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: servicedeployments.deployments.plural.sh -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/controller/config/default/kustomization.yaml b/controller/config/default/kustomization.yaml index 7fd4e0b0d..0c70eab2a 100644 --- a/controller/config/default/kustomization.yaml +++ b/controller/config/default/kustomization.yaml @@ -1,5 +1,5 @@ # Adds namespace to all resources. -namespace: test-system +namespace: plural-deployment-controller # Value of this field is prepended to the # names of all resources, e.g. a deployment named @@ -9,20 +9,16 @@ namespace: test-system namePrefix: test- # Labels to add to all resources and selectors. -#labels: -#- includeSelectors: true -# pairs: -# someName: someValue +labels: +- includeSelectors: true + pairs: + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + app.kubernetes.io/managed-by: kustomize resources: -#- ../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 @@ -31,112 +27,4 @@ patches: # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. - path: manager_auth_proxy_patch.yaml - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- path: manager_webhook_patch.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. -# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. -# 'CERTMANAGER' needs to be enabled to use ca injection -#- path: webhookcainjection_patch.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -# Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: -# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - source: # Add cert-manager annotation to the webhook Service -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true +- path: manager_config_patch.yaml diff --git a/controller/config/default/manager_auth_proxy_patch.yaml b/controller/config/default/manager_auth_proxy_patch.yaml index 70c3437f4..00221754f 100644 --- a/controller/config/default/manager_auth_proxy_patch.yaml +++ b/controller/config/default/manager_auth_proxy_patch.yaml @@ -1,10 +1,9 @@ -# This patch inject a sidecar container which is a HTTP proxy for the +# This patch injects a sidecar container which is am 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: diff --git a/controller/config/default/manager_config_patch.yaml b/controller/config/default/manager_config_patch.yaml index f6f589169..97aa3a112 100644 --- a/controller/config/default/manager_config_patch.yaml +++ b/controller/config/default/manager_config_patch.yaml @@ -2,9 +2,11 @@ apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager - namespace: system spec: template: spec: containers: - name: manager + image: deployment-controller:latest + imagePullPolicy: Never + diff --git a/controller/config/manager/kustomization.yaml b/controller/config/manager/kustomization.yaml index 5c5f0b84c..905de6d90 100644 --- a/controller/config/manager/kustomization.yaml +++ b/controller/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +- service_account.yaml + +secretGenerator: + - name: secrets + envs: [/tmp/config.env] + behavior: create diff --git a/controller/config/manager/manager.yaml b/controller/config/manager/manager.yaml index 917cdfc65..455760bcd 100644 --- a/controller/config/manager/manager.yaml +++ b/controller/config/manager/manager.yaml @@ -1,76 +1,38 @@ 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: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize - name: system + name: plural-deployment-controller --- 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/name: controller-manager app.kubernetes.io/component: manager - app.kubernetes.io/created-by: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: - control-plane: controller-manager + app.kubernetes.io/part-of: plural-deployment-controller replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: - control-plane: controller-manager + app.kubernetes.io/name: controller-manager + app.kubernetes.io/component: 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 + seccompProfile: + type: RuntimeDefault containers: - command: - /manager args: - --leader-elect - image: controller:latest + image: deployment-controller name: manager securityContext: allowPrivilegeEscalation: false diff --git a/controller/config/manager/service_account.yaml b/controller/config/manager/service_account.yaml new file mode 100644 index 000000000..69ece2e4c --- /dev/null +++ b/controller/config/manager/service_account.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager diff --git a/controller/config/prometheus/monitor.yaml b/controller/config/prometheus/monitor.yaml index 90e793f1e..d93d83c25 100644 --- a/controller/config/prometheus/monitor.yaml +++ b/controller/config/prometheus/monitor.yaml @@ -3,12 +3,9 @@ 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: test - app.kubernetes.io/part-of: test app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system @@ -22,4 +19,4 @@ spec: insecureSkipVerify: true selector: matchLabels: - control-plane: controller-manager + app.kubernetes.io/part-of: plural-deployment-controller diff --git a/controller/config/rbac/auth_proxy_client_clusterrole.yaml b/controller/config/rbac/auth_proxy_client_clusterrole.yaml deleted file mode 100644 index 8948dc18f..000000000 --- a/controller/config/rbac/auth_proxy_client_clusterrole.yaml +++ /dev/null @@ -1,16 +0,0 @@ -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: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize - name: metrics-reader -rules: -- nonResourceURLs: - - "/metrics" - verbs: - - get diff --git a/controller/config/rbac/auth_proxy_role.yaml b/controller/config/rbac/auth_proxy_role.yaml deleted file mode 100644 index 3e000420e..000000000 --- a/controller/config/rbac/auth_proxy_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -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: test - app.kubernetes.io/part-of: test - 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/controller/config/rbac/auth_proxy_role_binding.yaml b/controller/config/rbac/auth_proxy_role_binding.yaml deleted file mode 100644 index f7cf051f2..000000000 --- a/controller/config/rbac/auth_proxy_role_binding.yaml +++ /dev/null @@ -1,19 +0,0 @@ -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: test - app.kubernetes.io/part-of: test - 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/controller/config/rbac/auth_proxy_service.yaml b/controller/config/rbac/auth_proxy_service.yaml deleted file mode 100644 index e776faf8f..000000000 --- a/controller/config/rbac/auth_proxy_service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -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: test - app.kubernetes.io/part-of: test - 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/controller/config/rbac/kustomization.yaml b/controller/config/rbac/kustomization.yaml index 731832a6a..3ffdf5f31 100644 --- a/controller/config/rbac/kustomization.yaml +++ b/controller/config/rbac/kustomization.yaml @@ -4,15 +4,7 @@ resources: # 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/controller/config/rbac/leader_election_role.yaml b/controller/config/rbac/leader_election_role.yaml index 16c73fff6..4190ec805 100644 --- a/controller/config/rbac/leader_election_role.yaml +++ b/controller/config/rbac/leader_election_role.yaml @@ -2,13 +2,6 @@ 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: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: - apiGroups: diff --git a/controller/config/rbac/leader_election_role_binding.yaml b/controller/config/rbac/leader_election_role_binding.yaml index 93af1aa31..ef563e3af 100644 --- a/controller/config/rbac/leader_election_role_binding.yaml +++ b/controller/config/rbac/leader_election_role_binding.yaml @@ -1,13 +1,6 @@ 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: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io @@ -16,4 +9,4 @@ roleRef: subjects: - kind: ServiceAccount name: controller-manager - namespace: system + namespace: plural-deployment-controller diff --git a/controller/config/rbac/role.yaml b/controller/config/rbac/role.yaml index 09078d1bd..af402d1dc 100644 --- a/controller/config/rbac/role.yaml +++ b/controller/config/rbac/role.yaml @@ -4,12 +4,20 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list - apiGroups: - "" resources: - secrets verbs: - get + - list - apiGroups: - deployments.plural.sh resources: @@ -48,6 +56,12 @@ rules: - patch - update - watch +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories/finalizers + verbs: + - update - apiGroups: - deployments.plural.sh resources: @@ -94,6 +108,12 @@ rules: - patch - update - watch +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments/finalizers + verbs: + - update - apiGroups: - deployments.plural.sh resources: diff --git a/controller/config/rbac/role_binding.yaml b/controller/config/rbac/role_binding.yaml index 8cd4c5868..a316e37f1 100644 --- a/controller/config/rbac/role_binding.yaml +++ b/controller/config/rbac/role_binding.yaml @@ -1,13 +1,6 @@ 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: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io @@ -16,4 +9,4 @@ roleRef: subjects: - kind: ServiceAccount name: controller-manager - namespace: system + namespace: plural-deployment-controller diff --git a/controller/config/rbac/service_account.yaml b/controller/config/rbac/service_account.yaml deleted file mode 100644 index afc3eb17e..000000000 --- a/controller/config/rbac/service_account.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/name: serviceaccount - app.kubernetes.io/instance: controller-manager-sa - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: test - app.kubernetes.io/part-of: test - app.kubernetes.io/managed-by: kustomize - name: controller-manager - namespace: system diff --git a/controller/config/rbac/cluster_editor_role.yaml b/controller/config/rbac/user/cluster_editor_role.yaml similarity index 100% rename from controller/config/rbac/cluster_editor_role.yaml rename to controller/config/rbac/user/cluster_editor_role.yaml diff --git a/controller/config/rbac/cluster_viewer_role.yaml b/controller/config/rbac/user/cluster_viewer_role.yaml similarity index 100% rename from controller/config/rbac/cluster_viewer_role.yaml rename to controller/config/rbac/user/cluster_viewer_role.yaml diff --git a/controller/config/rbac/gitrepository_editor_role.yaml b/controller/config/rbac/user/gitrepository_editor_role.yaml similarity index 100% rename from controller/config/rbac/gitrepository_editor_role.yaml rename to controller/config/rbac/user/gitrepository_editor_role.yaml diff --git a/controller/config/rbac/gitrepository_viewer_role.yaml b/controller/config/rbac/user/gitrepository_viewer_role.yaml similarity index 100% rename from controller/config/rbac/gitrepository_viewer_role.yaml rename to controller/config/rbac/user/gitrepository_viewer_role.yaml diff --git a/controller/config/rbac/provider_editor_role.yaml b/controller/config/rbac/user/provider_editor_role.yaml similarity index 100% rename from controller/config/rbac/provider_editor_role.yaml rename to controller/config/rbac/user/provider_editor_role.yaml diff --git a/controller/config/rbac/provider_viewer_role.yaml b/controller/config/rbac/user/provider_viewer_role.yaml similarity index 100% rename from controller/config/rbac/provider_viewer_role.yaml rename to controller/config/rbac/user/provider_viewer_role.yaml diff --git a/controller/config/rbac/servicedeployment_editor_role.yaml b/controller/config/rbac/user/servicedeployment_editor_role.yaml similarity index 100% rename from controller/config/rbac/servicedeployment_editor_role.yaml rename to controller/config/rbac/user/servicedeployment_editor_role.yaml diff --git a/controller/config/rbac/servicedeployment_viewer_role.yaml b/controller/config/rbac/user/servicedeployment_viewer_role.yaml similarity index 100% rename from controller/config/rbac/servicedeployment_viewer_role.yaml rename to controller/config/rbac/user/servicedeployment_viewer_role.yaml diff --git a/controller/config/samples/deployments_v1alpha1_cluster.yaml b/controller/config/samples/deployments_v1alpha1_cluster.yaml deleted file mode 100644 index eba41d9fe..000000000 --- a/controller/config/samples/deployments_v1alpha1_cluster.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: deployments.plural.sh/v1alpha1 -kind: Cluster -metadata: - labels: - app.kubernetes.io/name: cluster - app.kubernetes.io/instance: cluster-sample - app.kubernetes.io/part-of: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: - name: cluster-sample -spec: - # TODO(user): Add fields here diff --git a/controller/config/samples/kustomization.yaml b/controller/config/samples/kustomization.yaml index 263935233..a46c3bb9d 100644 --- a/controller/config/samples/kustomization.yaml +++ b/controller/config/samples/kustomization.yaml @@ -1,4 +1,6 @@ ## Append samples of your project ## resources: -- deployments_v1alpha1_cluster.yaml -#+kubebuilder:scaffold:manifestskustomizesamples +- provider_aws_readonly.yaml +- git_repository.yaml +- cluster_byok.yaml +- service_deployment.yaml diff --git a/controller/hack/gen-client-mocks.sh b/controller/hack/gen-client-mocks.sh deleted file mode 100755 index 511a982df..000000000 --- a/controller/hack/gen-client-mocks.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# Exit on error -set -e - -cd $(dirname $0)/.. - -source hack/lib.sh - -CONTAINERIZE_IMAGE=golang:1.21.1 containerize ./hack/gen-client-mocks.sh - -go run github.com/vektra/mockery/v2@latest --dir=internal/client --name=ConsoleClient --output=internal/test/mocks \ No newline at end of file diff --git a/controller/hack/lib.sh b/controller/hack/lib.sh deleted file mode 100644 index 9bc3e8537..000000000 --- a/controller/hack/lib.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash - -echodate() { - # do not use -Is to keep this compatible with macOS - echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)]" "$@" -} - -containerize() { - local cmd="$1" - local image="${CONTAINERIZE_IMAGE:-golang:1.18.4}" - local gocache="${CONTAINERIZE_GOCACHE:-/tmp/.gocache}" - local gomodcache="${CONTAINERIZE_GOMODCACHE:-/tmp/.gomodcache}" - local skip="${NO_CONTAINERIZE:-}" - - # short-circuit containerize when in some cases it needs to be avoided - [ -n "$skip" ] && return - - if ! [ -f /.dockerenv ]; then - echodate "Running $cmd in a Docker container using $image..." - mkdir -p "$gocache" - mkdir -p "$gomodcache" - - exec docker run \ - -v "$PWD":/go/src/pluralsh/gqlclient \ - -v "$gocache":"$gocache" \ - -v "$gomodcache":"$gomodcache" \ - -w /go/src/pluralsh/gqlclient \ - -e "GOCACHE=$gocache" \ - -e "GOMODCACHE=$gomodcache" \ - -u "$(id -u):$(id -g)" \ - --entrypoint="$cmd" \ - --rm \ - -it \ - $image $@ - - exit $? - fi -} - -retry() { - # Works only with bash but doesn't fail on other shells - start_time=$(date +%s) - set +e - actual_retry $@ - rc=$? - return $rc -} -actual_retry() { - retries=$1 - shift - - count=0 - delay=1 - until "$@"; do - rc=$? - count=$((count + 1)) - if [ $count -lt "$retries" ]; then - echo "Retry $count/$retries exited $rc, retrying in $delay seconds..." > /dev/stderr - sleep $delay - else - echo "Retry $count/$retries exited $rc, no more retries left." > /dev/stderr - return $rc - fi - delay=$((delay * 2)) - done - return 0 -} diff --git a/controller/internal/controller/gitrepository_controller.go b/controller/internal/controller/gitrepository_controller.go index 2e0da315a..e49c9a270 100644 --- a/controller/internal/controller/gitrepository_controller.go +++ b/controller/internal/controller/gitrepository_controller.go @@ -5,10 +5,6 @@ import ( "fmt" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/internal/client" - "github.com/pluralsh/console/controller/internal/errors" - "github.com/pluralsh/console/controller/internal/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,6 +15,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pluralsh/console/controller/api/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/errors" + "github.com/pluralsh/console/controller/internal/utils" ) const ( @@ -43,8 +44,10 @@ type GitRepositoryReconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { logger := log.FromContext(ctx) diff --git a/controller/internal/controller/provider_controller.go b/controller/internal/controller/provider_controller.go index b11d03211..7116781c1 100644 --- a/controller/internal/controller/provider_controller.go +++ b/controller/internal/controller/provider_controller.go @@ -38,7 +38,7 @@ const ( // +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/status,verbs=get;update;patch // +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/finalizers,verbs=update -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the v1alpha1.Provider closer to the desired state diff --git a/controller/internal/controller/servicedeployment_controller.go b/controller/internal/controller/servicedeployment_controller.go index 9b5f48223..e478cd718 100644 --- a/controller/internal/controller/servicedeployment_controller.go +++ b/controller/internal/controller/servicedeployment_controller.go @@ -6,10 +6,6 @@ import ( "sort" console "github.com/pluralsh/console-client-go" - "github.com/pluralsh/console/controller/api/v1alpha1" - consoleclient "github.com/pluralsh/console/controller/internal/client" - "github.com/pluralsh/console/controller/internal/errors" - "github.com/pluralsh/console/controller/internal/utils" "github.com/pluralsh/polly/algorithms" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,6 +15,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/pluralsh/console/controller/api/v1alpha1" + consoleclient "github.com/pluralsh/console/controller/internal/client" + "github.com/pluralsh/console/controller/internal/errors" + "github.com/pluralsh/console/controller/internal/utils" ) const ( @@ -32,8 +33,11 @@ type ServiceReconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list +// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { logger := log.FromContext(ctx) diff --git a/controller/internal/controller/suite_test.go b/controller/internal/controller/suite_test.go index 5f4d17e2d..1f49bbbdd 100644 --- a/controller/internal/controller/suite_test.go +++ b/controller/internal/controller/suite_test.go @@ -33,7 +33,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" deploymentsv1alpha1 "github.com/pluralsh/console/controller/api/v1alpha1" - //+kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -75,8 +74,6 @@ var _ = BeforeSuite(func() { err = deploymentsv1alpha1.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()) From f680bb13502861a23f9d65da30a78ed6cf8baf4f Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 19 Dec 2023 13:13:52 +0100 Subject: [PATCH 171/198] use envsubst for kustomize --- controller/Makefile | 17 +++- controller/config/default/kustomization.yaml | 2 +- .../default/manager_auth_proxy_patch.yaml | 2 + .../config/default/manager_config_patch.yaml | 12 --- controller/config/manager/kustomization.yaml | 6 +- controller/config/manager/manager.yaml | 78 +++++++++++-------- controller/config/manager/secret.yaml | 7 ++ controller/go.mod | 2 +- controller/go.sum | 2 + controller/tools.go | 1 + 10 files changed, 74 insertions(+), 55 deletions(-) delete mode 100644 controller/config/default/manager_config_patch.yaml create mode 100644 controller/config/manager/secret.yaml diff --git a/controller/Makefile b/controller/Makefile index fd2b57207..d3a50fa11 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -12,6 +12,7 @@ CONTROLLER_GEN ?= $(shell which controller-gen) ENVTEST ?= $(shell which setup-envtest) GOLANGCI_LINT ?= $(shell which golangci-lint) MOCKERY ?= $(shell which mockery) +ENVSUBST ?= $(shell which envsubst) # Tool versions KUBEBUILDER_VERSION := 3.11.1 @@ -144,10 +145,14 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified $(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. - @echo asd > /tmp/config.env - $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - - @rm /tmp/config.env +deploy: manifests kustomize envsubst ## Deploy controller to the K8s cluster specified in ~/.kube/config. +ifndef PLURAL_CONSOLE_URL +$(error $$PLURAL_CONSOLE_URL environment variable not set) +endif +ifndef PLURAL_CONSOLE_TOKEN +$(error $$PLURAL_CONSOLE_TOKEN environment variable not set) +endif + $(KUSTOMIZE) build config/default | $(ENVSUBST) | $(KUBECTL) apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @@ -184,6 +189,10 @@ envtest: --tool ## Download and install setup-envtest in the $GOPATH/bin mockery: TOOL = mockery mockery: --tool +.PHONY: envsubst +envsubst: TOOL = envsubst +envsubst: --tool + .PHONY: kubebuilder kubebuilder: ## install kubebuilder @curl -L -O --output-dir bin/ "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_${OS}_${ARCH}" diff --git a/controller/config/default/kustomization.yaml b/controller/config/default/kustomization.yaml index 0c70eab2a..03df439e9 100644 --- a/controller/config/default/kustomization.yaml +++ b/controller/config/default/kustomization.yaml @@ -27,4 +27,4 @@ patches: # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. - path: manager_auth_proxy_patch.yaml -- path: manager_config_patch.yaml + diff --git a/controller/config/default/manager_auth_proxy_patch.yaml b/controller/config/default/manager_auth_proxy_patch.yaml index 00221754f..a6d369b35 100644 --- a/controller/config/default/manager_auth_proxy_patch.yaml +++ b/controller/config/default/manager_auth_proxy_patch.yaml @@ -36,3 +36,5 @@ spec: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" + - --console-url=$CONSOLE_URL + - --console-token=$CONSOLE_TOKEN diff --git a/controller/config/default/manager_config_patch.yaml b/controller/config/default/manager_config_patch.yaml deleted file mode 100644 index 97aa3a112..000000000 --- a/controller/config/default/manager_config_patch.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager -spec: - template: - spec: - containers: - - name: manager - image: deployment-controller:latest - imagePullPolicy: Never - diff --git a/controller/config/manager/kustomization.yaml b/controller/config/manager/kustomization.yaml index 905de6d90..84fa054a3 100644 --- a/controller/config/manager/kustomization.yaml +++ b/controller/config/manager/kustomization.yaml @@ -1,8 +1,4 @@ resources: - manager.yaml - service_account.yaml - -secretGenerator: - - name: secrets - envs: [/tmp/config.env] - behavior: create +- secret.yaml diff --git a/controller/config/manager/manager.yaml b/controller/config/manager/manager.yaml index 455760bcd..f00ce25f2 100644 --- a/controller/config/manager/manager.yaml +++ b/controller/config/manager/manager.yaml @@ -28,37 +28,51 @@ spec: seccompProfile: type: RuntimeDefault containers: - - command: - - /manager - args: - - --leader-elect - image: deployment-controller - 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 + - command: + - /manager + args: + - --leader-elect + - --console-url="$(CONSOLE_URL)" + - --console-token=$CONSOLE_TOKEN + image: deployment-controller:latest + imagePullPolicy: Never + name: manager + env: + - name: CONSOLE_URL + valueFrom: + secretKeyRef: + key: consoleUrl + name: secrets + - name: CONSOLE_TOKEN + valueFrom: + secretKeyRef: + key: consoleToken + name: secrets + 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/controller/config/manager/secret.yaml b/controller/config/manager/secret.yaml new file mode 100644 index 000000000..cf70fcb41 --- /dev/null +++ b/controller/config/manager/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secrets +stringData: + consoleUrl: "$PLURAL_CONSOLE_URL" # replaced with envsubst + consoleToken: "$PLURAL_CONSOLE_TOKEN" # replaced with envsubst diff --git a/controller/go.mod b/controller/go.mod index 18437859a..516becdbe 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -23,6 +23,7 @@ require ( // Tools require ( + github.com/a8m/envsubst v1.4.2 github.com/golangci/golangci-lint v1.55.2 github.com/vektra/mockery/v2 v2.38.0 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231215020716-1b80b9629af8 @@ -260,7 +261,6 @@ require github.com/onsi/ginkgo/v2 v2.13.1 require ( github.com/chigopher/pathlib v0.15.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect diff --git a/controller/go.sum b/controller/go.sum index 2c66d7e2a..873bea258 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -72,6 +72,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/ github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/Yamashou/gqlgenc v0.16.0 h1:k1X/dvwnkiDImaeYw+C1j+GDX3MnzB4aONOTE6Mrku4= github.com/Yamashou/gqlgenc v0.16.0/go.mod h1:yKaNzczoGrIElG3mG8j2Bg3imv4WyIjLSTRBtvhfMtU= +github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= +github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/go-check-sumtype v0.1.3 h1:M+tqMxB68hcgccRXBMVCPI4UJ+QUfdSx0xdbypKCqA8= diff --git a/controller/tools.go b/controller/tools.go index 5476335ec..f937deb5a 100644 --- a/controller/tools.go +++ b/controller/tools.go @@ -3,6 +3,7 @@ package tools import ( + _ "github.com/a8m/envsubst/cmd/envsubst" _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/vektra/mockery/v2" _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" From 0fba38fbe348b84124dfefd281049fd27c216afa Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 19 Dec 2023 13:30:53 +0100 Subject: [PATCH 172/198] update kustomize config to work with env var --- controller/config/default/manager_auth_proxy_patch.yaml | 4 ++-- controller/config/manager/manager.yaml | 4 ++-- controller/config/manager/secret.yaml | 4 ++-- controller/config/rbac/role.yaml | 2 ++ controller/internal/controller/gitrepository_controller.go | 2 +- controller/internal/controller/provider_controller.go | 2 +- .../internal/controller/servicedeployment_controller.go | 4 ++-- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/controller/config/default/manager_auth_proxy_patch.yaml b/controller/config/default/manager_auth_proxy_patch.yaml index a6d369b35..cfdbd98dc 100644 --- a/controller/config/default/manager_auth_proxy_patch.yaml +++ b/controller/config/default/manager_auth_proxy_patch.yaml @@ -36,5 +36,5 @@ spec: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" - - --console-url=$CONSOLE_URL - - --console-token=$CONSOLE_TOKEN + - --console-url=$(CONSOLE_URL) + - --console-token=$(CONSOLE_TOKEN) diff --git a/controller/config/manager/manager.yaml b/controller/config/manager/manager.yaml index f00ce25f2..fd7421548 100644 --- a/controller/config/manager/manager.yaml +++ b/controller/config/manager/manager.yaml @@ -32,8 +32,8 @@ spec: - /manager args: - --leader-elect - - --console-url="$(CONSOLE_URL)" - - --console-token=$CONSOLE_TOKEN + - --console-url=$(CONSOLE_URL) + - --console-token=$(CONSOLE_TOKEN) image: deployment-controller:latest imagePullPolicy: Never name: manager diff --git a/controller/config/manager/secret.yaml b/controller/config/manager/secret.yaml index cf70fcb41..deac172c2 100644 --- a/controller/config/manager/secret.yaml +++ b/controller/config/manager/secret.yaml @@ -3,5 +3,5 @@ kind: Secret metadata: name: secrets stringData: - consoleUrl: "$PLURAL_CONSOLE_URL" # replaced with envsubst - consoleToken: "$PLURAL_CONSOLE_TOKEN" # replaced with envsubst + consoleUrl: $PLURAL_CONSOLE_URL/gql # replaced with envsubst + consoleToken: $PLURAL_CONSOLE_TOKEN # replaced with envsubst diff --git a/controller/config/rbac/role.yaml b/controller/config/rbac/role.yaml index af402d1dc..0a521606c 100644 --- a/controller/config/rbac/role.yaml +++ b/controller/config/rbac/role.yaml @@ -11,6 +11,7 @@ rules: verbs: - get - list + - watch - apiGroups: - "" resources: @@ -18,6 +19,7 @@ rules: verbs: - get - list + - watch - apiGroups: - deployments.plural.sh resources: diff --git a/controller/internal/controller/gitrepository_controller.go b/controller/internal/controller/gitrepository_controller.go index e49c9a270..e2e1da847 100644 --- a/controller/internal/controller/gitrepository_controller.go +++ b/controller/internal/controller/gitrepository_controller.go @@ -47,7 +47,7 @@ type GitRepositoryReconciler struct { // +kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories/status,verbs=get;update;patch // +kubebuilder:rbac:groups=deployments.plural.sh,resources=gitrepositories/finalizers,verbs=update -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { logger := log.FromContext(ctx) diff --git a/controller/internal/controller/provider_controller.go b/controller/internal/controller/provider_controller.go index 7116781c1..795b57538 100644 --- a/controller/internal/controller/provider_controller.go +++ b/controller/internal/controller/provider_controller.go @@ -38,7 +38,7 @@ const ( // +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/status,verbs=get;update;patch // +kubebuilder:rbac:groups=deployments.plural.sh,resources=providers/finalizers,verbs=update -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the v1alpha1.Provider closer to the desired state diff --git a/controller/internal/controller/servicedeployment_controller.go b/controller/internal/controller/servicedeployment_controller.go index e478cd718..ee23c77e4 100644 --- a/controller/internal/controller/servicedeployment_controller.go +++ b/controller/internal/controller/servicedeployment_controller.go @@ -36,8 +36,8 @@ type ServiceReconciler struct { // +kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/status,verbs=get;update;patch // +kubebuilder:rbac:groups=deployments.plural.sh,resources=servicedeployments/finalizers,verbs=update -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list -// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { logger := log.FromContext(ctx) From 1f4efecf6fda13d231e5d3068a8d4166f87ff4f7 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 13:32:24 +0100 Subject: [PATCH 173/198] helmify --- charts/controller/.helmignore | 23 +++ charts/controller/Chart.yaml | 21 +++ charts/controller/templates/_helpers.tpl | 62 ++++++++ charts/controller/templates/deployment.yaml | 86 +++++++++++ .../templates/leader-election-rbac.yaml | 55 +++++++ charts/controller/templates/manager-rbac.yaml | 142 ++++++++++++++++++ charts/controller/templates/secrets.yaml | 12 ++ .../controller/templates/serviceaccount.yaml | 9 ++ charts/controller/values.yaml | 52 +++++++ controller/Makefile | 22 ++- controller/go.mod | 4 +- controller/go.sum | 90 ++++++++++- controller/tools.go | 1 + 13 files changed, 569 insertions(+), 10 deletions(-) create mode 100644 charts/controller/.helmignore create mode 100644 charts/controller/Chart.yaml create mode 100644 charts/controller/templates/_helpers.tpl create mode 100644 charts/controller/templates/deployment.yaml create mode 100644 charts/controller/templates/leader-election-rbac.yaml create mode 100644 charts/controller/templates/manager-rbac.yaml create mode 100644 charts/controller/templates/secrets.yaml create mode 100644 charts/controller/templates/serviceaccount.yaml create mode 100644 charts/controller/values.yaml diff --git a/charts/controller/.helmignore b/charts/controller/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/controller/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/controller/Chart.yaml b/charts/controller/Chart.yaml new file mode 100644 index 000000000..36c1185d5 --- /dev/null +++ b/charts/controller/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: controller +description: A Helm chart for Kubernetes +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/charts/controller/templates/_helpers.tpl b/charts/controller/templates/_helpers.tpl new file mode 100644 index 000000000..0aebd2c20 --- /dev/null +++ b/charts/controller/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "controller.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "controller.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "controller.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "controller.labels" -}} +helm.sh/chart: {{ include "controller.chart" . }} +{{ include "controller.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "controller.selectorLabels" -}} +app.kubernetes.io/name: {{ include "controller.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "controller.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "controller.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/controller/templates/deployment.yaml b/charts/controller/templates/deployment.yaml new file mode 100644 index 000000000..8f6a8a046 --- /dev/null +++ b/charts/controller/templates/deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "controller.fullname" . }}-controller-manager + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.controllerManager.replicas }} + selector: + matchLabels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + {{- include "controller.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: controller-manager + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + {{- include "controller.selectorLabels" . | nindent 8 }} + annotations: + kubectl.kubernetes.io/default-container: manager + spec: + containers: + - args: {{- toYaml .Values.controllerManager.kubeRbacProxy.args | nindent 8 }} + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag + | default .Chart.AppVersion }} + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent + 10 }} + securityContext: {{- toYaml .Values.controllerManager.kubeRbacProxy.containerSecurityContext + | nindent 10 }} + - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + command: + - /manager + env: + - name: CONSOLE_URL + valueFrom: + secretKeyRef: + key: consoleUrl + name: {{ include "controller.fullname" . }}-secrets + - name: CONSOLE_TOKEN + valueFrom: + secretKeyRef: + key: consoleToken + name: {{ include "controller.fullname" . }}-secrets + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag + | default .Chart.AppVersion }} + imagePullPolicy: {{ .Values.controllerManager.manager.imagePullPolicy }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 + }} + securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext + | nindent 10 }} + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: {{ include "controller.fullname" . }}-controller-manager + terminationGracePeriodSeconds: 10 \ No newline at end of file diff --git a/charts/controller/templates/leader-election-rbac.yaml b/charts/controller/templates/leader-election-rbac.yaml new file mode 100644 index 000000000..4702432dd --- /dev/null +++ b/charts/controller/templates/leader-election-rbac.yaml @@ -0,0 +1,55 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "controller.fullname" . }}-leader-election-role + labels: + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "controller.fullname" . }}-leader-election-rolebinding + labels: + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: '{{ include "controller.fullname" . }}-leader-election-role' +subjects: +- kind: ServiceAccount + name: '{{ include "controller.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/controller/templates/manager-rbac.yaml b/charts/controller/templates/manager-rbac.yaml new file mode 100644 index 000000000..c322c30bb --- /dev/null +++ b/charts/controller/templates/manager-rbac.yaml @@ -0,0 +1,142 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "controller.fullname" . }}-manager-role + labels: + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list +- apiGroups: + - deployments.plural.sh + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - clusters/finalizers + verbs: + - update +- apiGroups: + - deployments.plural.sh + resources: + - clusters/status + verbs: + - get + - patch + - update +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories/finalizers + verbs: + - update +- apiGroups: + - deployments.plural.sh + resources: + - gitrepositories/status + verbs: + - get + - patch + - update +- apiGroups: + - deployments.plural.sh + resources: + - providers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - providers/finalizers + verbs: + - update +- apiGroups: + - deployments.plural.sh + resources: + - providers/status + verbs: + - get + - patch + - update +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments/finalizers + verbs: + - update +- apiGroups: + - deployments.plural.sh + resources: + - servicedeployments/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "controller.fullname" . }}-manager-rolebinding + labels: + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "controller.fullname" . }}-manager-role' +subjects: +- kind: ServiceAccount + name: '{{ include "controller.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/controller/templates/secrets.yaml b/charts/controller/templates/secrets.yaml new file mode 100644 index 000000000..4d84cb6ea --- /dev/null +++ b/charts/controller/templates/secrets.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "controller.fullname" . }}-secrets + labels: + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} +stringData: + consoleToken: {{ required "secrets.consoleToken is required" .Values.secrets.consoleToken + | quote }} + consoleUrl: {{ required "secrets.consoleUrl is required" .Values.secrets.consoleUrl + | quote }} \ No newline at end of file diff --git a/charts/controller/templates/serviceaccount.yaml b/charts/controller/templates/serviceaccount.yaml new file mode 100644 index 000000000..5014b3d88 --- /dev/null +++ b/charts/controller/templates/serviceaccount.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "controller.fullname" . }}-controller-manager + labels: + app.kubernetes.io/part-of: plural-deployment-controller + {{- include "controller.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }} \ No newline at end of file diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml new file mode 100644 index 000000000..adf89e7cd --- /dev/null +++ b/charts/controller/values.yaml @@ -0,0 +1,52 @@ +controllerManager: + kubeRbacProxy: + args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: + repository: gcr.io/kubebuilder/kube-rbac-proxy + tag: v0.15.0 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + manager: + args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + - --console-url=$CONSOLE_URL + - --console-token=$CONSOLE_TOKEN + containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: + repository: deployment-controller + tag: latest + imagePullPolicy: Never + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + replicas: 1 + serviceAccount: + annotations: {} +kubernetesClusterDomain: cluster.local +secrets: + consoleToken: "" + consoleUrl: "" diff --git a/controller/Makefile b/controller/Makefile index d3a50fa11..e704a95b2 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -8,6 +8,7 @@ IMG ?= deployment-controller:latest # Tool binaries KUBECTL ?= $(shell which kubectl) KUSTOMIZE ?= $(shell which kustomize) +HELMIFY ?= $(shell which helmify) CONTROLLER_GEN ?= $(shell which controller-gen) ENVTEST ?= $(shell which setup-envtest) GOLANGCI_LINT ?= $(shell which golangci-lint) @@ -136,6 +137,11 @@ ifndef ignore-not-found ignore-not-found = false endif +.PHONY: helm +helm: manifests kustomize helmify + $(KUSTOMIZE) build config/default | $(HELMIFY) controller + @mv controller ../charts + .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - @@ -146,12 +152,12 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize envsubst ## Deploy controller to the K8s cluster specified in ~/.kube/config. -ifndef PLURAL_CONSOLE_URL -$(error $$PLURAL_CONSOLE_URL environment variable not set) -endif -ifndef PLURAL_CONSOLE_TOKEN -$(error $$PLURAL_CONSOLE_TOKEN environment variable not set) -endif + ifndef PLURAL_CONSOLE_URL + $(error $$PLURAL_CONSOLE_URL environment variable not set) + endif + ifndef PLURAL_CONSOLE_TOKEN + $(error $$PLURAL_CONSOLE_TOKEN environment variable not set) + endif $(KUSTOMIZE) build config/default | $(ENVSUBST) | $(KUBECTL) apply -f - .PHONY: undeploy @@ -177,6 +183,10 @@ controller-gen: --tool ## Download and install controller-gen in the $GOPATH/bin kustomize: TOOL = kustomize kustomize: --tool ## Download and install kustomize in the $GOPATH/bin +.PHONY: helmify +helmify: TOOL = helmify +helmify: --tool ## Download and install helmify in the $GOPATH/bin + .PHONY: golangci-lint golangci-lint: TOOL = golangci-lint golangci-lint: --tool ## Download and install golangci-lint in the $GOPATH/bin diff --git a/controller/go.mod b/controller/go.mod index 516becdbe..1af69afb8 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -24,6 +24,7 @@ require ( // Tools require ( github.com/a8m/envsubst v1.4.2 + github.com/arttor/helmify v0.4.10 github.com/golangci/golangci-lint v1.55.2 github.com/vektra/mockery/v2 v2.38.0 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231215020716-1b80b9629af8 @@ -123,7 +124,7 @@ require ( github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/imdario/mergo v0.3.13 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jgautheron/goconst v1.6.0 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect @@ -260,6 +261,7 @@ require ( require github.com/onsi/ginkgo/v2 v2.13.1 require ( + dario.cat/mergo v1.0.0 // indirect github.com/chigopher/pathlib v0.15.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect diff --git a/controller/go.sum b/controller/go.sum index 873bea258..aa8683f71 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -39,6 +39,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl 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= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.3.3 h1:ZsOxcwGD/jP4U/aw7qeWu58i7dwYemfy5Y+IF1ACoNw= github.com/4meepo/tagalign v1.3.3/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= @@ -50,6 +52,8 @@ github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTo github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= github.com/Antonboom/testifylint v0.2.3 h1:MFq9zyL+rIVpsvLX4vDPLojgN7qODzWsrnftNX2Qh60= github.com/Antonboom/testifylint v0.2.3/go.mod h1:IYaXaOX9NbfAyO+Y04nfjGI8wDemC1rUyM/cYolz018= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -68,6 +72,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= +github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/Yamashou/gqlgenc v0.16.0 h1:k1X/dvwnkiDImaeYw+C1j+GDX3MnzB4aONOTE6Mrku4= @@ -95,6 +101,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/arttor/helmify v0.4.10 h1:NTRXZraSpyNiQ5pHH6usugyHo4rofbmTZRFekeB1Uq0= +github.com/arttor/helmify v0.4.10/go.mod h1:jHLtbiOoxmUIIZYLGtGnZWAzjhR6NkMPED/WA2loCVc= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= @@ -133,6 +141,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= @@ -146,6 +156,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/containerd v1.6.15 h1:4wWexxzLNHNE46aIETc6ge4TofO550v+BlLoANrbses= +github.com/containerd/containerd v1.6.15/go.mod h1:U2NnBPIhzJDm59xF7xB2MMHnKtggpZ+phKg8o2TKj2c= github.com/coredns/caddy v1.1.0 h1:ezvsPrT/tA/7pYDBZxu0cT0VmWk75AfIaf6GSYCNMf0= github.com/coredns/caddy v1.1.0/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= github.com/coredns/corefile-migration v1.0.21 h1:W/DCETrHDiFo0Wj03EyMkaQ9fwsmSgqTCQDHpceaSsE= @@ -155,6 +167,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/daixiang0/gci v0.11.2 h1:Oji+oPsp3bQ6bNNgX30NBAVT18P4uBH4sRZnlOlTj7Y= github.com/daixiang0/gci v0.11.2/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -165,6 +179,20 @@ github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20 github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= +github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= +github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -181,6 +209,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= @@ -202,6 +232,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop 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-gorp/gorp/v3 v3.0.5 h1:PUjzYdYu3HBOh8LE+UUmRG2P0IRDak9XMeGNvaeq4Ow= +github.com/go-gorp/gorp/v3 v3.0.5/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= 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= @@ -306,6 +338,8 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSW github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.16.1 h1:3hZfSNiAU3KOiNtxuFXVp5WFy4hf/Ly3Sa4/7F8SXNo= github.com/google/cel-go v0.16.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -354,6 +388,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8= github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= @@ -366,6 +402,10 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -385,8 +425,8 @@ github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHL github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jgautheron/goconst v1.6.0 h1:gbMLWKRMkzAc6kYsQL6/TxaoBUg3Jm9LSF/Ih1ADWGA= @@ -397,6 +437,8 @@ github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -419,6 +461,8 @@ github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -437,12 +481,20 @@ github.com/kunwardeep/paralleltest v1.0.8 h1:Ul2KsqtzFxTlSU7IP0JusWlLiNqQaloB9vg github.com/kunwardeep/paralleltest v1.0.8/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/macabu/inamedparam v0.1.2 h1:RR5cnayM6Q7cDhQol32DE2BGAPGMnffJ31LFE+UklaU= @@ -483,10 +535,18 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -498,6 +558,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -522,6 +584,8 @@ github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc= github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= @@ -531,6 +595,8 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9 github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -588,6 +654,9 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/rubenv/sql-migrate v1.3.1 h1:Vx+n4Du8X8VTYuXbhNxdEUoh6wiJERA0GlWocR5FrbA= +github.com/rubenv/sql-migrate v1.3.1/go.mod h1:YzG/Vh82CwyhTFXy+Mf5ahAiiEOpAlHurg+23VEzcsk= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= @@ -696,6 +765,12 @@ github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zs github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/vektra/mockery/v2 v2.38.0 h1:I0LBuUzZHqAU4d1DknW0DTFBPO6n8TaD38WL2KJf3yI= github.com/vektra/mockery/v2 v2.38.0/go.mod h1:diB13hxXG6QrTR0ol2Rk8s2dRMftzvExSvPDKr+IYKk= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= @@ -1124,6 +1199,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 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= @@ -1162,9 +1239,10 @@ 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/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= +helm.sh/helm/v3 v3.11.2 h1:P3cLaFxfoxaGLGJVnoPrhf1j86LC5EDINSpYSpMUkkA= +helm.sh/helm/v3 v3.11.2/go.mod h1:Hw+09mfpDiRRKAgAIZlFkPSeOkvv7Acl5McBvQyNPVw= 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= @@ -1182,6 +1260,8 @@ k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg= k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w= +k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q= +k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k= k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q= @@ -1194,6 +1274,8 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= +k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= @@ -1204,6 +1286,8 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d h1:3rvTIIM22r9pvXk+q3swxUQAQOxksVMGK7sml4nG57w= mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RFiZMMsLVL+A96Nvptar8Fj71is= +oras.land/oras-go v1.2.2 h1:0E9tOHUfrNH7TCDk5KU0jVBEzCqbfdyuVfGmJ7ZeRPE= +oras.land/oras-go v1.2.2/go.mod h1:Apa81sKoZPpP7CDciE006tSZ0x3Q3+dOoBcMZ/aNxvw= 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= diff --git a/controller/tools.go b/controller/tools.go index f937deb5a..120f56838 100644 --- a/controller/tools.go +++ b/controller/tools.go @@ -4,6 +4,7 @@ package tools import ( _ "github.com/a8m/envsubst/cmd/envsubst" + _ "github.com/arttor/helmify/cmd/helmify" _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/vektra/mockery/v2" _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" From f7d66a8d6af8ba906b8695101473aec0f05077b0 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 13:33:23 +0100 Subject: [PATCH 174/198] helmify --- charts/controller/templates/manager-rbac.yaml | 2 ++ charts/controller/values.yaml | 4 ++-- controller/Makefile | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/charts/controller/templates/manager-rbac.yaml b/charts/controller/templates/manager-rbac.yaml index c322c30bb..d34cc573f 100644 --- a/charts/controller/templates/manager-rbac.yaml +++ b/charts/controller/templates/manager-rbac.yaml @@ -13,6 +13,7 @@ rules: verbs: - get - list + - watch - apiGroups: - "" resources: @@ -20,6 +21,7 @@ rules: verbs: - get - list + - watch - apiGroups: - deployments.plural.sh resources: diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml index adf89e7cd..b7b6e668f 100644 --- a/charts/controller/values.yaml +++ b/charts/controller/values.yaml @@ -25,8 +25,8 @@ controllerManager: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 - --leader-elect - - --console-url=$CONSOLE_URL - - --console-token=$CONSOLE_TOKEN + - --console-url=$(CONSOLE_URL) + - --console-token=$(CONSOLE_TOKEN) containerSecurityContext: allowPrivilegeEscalation: false capabilities: diff --git a/controller/Makefile b/controller/Makefile index e704a95b2..0832d5ad6 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -139,6 +139,7 @@ endif .PHONY: helm helm: manifests kustomize helmify + @rm -rf ../charts/controller $(KUSTOMIZE) build config/default | $(HELMIFY) controller @mv controller ../charts From 914fc77be31f33340e95b23d7e543a16f9e96945 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 13:45:30 +0100 Subject: [PATCH 175/198] update helmify target --- controller/Makefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 0832d5ad6..c6cf95baf 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -139,9 +139,7 @@ endif .PHONY: helm helm: manifests kustomize helmify - @rm -rf ../charts/controller - $(KUSTOMIZE) build config/default | $(HELMIFY) controller - @mv controller ../charts + @cd ../charts && $(KUSTOMIZE) build ../controller/config/default | $(HELMIFY) controller .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. From 5a7af27d642daf79882f9116ddf1fc15faae4b3d Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 13:51:56 +0100 Subject: [PATCH 176/198] helmify update --- charts/controller/crds/cluster-crd.yaml | 380 +++++++++++++++ charts/controller/crds/gitrepository-crd.yaml | 163 +++++++ charts/controller/crds/provider-crd.yaml | 220 +++++++++ .../crds/servicedeployment-crd.yaml | 434 ++++++++++++++++++ charts/controller/templates/deployment.yaml | 1 + charts/controller/values.yaml | 1 + controller/Makefile | 2 +- controller/config/default/kustomization.yaml | 1 + 8 files changed, 1201 insertions(+), 1 deletion(-) create mode 100644 charts/controller/crds/cluster-crd.yaml create mode 100644 charts/controller/crds/gitrepository-crd.yaml create mode 100644 charts/controller/crds/provider-crd.yaml create mode 100644 charts/controller/crds/servicedeployment-crd.yaml diff --git a/charts/controller/crds/cluster-crd.yaml b/charts/controller/crds/cluster-crd.yaml new file mode 100644 index 000000000..954b8097d --- /dev/null +++ b/charts/controller/crds/cluster-crd.yaml @@ -0,0 +1,380 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + name: clusters.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: Cluster + listKind: ClusterList + plural: clusters + singular: cluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Current Kubernetes version + jsonPath: .status.currentVersion + name: CurrentVersion + type: string + - description: Console ID + jsonPath: .status.id + name: Id + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + bindings: + description: Bindings contain read and write policies of this cluster + properties: + read: + description: Read bindings. + items: + properties: + UserID: + type: string + groupID: + type: string + id: + type: string + type: object + type: array + write: + description: Write bindings. + items: + properties: + UserID: + type: string + groupID: + type: string + id: + type: string + type: object + type: array + type: object + x-kubernetes-validations: + - message: Bindings are immutable + rule: self == oldSelf + cloud: + description: Cloud provider to use for this cluster. + enum: + - aws + - azure + - gcp + - byok + example: azure + type: string + x-kubernetes-validations: + - message: Cloud is immutable + rule: self == oldSelf + cloudSettings: + description: CloudSettings contains cloud-specific settings for this + cluster. + properties: + aws: + description: AWS cluster customizations. + properties: + region: + description: Region in AWS to deploy this cluster to. + type: string + required: + - region + type: object + azure: + description: Azure cluster customizations. + properties: + location: + description: Location in Azure to deploy this cluster to. + example: eastus + type: string + network: + description: Network is a name for the Azure virtual network + for this cluster. + example: mynetwork + type: string + resourceGroup: + description: ResourceGroup is a name for the Azure resource + group for this cluster. + example: myresourcegroup + type: string + subscriptionId: + description: SubscriptionId is GUID of the Azure subscription + to hold this cluster. + type: string + required: + - location + - network + - resourceGroup + - subscriptionId + type: object + gcp: + description: GCP cluster customizations. + properties: + network: + description: Network in GCP to use when creating the cluster. + type: string + project: + description: Project in GCP to deploy cluster to. + type: string + region: + description: Region in GCP to deploy cluster to. + type: string + required: + - network + - project + - region + type: object + type: object + x-kubernetes-map-type: atomic + handle: + description: Handle is a short, unique human-readable name used to + identify this cluster. Does not necessarily map to the cloud resource + name. This has to be specified in order to adopt existing cluster. + example: myclusterhandle + type: string + nodePools: + description: NodePools contains specs of node pools managed by this + cluster. + items: + properties: + cloudSettings: + description: CloudSettings contains cloud-specific settings + for this node pool. + properties: + aws: + description: AWS node pool customizations. + properties: + launchTemplateId: + description: LaunchTemplateId is an ID of custom launch + template for your nodes. Useful for Golden AMI setups. + type: string + type: object + type: object + x-kubernetes-map-type: atomic + instanceType: + description: InstanceType contains the type of node to use. + Usually cloud-specific. + type: string + labels: + additionalProperties: + type: string + description: Labels to apply to the nodes in this pool. Useful + for node selectors. + type: object + maxSize: + description: MaxSize is maximum number of instances in this + node pool. + format: int64 + minimum: 1 + type: integer + minSize: + description: MinSize is minimum number of instances in this + node pool. + format: int64 + minimum: 1 + type: integer + name: + description: Name of the node pool. Must be unique. + type: string + taints: + description: Taints you'd want to apply to a node, i.e. for + preventing scheduling on spot instances. + items: + description: Taint represents a Kubernetes taint. + properties: + effect: + description: Effect specifies the effect for the taint. + enum: + - NoSchedule + - NoExecute + - PreferNoSchedule + type: string + key: + description: Key is the key of the taint. + type: string + value: + description: Value is the value of the taint. + type: string + required: + - effect + - key + - value + type: object + type: array + required: + - instanceType + - maxSize + - minSize + - name + type: object + type: array + protect: + description: Protect cluster from being deleted. + example: false + type: boolean + providerRef: + description: ProviderRef references provider to use for this cluster. + Can be skipped only for BYOK. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + tags: + additionalProperties: + type: string + description: Tags used to filter clusters. + type: object + version: + description: Version of Kubernetes to use for this cluster. Can be + skipped only for BYOK. + example: 1.25.11 + type: string + type: object + status: + properties: + conditions: + description: Represents the observations of a Cluster current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentVersion: + description: CurrentVersion contains current Kubernetes version this + cluster is using. + type: string + id: + description: ID from Console. + type: string + kasURL: + description: KasURL contains KAS URL. + type: string + pingedAt: + description: PingedAt contains timestamp of last successful cluster + ping. + type: string + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/controller/crds/gitrepository-crd.yaml b/charts/controller/crds/gitrepository-crd.yaml new file mode 100644 index 000000000..6e69de412 --- /dev/null +++ b/charts/controller/crds/gitrepository-crd.yaml @@ -0,0 +1,163 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + name: gitrepositories.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: GitRepository + listKind: GitRepositoryList + plural: gitrepositories + singular: gitrepository + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Repo health status + jsonPath: .status.health + name: Health + type: string + - description: Console repo Id + jsonPath: .status.id + name: Id + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + credentialsRef: + description: CredentialsRef is a secret reference which should contain + privateKey, passphrase, username and password. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + type: string + x-kubernetes-validations: + - message: Url is immutable + rule: self == oldSelf + required: + - url + type: object + status: + properties: + conditions: + description: Represents the observations of Repository current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + health: + description: Health status. + enum: + - PULLABLE + - FAILED + type: string + id: + description: ID of the provider in the Console API. + type: string + message: + description: Message indicating details about last transition. + type: string + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/controller/crds/provider-crd.yaml b/charts/controller/crds/provider-crd.yaml new file mode 100644 index 000000000..898ada099 --- /dev/null +++ b/charts/controller/crds/provider-crd.yaml @@ -0,0 +1,220 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + name: providers.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: Provider + listKind: ProviderList + plural: providers + singular: provider + scope: Cluster + versions: + - additionalPrinterColumns: + - description: ID of the provider in the Console API. + jsonPath: .status.id + name: ID + type: string + - description: Human-readable name of the Provider. + jsonPath: .spec.name + name: Name + type: string + - description: Name of the Provider cloud service. + jsonPath: .spec.cloud + name: Cloud + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Provider ... + 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: ProviderSpec ... + properties: + cloud: + description: 'Cloud is the name of the cloud service for the Provider. + One of (CloudProvider): [gcp, aws, azure]' + enum: + - gcp + - aws + - azure + example: aws + type: string + x-kubernetes-validations: + - message: Cloud is immutable + rule: self == oldSelf + cloudSettings: + description: CloudSettings reference cloud provider credentials secrets + used for provisioning the Cluster. Not required when Cloud is set + to CloudProvider(BYOK). + properties: + aws: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + azure: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + gcp: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-map-type: atomic + name: + description: Name is a human-readable name of the Provider. + example: gcp-provider + type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf + namespace: + description: Namespace is the namespace ClusterAPI resources are deployed + into. + example: capi-gcp + type: string + x-kubernetes-validations: + - message: Namespace is immutable + rule: self == oldSelf + required: + - cloud + type: object + status: + description: ProviderStatus ... + properties: + conditions: + description: Represents the observations of a Provider's current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: ID of the provider in the Console API. + type: string + sha: + description: SHA of last applied configuration. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/controller/crds/servicedeployment-crd.yaml b/charts/controller/crds/servicedeployment-crd.yaml new file mode 100644 index 000000000..2b5828f39 --- /dev/null +++ b/charts/controller/crds/servicedeployment-crd.yaml @@ -0,0 +1,434 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: plural-deployment-controller + app.kubernetes.io/version: dev + name: servicedeployments.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: ServiceDeployment + listKind: ServiceDeploymentList + plural: servicedeployments + singular: servicedeployment + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Console repo Id + jsonPath: .status.id + name: Id + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + bindings: + description: Bindings contain read and write policies of this cluster + properties: + read: + description: Read bindings. + items: + properties: + UserID: + type: string + groupID: + type: string + id: + type: string + type: object + type: array + write: + description: Write bindings. + items: + properties: + UserID: + type: string + groupID: + type: string + id: + type: string + type: object + type: array + type: object + clusterRef: + description: "ObjectReference contains enough information to let you + inspect or modify the referred object. --- New uses of this type + are discouraged because of difficulty describing its usage when + embedded in APIs. 1. Ignored fields. It includes many fields which + are not generally honored. For instance, ResourceVersion and FieldPath + are both very rarely valid in actual usage. 2. Invalid usage help. + \ It is impossible to add specific help for individual usage. In + most embedded usages, there are particular restrictions like, \"must + refer only to types A and B\" or \"UID not honored\" or \"name must + be restricted\". Those cannot be well described when embedded. 3. + Inconsistent validation. Because the usages are different, the + validation rules are different by usage, which makes it hard for + users to predict what will happen. 4. The fields are both imprecise + and overly precise. Kind is not a precise mapping to a URL. This + can produce ambiguity during interpretation and require a REST mapping. + \ In most cases, the dependency is on the group,resource tuple and + the version of the actual struct is irrelevant. 5. We cannot easily + change it. Because this type is embedded in many locations, updates + to this type will affect numerous schemas. Don't make new APIs + embed an underspecified API type they do not control. \n Instead + of using this type, create a locally provided and used type that + is well-focused on your reference. For example, ServiceReferences + for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + ." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: Cluster is immutable + rule: self == oldSelf + configurationRef: + description: ConfigurationRef is a secret reference which should contain + service configuration. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + docsPath: + type: string + git: + properties: + folder: + type: string + ref: + type: string + required: + - folder + - ref + type: object + helm: + properties: + chart: + description: Selects a key from a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + repository: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + values: + description: Selects a key from a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + valuesFiles: + items: + type: string + type: array + version: + type: string + type: object + kustomize: + properties: + path: + type: string + required: + - path + type: object + protect: + type: boolean + repositoryRef: + description: "ObjectReference contains enough information to let you + inspect or modify the referred object. --- New uses of this type + are discouraged because of difficulty describing its usage when + embedded in APIs. 1. Ignored fields. It includes many fields which + are not generally honored. For instance, ResourceVersion and FieldPath + are both very rarely valid in actual usage. 2. Invalid usage help. + \ It is impossible to add specific help for individual usage. In + most embedded usages, there are particular restrictions like, \"must + refer only to types A and B\" or \"UID not honored\" or \"name must + be restricted\". Those cannot be well described when embedded. 3. + Inconsistent validation. Because the usages are different, the + validation rules are different by usage, which makes it hard for + users to predict what will happen. 4. The fields are both imprecise + and overly precise. Kind is not a precise mapping to a URL. This + can produce ambiguity during interpretation and require a REST mapping. + \ In most cases, the dependency is on the group,resource tuple and + the version of the actual struct is irrelevant. 5. We cannot easily + change it. Because this type is embedded in many locations, updates + to this type will affect numerous schemas. Don't make new APIs + embed an underspecified API type they do not control. \n Instead + of using this type, create a locally provided and used type that + is well-focused on your reference. For example, ServiceReferences + for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + ." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: Repository is immutable + rule: self == oldSelf + syncConfig: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + version: + default: '''0.0.1''' + type: string + required: + - clusterRef + - repositoryRef + - version + type: object + status: + properties: + components: + items: + properties: + group: + type: string + id: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + state: + description: State specifies the component state + enum: + - RUNNING + - PENDING + - FAILED + type: string + synced: + type: boolean + version: + type: string + required: + - id + - kind + - name + - synced + type: object + type: array + conditions: + description: Represents the observations of Repository current state. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + errors: + items: + properties: + message: + type: string + source: + type: string + required: + - message + - source + type: object + type: array + id: + description: ID of the provider in the Console API. + type: string + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/controller/templates/deployment.yaml b/charts/controller/templates/deployment.yaml index 8f6a8a046..8424a5277 100644 --- a/charts/controller/templates/deployment.yaml +++ b/charts/controller/templates/deployment.yaml @@ -78,6 +78,7 @@ spec: }} securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext | nindent 10 }} + imagePullSecrets: {{ .Values.imagePullSecrets | default list | toJson }} securityContext: runAsNonRoot: true seccompProfile: diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml index b7b6e668f..77d7d79c2 100644 --- a/charts/controller/values.yaml +++ b/charts/controller/values.yaml @@ -46,6 +46,7 @@ controllerManager: replicas: 1 serviceAccount: annotations: {} +imagePullSecrets: [] kubernetesClusterDomain: cluster.local secrets: consoleToken: "" diff --git a/controller/Makefile b/controller/Makefile index c6cf95baf..a04056da3 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -139,7 +139,7 @@ endif .PHONY: helm helm: manifests kustomize helmify - @cd ../charts && $(KUSTOMIZE) build ../controller/config/default | $(HELMIFY) controller + $(KUSTOMIZE) build config/default | $(HELMIFY) -generate-defaults -image-pull-secrets -crd-dir ../charts/controller .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. diff --git a/controller/config/default/kustomization.yaml b/controller/config/default/kustomization.yaml index 03df439e9..cc062df18 100644 --- a/controller/config/default/kustomization.yaml +++ b/controller/config/default/kustomization.yaml @@ -19,6 +19,7 @@ labels: resources: - ../rbac - ../manager +- ../crd # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus From f20683ccf855168b39066e86289387260088b48f Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Tue, 19 Dec 2023 14:07:46 +0100 Subject: [PATCH 177/198] helmify update --- charts/controller/crds/cluster-crd.yaml | 2 +- charts/controller/crds/gitrepository-crd.yaml | 2 +- charts/controller/crds/provider-crd.yaml | 2 +- charts/controller/crds/servicedeployment-crd.yaml | 2 +- charts/controller/templates/deployment.yaml | 4 ++-- controller/Makefile | 1 + 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/charts/controller/crds/cluster-crd.yaml b/charts/controller/crds/cluster-crd.yaml index 954b8097d..1c6c92f9d 100644 --- a/charts/controller/crds/cluster-crd.yaml +++ b/charts/controller/crds/cluster-crd.yaml @@ -4,7 +4,7 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 labels: - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/managed-by: helm app.kubernetes.io/part-of: plural-deployment-controller app.kubernetes.io/version: dev name: clusters.deployments.plural.sh diff --git a/charts/controller/crds/gitrepository-crd.yaml b/charts/controller/crds/gitrepository-crd.yaml index 6e69de412..f618ab685 100644 --- a/charts/controller/crds/gitrepository-crd.yaml +++ b/charts/controller/crds/gitrepository-crd.yaml @@ -4,7 +4,7 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 labels: - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/managed-by: helm app.kubernetes.io/part-of: plural-deployment-controller app.kubernetes.io/version: dev name: gitrepositories.deployments.plural.sh diff --git a/charts/controller/crds/provider-crd.yaml b/charts/controller/crds/provider-crd.yaml index 898ada099..2032525d1 100644 --- a/charts/controller/crds/provider-crd.yaml +++ b/charts/controller/crds/provider-crd.yaml @@ -4,7 +4,7 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 labels: - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/managed-by: helm app.kubernetes.io/part-of: plural-deployment-controller app.kubernetes.io/version: dev name: providers.deployments.plural.sh diff --git a/charts/controller/crds/servicedeployment-crd.yaml b/charts/controller/crds/servicedeployment-crd.yaml index 2b5828f39..aad2c916f 100644 --- a/charts/controller/crds/servicedeployment-crd.yaml +++ b/charts/controller/crds/servicedeployment-crd.yaml @@ -4,7 +4,7 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 labels: - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/managed-by: helm app.kubernetes.io/part-of: plural-deployment-controller app.kubernetes.io/version: dev name: servicedeployments.deployments.plural.sh diff --git a/charts/controller/templates/deployment.yaml b/charts/controller/templates/deployment.yaml index 8424a5277..0ad989aa9 100644 --- a/charts/controller/templates/deployment.yaml +++ b/charts/controller/templates/deployment.yaml @@ -10,7 +10,7 @@ spec: replicas: {{ .Values.controllerManager.replicas }} selector: matchLabels: - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/managed-by: helm app.kubernetes.io/part-of: plural-deployment-controller app.kubernetes.io/version: dev {{- include "controller.selectorLabels" . | nindent 6 }} @@ -18,7 +18,7 @@ spec: metadata: labels: app.kubernetes.io/component: manager - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/managed-by: helm app.kubernetes.io/name: controller-manager app.kubernetes.io/part-of: plural-deployment-controller app.kubernetes.io/version: dev diff --git a/controller/Makefile b/controller/Makefile index a04056da3..ed648dadf 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -140,6 +140,7 @@ endif .PHONY: helm helm: manifests kustomize helmify $(KUSTOMIZE) build config/default | $(HELMIFY) -generate-defaults -image-pull-secrets -crd-dir ../charts/controller + find ../charts/controller -type f -exec sed -i 's/app.kubernetes.io\/managed-by: kustomize/app.kubernetes.io\/managed-by: helm/g' {} \; .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. From 98d6f0be4379b43b5d57527a1d0b1ea77f37c669 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Tue, 19 Dec 2023 14:45:48 +0100 Subject: [PATCH 178/198] add controller dependency to console chart --- charts/console/Chart.lock | 7 +++++-- charts/console/Chart.yaml | 5 ++++- charts/console/charts/controller-0.0.1.tgz | Bin 0 -> 9286 bytes charts/console/charts/kas-0.0.3.tgz | Bin 99648 -> 99645 bytes charts/controller/Chart.yaml | 5 +++-- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 charts/console/charts/controller-0.0.1.tgz diff --git a/charts/console/Chart.lock b/charts/console/Chart.lock index 653a1dc5c..24cae6720 100644 --- a/charts/console/Chart.lock +++ b/charts/console/Chart.lock @@ -2,5 +2,8 @@ dependencies: - name: kas repository: file://../../plural/helm/kas version: 0.0.3 -digest: sha256:71264d8a9936ef983bf48ac4779febdb72f651058f8ab9b3b75e013a2bac040f -generated: "2023-11-14T13:48:17.925228-05:00" +- name: controller + repository: file://../controller + version: 0.0.1 +digest: sha256:d37b6c2a15aaa4b5efdc8499cf05b2514626ba93f6f5ddcea40e3b39729ea6f4 +generated: "2023-12-19T14:21:20.581699121+01:00" diff --git a/charts/console/Chart.yaml b/charts/console/Chart.yaml index f44f7a912..6f67cc701 100644 --- a/charts/console/Chart.yaml +++ b/charts/console/Chart.yaml @@ -7,4 +7,7 @@ appVersion: 0.7.5 dependencies: - name: kas version: 0.0.3 - repository: file://../../plural/helm/kas \ No newline at end of file + repository: file://../../plural/helm/kas +- name: controller + version: 0.0.1 + repository: file://../controller diff --git a/charts/console/charts/controller-0.0.1.tgz b/charts/console/charts/controller-0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..233e180210e254e7d6481f541615ae07c275aab2 GIT binary patch literal 9286 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 literal 0 HcmV?d00001 diff --git a/charts/console/charts/kas-0.0.3.tgz b/charts/console/charts/kas-0.0.3.tgz index 207128ebdba7f9bb4e234b894df2f707d34e790d..5ed5bee77892cd6ef62f76cd5779d6f416e9ad75 100644 GIT binary patch delta 94456 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 delta 94437 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%0D>C_^yf?IW*$ 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 F2>?h1$Fcwb diff --git a/charts/controller/Chart.yaml b/charts/controller/Chart.yaml index 36c1185d5..1f0827d79 100644 --- a/charts/controller/Chart.yaml +++ b/charts/controller/Chart.yaml @@ -13,9 +13,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.0.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.1.0" +# IMPORTANT: Should follow Console app version +appVersion: "0.7.5" From 9fc23c7965b68a7737f77697a47384b33732ecc7 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 19 Dec 2023 15:10:55 +0100 Subject: [PATCH 179/198] add ginko test for services --- .../servicedeployment_controller_test.go | 322 ------------------ .../servicedeployment_ginkgo_test.go | 227 ++++++++++++ 2 files changed, 227 insertions(+), 322 deletions(-) delete mode 100644 controller/internal/controller/servicedeployment_controller_test.go create mode 100644 controller/internal/controller/servicedeployment_ginkgo_test.go diff --git a/controller/internal/controller/servicedeployment_controller_test.go b/controller/internal/controller/servicedeployment_controller_test.go deleted file mode 100644 index a52d36e40..000000000 --- a/controller/internal/controller/servicedeployment_controller_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package controller_test - -// -//import ( -// "context" -// "encoding/json" -// "testing" -// "time" -// -// gqlclient "github.com/pluralsh/console-client-go" -// "github.com/pluralsh/console/controller/api/v1alpha1" -// "github.com/pluralsh/console/controller/internal/controller" -// "github.com/pluralsh/console/controller/internal/test/mocks" -// "github.com/samber/lo" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/mock" -// corev1 "k8s.io/api/core/v1" -// apierrors "k8s.io/apimachinery/pkg/api/errors" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/types" -// utilruntime "k8s.io/apimachinery/pkg/util/runtime" -// "k8s.io/client-go/kubernetes/scheme" -// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" -// "sigs.k8s.io/controller-runtime/pkg/client/fake" -// "sigs.k8s.io/controller-runtime/pkg/reconcile" -//) -// -//func init() { -// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -//} -// -//func TestCreateNewService(t *testing.T) { -// const ( -// serviceName = "test" -// clusterName = "testCluster" -// repoName = "testRepo" -// ) -// tests := []struct { -// name string -// service string -// returnGetService *gqlclient.ServiceDeploymentExtended -// returnIsClusterExisting bool -// returnCreateCluster *gqlclient.CreateServiceDeployment -// returnErrorCreateCluster error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ServiceStatus -// }{ -// { -// name: "scenario 1: create a new service", -// service: "test", -// expectedStatus: v1alpha1.ServiceStatus{ -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetService: &gqlclient.ServiceDeploymentExtended{ -// ID: "123", -// }, -// returnIsClusterExisting: false, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.ServiceDeployment{ -// ObjectMeta: metav1.ObjectMeta{Name: serviceName}, -// Spec: v1alpha1.ServiceSpec{ -// Version: "1.24", -// ClusterRef: corev1.ObjectReference{Name: clusterName}, -// RepositoryRef: corev1.ObjectReference{Name: repoName}, -// }, -// }, -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: clusterName, -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr("123"), -// }, -// }, -// &v1alpha1.GitRepository{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: repoName, -// }, -// Status: v1alpha1.GitRepositoryStatus{ -// ID: lo.ToPtr("123"), -// Health: v1alpha1.GitHealthPullable, -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() -// fakeConsoleClient.On("CreateService", mock.Anything, mock.Anything).Return(nil, nil) -// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) -// -// ctx := context.Background() -// -// target := &controller.ServiceReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) -// assert.NoError(t, err) -// -// existingService := &v1alpha1.ServiceDeployment{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) -// assert.NoError(t, err) -// existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) -// assert.NoError(t, err) -// expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} -// -//func sanitizeServiceConditions(status v1alpha1.ServiceStatus) v1alpha1.ServiceStatus { -// for i := range status.Conditions { -// status.Conditions[i].LastTransitionTime = metav1.Time{} -// status.Conditions[i].ObservedGeneration = 0 -// } -// -// return status -//} -// -//func TestDeleteService(t *testing.T) { -// const ( -// serviceName = "test" -// clusterName = "testCluster" -// repoName = "testRepo" -// ) -// tests := []struct { -// name string -// service string -// returnGetService *gqlclient.ServiceDeploymentExtended -// returnIsClusterExisting bool -// returnCreateCluster *gqlclient.CreateServiceDeployment -// returnErrorCreateCluster error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ServiceStatus -// }{ -// { -// name: "scenario 1: delete service", -// service: "test", -// expectedStatus: v1alpha1.ServiceStatus{ -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), -// }, -// returnGetService: &gqlclient.ServiceDeploymentExtended{ -// ID: "123", -// }, -// returnIsClusterExisting: false, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.ServiceDeployment{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: serviceName, -// DeletionTimestamp: &metav1.Time{Time: time.Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, -// Finalizers: []string{controller.ServiceFinalizer}, -// }, -// Spec: v1alpha1.ServiceSpec{ -// Version: "1.24", -// ClusterRef: corev1.ObjectReference{Name: clusterName}, -// RepositoryRef: corev1.ObjectReference{Name: repoName}, -// }, -// Status: v1alpha1.ServiceStatus{ -// ID: lo.ToPtr("123"), -// }, -// }, -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: clusterName, -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr("123"), -// }, -// }, -// &v1alpha1.GitRepository{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: repoName, -// }, -// Status: v1alpha1.GitRepositoryStatus{ -// ID: lo.ToPtr("123"), -// Health: v1alpha1.GitHealthPullable, -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() -// ctx := context.Background() -// -// target := &controller.ServiceReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) -// assert.NoError(t, err) -// -// existingService := &v1alpha1.ServiceDeployment{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) -// assert.True(t, apierrors.IsNotFound(err)) -// }) -// } -//} -// -//func TestUpdateService(t *testing.T) { -// const ( -// serviceName = "test" -// clusterName = "testCluster" -// repoName = "testRepo" -// ) -// tests := []struct { -// name string -// service string -// returnGetService *gqlclient.ServiceDeploymentExtended -// returnIsClusterExisting bool -// returnCreateCluster *gqlclient.CreateServiceDeployment -// returnErrorCreateCluster error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ServiceStatus -// }{ -// { -// name: "scenario 1: update service", -// service: "test", -// expectedStatus: v1alpha1.ServiceStatus{ -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// returnGetService: &gqlclient.ServiceDeploymentExtended{ -// ID: "123", -// }, -// returnIsClusterExisting: false, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.ServiceDeployment{ -// ObjectMeta: metav1.ObjectMeta{Name: serviceName}, -// Spec: v1alpha1.ServiceSpec{ -// Version: "1.24", -// ClusterRef: corev1.ObjectReference{Name: clusterName}, -// RepositoryRef: corev1.ObjectReference{Name: repoName}, -// }, -// Status: v1alpha1.ServiceStatus{ -// ID: lo.ToPtr("123"), -// SHA: lo.ToPtr("abc"), -// }, -// }, -// &v1alpha1.Cluster{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: clusterName, -// }, -// Status: v1alpha1.ClusterStatus{ -// ID: lo.ToPtr("123"), -// }, -// }, -// &v1alpha1.GitRepository{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: repoName, -// }, -// Status: v1alpha1.GitRepositoryStatus{ -// ID: lo.ToPtr("123"), -// Health: v1alpha1.GitHealthPullable, -// }, -// }, -// }, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(test.existingObjects...).Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) -// fakeConsoleClient.On("UpdateService", mock.Anything, mock.Anything).Return(nil) -// -// ctx := context.Background() -// -// target := &controller.ServiceReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// _, err := target.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.service}}) -// assert.NoError(t, err) -// -// existingService := &v1alpha1.ServiceDeployment{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.service}, existingService) -// assert.NoError(t, err) -// existingStatusJson, err := json.Marshal(sanitizeServiceConditions(existingService.Status)) -// assert.NoError(t, err) -// expectedStatusJson, err := json.Marshal(sanitizeServiceConditions(test.expectedStatus)) -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingStatusJson)) -// }) -// } -//} diff --git a/controller/internal/controller/servicedeployment_ginkgo_test.go b/controller/internal/controller/servicedeployment_ginkgo_test.go new file mode 100644 index 000000000..ae5df9974 --- /dev/null +++ b/controller/internal/controller/servicedeployment_ginkgo_test.go @@ -0,0 +1,227 @@ +package controller + +import ( + "context" + "github.com/pluralsh/console/controller/internal/test/utils" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/test/mocks" +) + +func sanitizeServiceConditions(status v1alpha1.ServiceStatus) v1alpha1.ServiceStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + +var _ = Describe("Service Controller", Ordered, func() { + Context("When reconciling a resource", func() { + const ( + serviceName = "service-test" + clusterName = "cluster-test" + repoName = "repo-test" + namespace = "default" + id = "123" + repoUrl = "https://test" + ) + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: serviceName, + Namespace: namespace, + } + + service := &v1alpha1.ServiceDeployment{} + BeforeAll(func() { + By("creating the custom resource for the Kind ServiceDeployment") + err := k8sClient.Get(ctx, typeNamespacedName, service) + if err != nil && errors.IsNotFound(err) { + resource := &v1alpha1.ServiceDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: namespace, + }, + Spec: v1alpha1.ServiceSpec{ + Version: "1.24", + ClusterRef: corev1.ObjectReference{Name: clusterName, Namespace: namespace}, + RepositoryRef: corev1.ObjectReference{Name: repoName, Namespace: namespace}, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + By("creating the custom resource for the Kind Cluster") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName, Namespace: namespace}, + Spec: v1alpha1.ClusterSpec{ + Cloud: "aws", + }, + }, func(p *v1alpha1.Cluster) { + p.Status.ID = lo.ToPtr(id) + })).To(Succeed()) + By("creating the custom resource for the Kind Repository") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{Name: repoName, Namespace: namespace}, + Spec: v1alpha1.GitRepositorySpec{ + Url: repoUrl, + }, + }, func(p *v1alpha1.GitRepository) { + p.Status.ID = lo.ToPtr(id) + p.Status.Health = v1alpha1.GitHealthPullable + })).To(Succeed()) + }) + + AfterAll(func() { + resource := &v1alpha1.Cluster{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Cluster") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + repo := &v1alpha1.GitRepository{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: repoName, Namespace: namespace}, repo) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Repository") + Expect(k8sClient.Delete(ctx, repo)).To(Succeed()) + }) + + It("should successfully reconcile the resource", func() { + By("Create resource") + test := struct { + returnGetService *gqlclient.ServiceDeploymentExtended + expectedStatus v1alpha1.ServiceStatus + }{ + expectedStatus: v1alpha1.ServiceStatus{ + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }, + returnGetService: &gqlclient.ServiceDeploymentExtended{ + ID: "123", + }, + } + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() + fakeConsoleClient.On("CreateService", mock.Anything, mock.Anything).Return(nil, nil) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) + serviceReconciler := &ServiceReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := serviceReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + + service := &v1alpha1.ServiceDeployment{} + err = k8sClient.Get(ctx, typeNamespacedName, service) + + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeServiceConditions(service.Status)).To(Equal(sanitizeServiceConditions(test.expectedStatus))) + }) + It("should successfully reconcile the resource", func() { + By("Update resource") + test := struct { + returnGetService *gqlclient.ServiceDeploymentExtended + expectedStatus v1alpha1.ServiceStatus + }{ + expectedStatus: v1alpha1.ServiceStatus{ + ID: lo.ToPtr("123"), + SHA: lo.ToPtr("E2KK4GJDZD4C62CW2OXWRDOWPOQ6XQJ4XHGZYFTANUMGIN7SGTPQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }, + returnGetService: &gqlclient.ServiceDeploymentExtended{ + ID: "123", + }, + } + + Expect(utils.MaybePatch(k8sClient, &v1alpha1.ServiceDeployment{ + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + }, func(p *v1alpha1.ServiceDeployment) { + p.Status.ID = lo.ToPtr(id) + p.Status.SHA = lo.ToPtr("ABC") + })).To(Succeed()) + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(test.returnGetService, nil) + fakeConsoleClient.On("UpdateService", mock.Anything, mock.Anything).Return(nil) + serviceReconciler := &ServiceReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := serviceReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + + service := &v1alpha1.ServiceDeployment{} + err = k8sClient.Get(ctx, typeNamespacedName, service) + + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeServiceConditions(service.Status)).To(Equal(sanitizeServiceConditions(test.expectedStatus))) + }) + It("should successfully reconcile the resource", func() { + By("Delete resource") + resource := &v1alpha1.ServiceDeployment{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Delete(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetService", mock.Anything, mock.Anything).Return(nil, nil).Once() + serviceReconciler := &ServiceReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err = serviceReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + + service := &v1alpha1.ServiceDeployment{} + err = k8sClient.Get(ctx, typeNamespacedName, service) + Expect(err).To(HaveOccurred()) + + }) + }) + +}) From ab8aa8db7ba02e3f6e93320c38c991b56e4cf417 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 11:22:16 +0100 Subject: [PATCH 180/198] adjust kustomize to console secret schema --- controller/Makefile | 15 +++++++++------ controller/config/default/kustomization.yaml | 14 +++++++++++++- .../config/default/manager_auth_proxy_patch.yaml | 2 -- controller/config/manager/manager.yaml | 11 ++--------- controller/config/manager/secret.yaml | 5 ++--- .../config/rbac/leader_election_role_binding.yaml | 2 +- controller/config/rbac/role_binding.yaml | 2 +- 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index ed648dadf..68598ec6d 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -5,6 +5,10 @@ include $(ROOT_DIRECTORY)/hack/include/build.mk # Image URL to use all building/pushing image targets IMG ?= deployment-controller:latest +# Config variables used for testing +DEFAULT_PLURAL_CONSOLE_URL := "https://console.aws-capi.onplural.sh") +PLURAL_CONSOLE_URL := $(if $(PLURAL_CONSOLE_URL),$(PLURAL_CONSOLE_URL),$(DEFAULT_PLURAL_CONSOLE_URL)) + # Tool binaries KUBECTL ?= $(shell which kubectl) KUSTOMIZE ?= $(shell which kustomize) @@ -24,10 +28,15 @@ ENVTEST_K8S_VERSION := 1.28.3 SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec +# Validate required env variables ifndef GOPATH $(error $$GOPATH environment variable not set) endif +ifndef PLURAL_CONSOLE_TOKEN +$(warning $$PLURAL_CONSOLE_TOKEN environment variable not set. Deploy will not work.) +endif + ifeq (,$(findstring $(GOPATH)/bin,$(PATH))) $(error $$GOPATH/bin directory is not in your $$PATH) endif @@ -152,12 +161,6 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize envsubst ## Deploy controller to the K8s cluster specified in ~/.kube/config. - ifndef PLURAL_CONSOLE_URL - $(error $$PLURAL_CONSOLE_URL environment variable not set) - endif - ifndef PLURAL_CONSOLE_TOKEN - $(error $$PLURAL_CONSOLE_TOKEN environment variable not set) - endif $(KUSTOMIZE) build config/default | $(ENVSUBST) | $(KUBECTL) apply -f - .PHONY: undeploy diff --git a/controller/config/default/kustomization.yaml b/controller/config/default/kustomization.yaml index cc062df18..68cad7c8e 100644 --- a/controller/config/default/kustomization.yaml +++ b/controller/config/default/kustomization.yaml @@ -1,5 +1,5 @@ # Adds namespace to all resources. -namespace: plural-deployment-controller +namespace: console # Value of this field is prepended to the # names of all resources, e.g. a deployment named @@ -28,4 +28,16 @@ patches: # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. - path: manager_auth_proxy_patch.yaml +- patch: |- + - op: add + path: /spec/template/spec/containers/1/args/- + value: --console-url=$PLURAL_CONSOLE_URL/gql + target: + kind: Deployment +- patch: |- + - op: add + path: /spec/template/spec/containers/1/args/- + value: --console-token=$(CONSOLE_TOKEN) + target: + kind: Deployment diff --git a/controller/config/default/manager_auth_proxy_patch.yaml b/controller/config/default/manager_auth_proxy_patch.yaml index cfdbd98dc..00221754f 100644 --- a/controller/config/default/manager_auth_proxy_patch.yaml +++ b/controller/config/default/manager_auth_proxy_patch.yaml @@ -36,5 +36,3 @@ spec: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" - - --console-url=$(CONSOLE_URL) - - --console-token=$(CONSOLE_TOKEN) diff --git a/controller/config/manager/manager.yaml b/controller/config/manager/manager.yaml index fd7421548..f18175ed5 100644 --- a/controller/config/manager/manager.yaml +++ b/controller/config/manager/manager.yaml @@ -32,22 +32,15 @@ spec: - /manager args: - --leader-elect - - --console-url=$(CONSOLE_URL) - - --console-token=$(CONSOLE_TOKEN) image: deployment-controller:latest imagePullPolicy: Never name: manager env: - - name: CONSOLE_URL - valueFrom: - secretKeyRef: - key: consoleUrl - name: secrets - name: CONSOLE_TOKEN valueFrom: secretKeyRef: - key: consoleToken - name: secrets + key: access-token + name: console-auth-token securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/controller/config/manager/secret.yaml b/controller/config/manager/secret.yaml index deac172c2..e45322c26 100644 --- a/controller/config/manager/secret.yaml +++ b/controller/config/manager/secret.yaml @@ -1,7 +1,6 @@ apiVersion: v1 kind: Secret metadata: - name: secrets + name: console-auth-token stringData: - consoleUrl: $PLURAL_CONSOLE_URL/gql # replaced with envsubst - consoleToken: $PLURAL_CONSOLE_TOKEN # replaced with envsubst + access-token: $PLURAL_CONSOLE_TOKEN # replaced with envsubst diff --git a/controller/config/rbac/leader_election_role_binding.yaml b/controller/config/rbac/leader_election_role_binding.yaml index ef563e3af..d9866e929 100644 --- a/controller/config/rbac/leader_election_role_binding.yaml +++ b/controller/config/rbac/leader_election_role_binding.yaml @@ -9,4 +9,4 @@ roleRef: subjects: - kind: ServiceAccount name: controller-manager - namespace: plural-deployment-controller + namespace: console diff --git a/controller/config/rbac/role_binding.yaml b/controller/config/rbac/role_binding.yaml index a316e37f1..a62b90916 100644 --- a/controller/config/rbac/role_binding.yaml +++ b/controller/config/rbac/role_binding.yaml @@ -9,4 +9,4 @@ roleRef: subjects: - kind: ServiceAccount name: controller-manager - namespace: plural-deployment-controller + namespace: console From 90de1166021119f7e35d89badf55ca83325e0a11 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 11:34:24 +0100 Subject: [PATCH 181/198] update controller chart --- charts/console/charts/controller-0.0.1.tgz | Bin 9286 -> 9204 bytes charts/console/charts/kas-0.0.3.tgz | Bin 99645 -> 99647 bytes charts/controller/templates/deployment.yaml | 16 ++++++---------- charts/controller/templates/secrets.yaml | 12 ------------ charts/controller/values.yaml | 9 ++++----- controller/Makefile | 5 ++--- 6 files changed, 12 insertions(+), 30 deletions(-) delete mode 100644 charts/controller/templates/secrets.yaml diff --git a/charts/console/charts/controller-0.0.1.tgz b/charts/console/charts/controller-0.0.1.tgz index 233e180210e254e7d6481f541615ae07c275aab2..4a69ab1c674d84b4b976bf3267f7de9cbf87a1ba 100644 GIT binary patch delta 9173 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 f;C_`Pw_WKq literal 9286 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 diff --git a/charts/console/charts/kas-0.0.3.tgz b/charts/console/charts/kas-0.0.3.tgz index 5ed5bee77892cd6ef62f76cd5779d6f416e9ad75..39d637b3cea22b6decff325b88f50abc23361a5d 100644 GIT binary patch delta 84620 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�@o4bab{V#wHJPrsNG9{^ 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 delta 84624 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 Ze!iZs=j*|*{|^8F|NpV2MV9~s2>`M`+2a5J diff --git a/charts/controller/templates/deployment.yaml b/charts/controller/templates/deployment.yaml index 0ad989aa9..353691797 100644 --- a/charts/controller/templates/deployment.yaml +++ b/charts/controller/templates/deployment.yaml @@ -42,20 +42,16 @@ spec: 10 }} securityContext: {{- toYaml .Values.controllerManager.kubeRbacProxy.containerSecurityContext | nindent 10 }} - - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + - args: + {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + - --console-url={{ .Values.consoleUrl }}/gql + - --console-token=$(CONSOLE_TOKEN) command: - /manager env: - - name: CONSOLE_URL - valueFrom: - secretKeyRef: - key: consoleUrl - name: {{ include "controller.fullname" . }}-secrets - name: CONSOLE_TOKEN valueFrom: - secretKeyRef: - key: consoleToken - name: {{ include "controller.fullname" . }}-secrets + secretKeyRef: {{- toYaml .Values.tokenSecretRef | nindent 14 }} - name: KUBERNETES_CLUSTER_DOMAIN value: {{ quote .Values.kubernetesClusterDomain }} image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag @@ -84,4 +80,4 @@ spec: seccompProfile: type: RuntimeDefault serviceAccountName: {{ include "controller.fullname" . }}-controller-manager - terminationGracePeriodSeconds: 10 \ No newline at end of file + terminationGracePeriodSeconds: 10 diff --git a/charts/controller/templates/secrets.yaml b/charts/controller/templates/secrets.yaml deleted file mode 100644 index 4d84cb6ea..000000000 --- a/charts/controller/templates/secrets.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "controller.fullname" . }}-secrets - labels: - app.kubernetes.io/part-of: plural-deployment-controller - {{- include "controller.labels" . | nindent 4 }} -stringData: - consoleToken: {{ required "secrets.consoleToken is required" .Values.secrets.consoleToken - | quote }} - consoleUrl: {{ required "secrets.consoleUrl is required" .Values.secrets.consoleUrl - | quote }} \ No newline at end of file diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml index 77d7d79c2..5c2896967 100644 --- a/charts/controller/values.yaml +++ b/charts/controller/values.yaml @@ -1,3 +1,7 @@ +consoleUrl: "" +tokenSecretRef: + name: console-auth-token + key: access-token controllerManager: kubeRbacProxy: args: @@ -25,8 +29,6 @@ controllerManager: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 - --leader-elect - - --console-url=$(CONSOLE_URL) - - --console-token=$(CONSOLE_TOKEN) containerSecurityContext: allowPrivilegeEscalation: false capabilities: @@ -48,6 +50,3 @@ controllerManager: annotations: {} imagePullSecrets: [] kubernetesClusterDomain: cluster.local -secrets: - consoleToken: "" - consoleUrl: "" diff --git a/controller/Makefile b/controller/Makefile index 68598ec6d..52dce2312 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -107,7 +107,6 @@ manifests: controller-gen ## generate WebhookConfiguration, ClusterRole and Cust .PHONY: generate generate: controller-gen ## generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations - go generate ./pkg/... $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: genmock @@ -148,8 +147,8 @@ endif .PHONY: helm helm: manifests kustomize helmify - $(KUSTOMIZE) build config/default | $(HELMIFY) -generate-defaults -image-pull-secrets -crd-dir ../charts/controller - find ../charts/controller -type f -exec sed -i 's/app.kubernetes.io\/managed-by: kustomize/app.kubernetes.io\/managed-by: helm/g' {} \; + @$(KUSTOMIZE) build config/default | $(HELMIFY) -generate-defaults -image-pull-secrets -crd-dir ../charts/controller + @find ../charts/controller -type f -exec sed -i 's/app.kubernetes.io\/managed-by: kustomize/app.kubernetes.io\/managed-by: helm/g' {} \; .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. From 2981f9f91476c3c16bc78d176219bdf3ec963b91 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 11:49:33 +0100 Subject: [PATCH 182/198] add deploy helm target --- controller/Makefile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 52dce2312..979837d59 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -18,6 +18,7 @@ ENVTEST ?= $(shell which setup-envtest) GOLANGCI_LINT ?= $(shell which golangci-lint) MOCKERY ?= $(shell which mockery) ENVSUBST ?= $(shell which envsubst) +HELM ?= $(shell which helm) # Tool versions KUBEBUILDER_VERSION := 3.11.1 @@ -103,11 +104,11 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform .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 + @$(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="./..." + @$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: genmock genmock: mockery ## generates mocks before running tests @@ -158,10 +159,22 @@ install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~ 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: uninstall-helm +uninstall-helm: NAMESPACE := console +uninstall-helm: RELEASE_NAME := controller-manager +uninstall-helm: manifests + @$(HELM) uninstall --namespace $(NAMESPACE) $(RELEASE_NAME) + .PHONY: deploy deploy: manifests kustomize envsubst ## Deploy controller to the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/default | $(ENVSUBST) | $(KUBECTL) apply -f - +.PHONY: deploy-helm +deploy-helm: NAMESPACE := console +deploy-helm: RELEASE_NAME := controller-manager +deploy-helm: manifests + @$(HELM) upgrade --install --create-namespace --namespace $(NAMESPACE) --set consoleUrl=$(PLURAL_CONSOLE_URL) $(RELEASE_NAME) ../charts/controller + .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - From 2360a6b95f3b8541b86085bfa827e45691a5ceef Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 20 Dec 2023 12:46:25 +0100 Subject: [PATCH 183/198] rewrite provider tests --- .../controller/cluster_controller_test.go | 4 +- .../controller/provider_controller_test.go | 566 +++++++----------- 2 files changed, 233 insertions(+), 337 deletions(-) diff --git a/controller/internal/controller/cluster_controller_test.go b/controller/internal/controller/cluster_controller_test.go index 64435e860..aa798129f 100644 --- a/controller/internal/controller/cluster_controller_test.go +++ b/controller/internal/controller/cluster_controller_test.go @@ -176,7 +176,7 @@ var _ = Describe("Cluster Controller", Ordered, func() { }))) }) - It("should successfully reconcile and update AWS cluster that was created in previous unit test", func() { + It("should successfully reconcile and update previously created AWS cluster", func() { Expect(utils.MaybePatch(k8sClient, &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{Name: awsClusterName, Namespace: "default"}, }, func(p *v1alpha1.Cluster) { @@ -255,7 +255,7 @@ var _ = Describe("Cluster Controller", Ordered, func() { }))) }) - It("should successfully reconcile and update BYOK cluster that was created in previous unit test", func() { + It("should successfully reconcile and update previously created BYOK cluster", func() { Expect(utils.MaybePatch(k8sClient, &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{Name: byokClusterName, Namespace: "default"}, }, func(p *v1alpha1.Cluster) { diff --git a/controller/internal/controller/provider_controller_test.go b/controller/internal/controller/provider_controller_test.go index f5001bec2..1339638dc 100644 --- a/controller/internal/controller/provider_controller_test.go +++ b/controller/internal/controller/provider_controller_test.go @@ -1,336 +1,232 @@ -package controller_test +package controller -// -//import ( -// "context" -// "encoding/json" -// "testing" -// -// gqlclient "github.com/pluralsh/console-client-go" -// "github.com/pluralsh/console/controller/internal/controller" -// "github.com/samber/lo" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/mock" -// corev1 "k8s.io/api/core/v1" -// "k8s.io/apimachinery/pkg/api/errors" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/runtime/schema" -// "k8s.io/apimachinery/pkg/types" -// utilruntime "k8s.io/apimachinery/pkg/util/runtime" -// "k8s.io/client-go/kubernetes/scheme" -// ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" -// "sigs.k8s.io/controller-runtime/pkg/client/fake" -// "sigs.k8s.io/controller-runtime/pkg/reconcile" -// -// "github.com/pluralsh/console/controller/api/v1alpha1" -// "github.com/pluralsh/console/controller/internal/test/mocks" -//) -// -//func init() { -// utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) -//} -// -//func sanitizeConditions(status v1alpha1.ProviderStatus) v1alpha1.ProviderStatus { -// for i := range status.Conditions { -// status.Conditions[i].LastTransitionTime = metav1.Time{} -// status.Conditions[i].ObservedGeneration = 0 -// } -// -// return status -//} -// -//func TestCreateNewProvider(t *testing.T) { -// test := struct { -// name string -// providerName string -// returnCreateProvider *gqlclient.ClusterProviderFragment -// returnGetProviderByCloudError error -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ProviderStatus -// }{ -// -// name: "create a new provider", -// providerName: "gcp-provider", -// returnCreateProvider: &gqlclient.ClusterProviderFragment{ -// ID: "1234", -// Name: "gcp-provider", -// Namespace: "gcp", -// Cloud: "gcp", -// }, -// returnGetProviderByCloudError: errors.NewNotFound(schema.GroupResource{}, "gcp-provider"), -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Provider{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "gcp-provider", -// }, -// Spec: v1alpha1.ProviderSpec{ -// Cloud: "gcp", -// CloudSettings: &v1alpha1.CloudProviderSettings{ -// GCP: &corev1.SecretReference{ -// Name: "credentials", -// }, -// }, -// Name: "gcp-provider", -// Namespace: "gcp", -// }, -// }, -// &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "credentials", -// }, -// Data: map[string][]byte{ -// "applicationCredentials": []byte("mock"), -// }, -// }, -// }, -// expectedStatus: v1alpha1.ProviderStatus{ -// ID: lo.ToPtr("1234"), -// SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// } -// -// t.Run(test.name, func(t *testing.T) { -// // set up the test scenario -// fakeClient := fake. -// NewClientBuilder(). -// WithScheme(scheme.Scheme). -// WithObjects(test.existingObjects...). -// Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// // act -// ctx := context.Background() -// providerReconciler := &controller.ProviderReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, test.returnGetProviderByCloudError) -// fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(false) -// fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(test.returnCreateProvider, nil) -// -// _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) -// assert.NoError(t, err) -// -// existingProvider := &v1alpha1.Provider{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) -// -// existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) -// expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) -// -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) -// }) -//} -// -//func TestAdoptProvider(t *testing.T) { -// test := struct { -// name string -// providerName string -// returnGetProviderByCloud *gqlclient.ClusterProviderFragment -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ProviderStatus -// }{ -// name: "adopt existing provider", -// providerName: "gcp-provider", -// returnGetProviderByCloud: &gqlclient.ClusterProviderFragment{ -// ID: "1234", -// Name: "gcp-provider", -// Namespace: "gcp", -// Cloud: "gcp", -// }, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Provider{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "gcp-provider", -// }, -// Spec: v1alpha1.ProviderSpec{ -// Cloud: "gcp", -// CloudSettings: &v1alpha1.CloudProviderSettings{ -// GCP: &corev1.SecretReference{ -// Name: "credentials", -// }, -// }, -// Name: "gcp-provider", -// Namespace: "gcp", -// }, -// }, -// &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "credentials", -// }, -// Data: map[string][]byte{ -// "applicationCredentials": []byte("mock"), -// }, -// }, -// }, -// expectedStatus: v1alpha1.ProviderStatus{ -// ID: lo.ToPtr("1234"), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// Message: v1alpha1.ReadonlyTrueConditionMessage.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// } -// -// t.Run(test.name, func(t *testing.T) { -// // set up the test scenario -// fakeClient := fake. -// NewClientBuilder(). -// WithScheme(scheme.Scheme). -// WithObjects(test.existingObjects...). -// Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// // act -// ctx := context.Background() -// providerReconciler := &controller.ProviderReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(test.returnGetProviderByCloud, nil) -// -// _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) -// assert.NoError(t, err) -// -// existingProvider := &v1alpha1.Provider{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) -// -// existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) -// expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) -// -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) -// }) -//} -// -//func TestUpdateProvider(t *testing.T) { -// test := struct { -// name string -// providerName string -// returnUpdateProvider *gqlclient.ClusterProviderFragment -// existingObjects []ctrlruntimeclient.Object -// expectedStatus v1alpha1.ProviderStatus -// }{ -// name: "update existing provider", -// providerName: "gcp-provider", -// returnUpdateProvider: &gqlclient.ClusterProviderFragment{ -// ID: "1234", -// Name: "gcp-provider", -// Namespace: "gcp", -// Cloud: "gcp", -// }, -// existingObjects: []ctrlruntimeclient.Object{ -// &v1alpha1.Provider{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "gcp-provider", -// }, -// Spec: v1alpha1.ProviderSpec{ -// Cloud: "gcp", -// CloudSettings: &v1alpha1.CloudProviderSettings{ -// GCP: &corev1.SecretReference{ -// Name: "credentials", -// }, -// }, -// Name: "gcp-provider", -// Namespace: "gcp", -// }, -// Status: v1alpha1.ProviderStatus{ -// ID: lo.ToPtr("1234"), -// SHA: lo.ToPtr(""), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// }, -// }, -// }, -// &corev1.Secret{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "credentials", -// }, -// Data: map[string][]byte{ -// "applicationCredentials": []byte("mock"), -// }, -// }, -// }, -// expectedStatus: v1alpha1.ProviderStatus{ -// ID: lo.ToPtr("1234"), -// SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), -// Conditions: []metav1.Condition{ -// { -// Type: v1alpha1.ReadonlyConditionType.String(), -// Status: metav1.ConditionFalse, -// Reason: v1alpha1.ReadonlyConditionReason.String(), -// }, -// { -// Type: v1alpha1.ReadyConditionType.String(), -// Status: metav1.ConditionTrue, -// Reason: v1alpha1.ReadyConditionReason.String(), -// }, -// }, -// }, -// } -// -// t.Run(test.name, func(t *testing.T) { -// // set up the test scenario -// fakeClient := fake. -// NewClientBuilder(). -// WithScheme(scheme.Scheme). -// WithObjects(test.existingObjects...). -// Build() -// -// fakeConsoleClient := mocks.NewConsoleClient(t) -// -// // act -// ctx := context.Background() -// providerReconciler := &controller.ProviderReconciler{ -// Client: fakeClient, -// Scheme: scheme.Scheme, -// ConsoleClient: fakeConsoleClient, -// } -// -// fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(true, nil) -// fakeConsoleClient.On("UpdateProvider", mock.Anything, mock.Anything, mock.Anything).Return(test.returnUpdateProvider, nil) -// -// _, err := providerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: test.providerName}}) -// assert.NoError(t, err) -// -// existingProvider := &v1alpha1.Provider{} -// err = fakeClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: test.providerName}, existingProvider) -// -// existingProviderStatusJson, err := json.Marshal(sanitizeConditions(existingProvider.Status)) -// expectedStatusJson, err := json.Marshal(sanitizeConditions(test.expectedStatus)) -// -// assert.NoError(t, err) -// assert.EqualValues(t, string(expectedStatusJson), string(existingProviderStatusJson)) -// }) -//} +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/samber/lo" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pluralsh/console/controller/api/v1alpha1" + "github.com/pluralsh/console/controller/internal/test/mocks" + "github.com/pluralsh/console/controller/internal/test/utils" +) + +func sanitizeProviderStatus(status v1alpha1.ProviderStatus) v1alpha1.ProviderStatus { + for i := range status.Conditions { + status.Conditions[i].LastTransitionTime = metav1.Time{} + status.Conditions[i].ObservedGeneration = 0 + } + + return status +} + +var _ = Describe("Provider Controller", Ordered, func() { + Context("when reconciling resource", func() { + const ( + providerName = "provider" + providerSecretName = "credentials" + providerSecretNamespace = "default" + providerConsoleID = "provider-console-id" + readonlyProviderName = "readonly-provider" + readonlyProviderConsoleID = "readonly-provider-console-id" + ) + + ctx := context.Background() + namespacedName := types.NamespacedName{Name: providerName} + readonlyNamespacedName := types.NamespacedName{Name: readonlyProviderName} + + BeforeAll(func() { + By("Creating provider secret") + Expect(utils.MaybeCreate(k8sClient, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSecretName, + Namespace: providerSecretNamespace, + }, + Data: map[string][]byte{ + "applicationCredentials": []byte("mock"), + }, + }, nil)).To(Succeed()) + + By("Creating provider") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerName, + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: "gcp", + CloudSettings: &v1alpha1.CloudProviderSettings{ + GCP: &corev1.SecretReference{ + Name: providerSecretName, + Namespace: providerSecretNamespace, + }, + }, + Name: providerName, + Namespace: "gcp", + }, + }, func(p *v1alpha1.Provider) { + p.Status.ID = lo.ToPtr(providerConsoleID) + })).To(Succeed()) + + By("Creating readonly provider") + Expect(utils.MaybeCreate(k8sClient, &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: readonlyProviderName, + }, + Spec: v1alpha1.ProviderSpec{ + Cloud: "aws", + Name: "aws", + }, + }, nil)).To(Succeed()) + }) + + AfterAll(func() { + By("Cleanup provider") + provider := &v1alpha1.Provider{} + err := k8sClient.Get(ctx, namespacedName, provider) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, provider)).To(Succeed()) + + By("Cleanup readonly provider") + readonlyProvider := &v1alpha1.Provider{} + err = k8sClient.Get(ctx, namespacedName, readonlyProvider) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, readonlyProvider)).To(Succeed()) + }) + + It("should successfully reconcile provider", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.GCP).Return(nil, errors.NewNotFound(schema.GroupResource{}, providerName)) + fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.AnythingOfType("string")).Return(false) + fakeConsoleClient.On("CreateProvider", mock.Anything, mock.Anything).Return(&gqlclient.ClusterProviderFragment{ + ID: providerConsoleID, + Name: providerName, + Namespace: "gcp", + Cloud: "gcp", + }, nil) + + controllerReconciler := &ProviderReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: namespacedName}) + Expect(err).NotTo(HaveOccurred()) + + provider := &v1alpha1.Provider{} + err = k8sClient.Get(ctx, namespacedName, provider) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeProviderStatus(provider.Status)).To(Equal(sanitizeProviderStatus(v1alpha1.ProviderStatus{ + ID: lo.ToPtr(providerConsoleID), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + + It("should successfully reconcile and update previously created provider", func() { + Expect(utils.MaybePatch(k8sClient, &v1alpha1.Provider{ + ObjectMeta: metav1.ObjectMeta{Name: providerName, Namespace: "default"}, + }, func(p *v1alpha1.Provider) { + p.Status.SHA = lo.ToPtr("diff-sha") + })).To(Succeed()) + + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("IsProviderExists", mock.Anything, mock.Anything).Return(true, nil) + fakeConsoleClient.On("UpdateProvider", mock.Anything, mock.Anything, mock.Anything).Return(&gqlclient.ClusterProviderFragment{ + ID: providerConsoleID, + Name: providerName, + Namespace: "gcp", + Cloud: "gcp", + }, nil) + + controllerReconciler := &ProviderReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: namespacedName}) + Expect(err).NotTo(HaveOccurred()) + + provider := &v1alpha1.Provider{} + err = k8sClient.Get(ctx, namespacedName, provider) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeProviderStatus(provider.Status)).To(Equal(sanitizeProviderStatus(v1alpha1.ProviderStatus{ + ID: lo.ToPtr(providerConsoleID), + SHA: lo.ToPtr("QL7PGU67IFKWWO4A7AU33D2HCTSGG4GGXR32DZXNPE6GDBHLXUSQ===="), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReadonlyConditionReason.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + + It("should successfully reconcile readonly provider", func() { + fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT) + fakeConsoleClient.On("GetProviderByCloud", mock.Anything, v1alpha1.AWS).Return(&gqlclient.ClusterProviderFragment{ + ID: readonlyProviderConsoleID, + Name: readonlyProviderName, + Namespace: "aws", + Cloud: "aws", + }, nil) + + controllerReconciler := &ProviderReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + ConsoleClient: fakeConsoleClient, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: readonlyNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + provider := &v1alpha1.Provider{} + err = k8sClient.Get(ctx, readonlyNamespacedName, provider) + Expect(err).NotTo(HaveOccurred()) + Expect(sanitizeProviderStatus(provider.Status)).To(Equal(sanitizeProviderStatus(v1alpha1.ProviderStatus{ + ID: lo.ToPtr(readonlyProviderConsoleID), + Conditions: []metav1.Condition{ + { + Type: v1alpha1.ReadonlyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadonlyConditionReason.String(), + Message: v1alpha1.ReadonlyTrueConditionMessage.String(), + }, + { + Type: v1alpha1.ReadyConditionType.String(), + Status: metav1.ConditionTrue, + Reason: v1alpha1.ReadyConditionReason.String(), + }, + }, + }))) + }) + }) +}) From 98f6c32918fb0d001f57d033e818c4c2db1db59d Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 12:50:14 +0100 Subject: [PATCH 184/198] update controller cd action --- .github/workflows/controller-cd.yaml | 73 +++++++++++++++++++ .../{controller.yaml => controller-ci.yaml} | 0 controller/Dockerfile | 2 +- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/controller-cd.yaml rename .github/workflows/{controller.yaml => controller-ci.yaml} (100%) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml new file mode 100644 index 000000000..20dbc3e7c --- /dev/null +++ b/.github/workflows/controller-cd.yaml @@ -0,0 +1,73 @@ +name: CD / Controller + +on: + pull_request: + branches: + - "master" + paths: + - "controller/**" + push: + tags: + - 'v*.*.*' + +permissions: + contents: read + +env: + GOPATH: /home/runner/go/ + PATH: $PATH:$GOPATH/bin + GOPROXY: "https://proxy.golang.org" + +jobs: + test: + name: Unit test + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + check-latest: true + - name: Test + run: make test + publish-docker: + name: Build and push controller container + runs-on: ubuntu-20.04 + needs: [ test ] + permissions: + contents: 'read' + id-token: 'write' + packages: 'write' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/pluralsh/deployment-controller + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: "." + file: "./Dockerfile" + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + GIT_COMMIT=${{ github.sha }} + VERSION=${{ steps.meta.outputs.version }} diff --git a/.github/workflows/controller.yaml b/.github/workflows/controller-ci.yaml similarity index 100% rename from .github/workflows/controller.yaml rename to .github/workflows/controller-ci.yaml diff --git a/controller/Dockerfile b/controller/Dockerfile index 36e5cd88a..2ab8356c4 100644 --- a/controller/Dockerfile +++ b/controller/Dockerfile @@ -21,7 +21,7 @@ COPY internal/ internal/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags '-s -w -X main.version=${VERSION} -X main.commit=${GIT_COMMIT}' -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details From 75446d5b3c5e8cedb74cae7eebd69832fe97de69 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 12:53:00 +0100 Subject: [PATCH 185/198] update controller cd action --- .github/workflows/controller-cd.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 20dbc3e7c..ca0d6105a 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -15,7 +15,6 @@ permissions: env: GOPATH: /home/runner/go/ - PATH: $PATH:$GOPATH/bin GOPROXY: "https://proxy.golang.org" jobs: @@ -31,7 +30,7 @@ jobs: go-version-file: go.mod check-latest: true - name: Test - run: make test + run: $PATH:$GOPATH/bin make test publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 From 02d7f4833f8e6d2edbe6ef50e2850e262215fec5 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 12:54:12 +0100 Subject: [PATCH 186/198] update controller cd action --- .github/workflows/controller-cd.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index ca0d6105a..ec0909a6a 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -21,6 +21,10 @@ jobs: test: name: Unit test runs-on: ubuntu-20.04 + defaults: + run: + shell: bash + working-directory: controller steps: - name: Checkout uses: actions/checkout@v4 @@ -34,6 +38,10 @@ jobs: publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 + defaults: + run: + shell: bash + working-directory: controller needs: [ test ] permissions: contents: 'read' From 1618b2641bb2435f9dfd2bd06489512f1e53aeb8 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 12:57:52 +0100 Subject: [PATCH 187/198] update controller cd action --- .github/workflows/controller-cd.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index ec0909a6a..278d77f47 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -32,7 +32,6 @@ jobs: uses: actions/setup-go@v4 with: go-version-file: go.mod - check-latest: true - name: Test run: $PATH:$GOPATH/bin make test publish-docker: From 28e1fd6d16680ac6871a8cf0cc806805c81667d8 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:00:16 +0100 Subject: [PATCH 188/198] update controller cd action --- .github/workflows/controller-cd.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 278d77f47..0dbb7c173 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -25,13 +25,15 @@ jobs: run: shell: bash working-directory: controller + timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version-file: go.mod + go-version-file: controller/go.mod + cache: true - name: Test run: $PATH:$GOPATH/bin make test publish-docker: From 77858f3bc901205ac8a05ea1c6610860dd785530 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:01:31 +0100 Subject: [PATCH 189/198] update controller cd action --- .github/workflows/controller-cd.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 0dbb7c173..329c3fcb6 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -14,7 +14,7 @@ permissions: contents: read env: - GOPATH: /home/runner/go/ + GOPATH: /home/runner/go GOPROXY: "https://proxy.golang.org" jobs: @@ -35,7 +35,7 @@ jobs: go-version-file: controller/go.mod cache: true - name: Test - run: $PATH:$GOPATH/bin make test + run: make test publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 From 2d3934ffb4c0febf5b7bba47eb234ffd1bbd6cec Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:04:43 +0100 Subject: [PATCH 190/198] update controller cd action --- .github/workflows/controller-cd.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 329c3fcb6..222bf483f 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -35,7 +35,7 @@ jobs: go-version-file: controller/go.mod cache: true - name: Test - run: make test + run: PATH=$PATH:$GOPATH/bin make test publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 @@ -50,7 +50,7 @@ jobs: packages: 'write' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Docker meta From 73e9a46532a422ff96a85d607fd2a9afdf633f00 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:10:44 +0100 Subject: [PATCH 191/198] update controller cd action --- .github/workflows/controller-cd.yaml | 2 ++ controller/Makefile | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 222bf483f..dc5f1d123 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -34,6 +34,8 @@ jobs: with: go-version-file: controller/go.mod cache: true + - name: Download dependencies + run: go mod download - name: Test run: PATH=$PATH:$GOPATH/bin make test publish-docker: diff --git a/controller/Makefile b/controller/Makefile index 979837d59..6c0a3158b 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -104,15 +104,15 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform .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 + $(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="./..." + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: genmock genmock: mockery ## generates mocks before running tests - @$(MOCKERY) + $(MOCKERY) ##@ Tests From 0e4ff8c0dc7f414781add781b58796f3b065f0eb Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:12:52 +0100 Subject: [PATCH 192/198] update controller cd action --- .github/workflows/controller-cd.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index dc5f1d123..2635438fb 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -15,6 +15,7 @@ permissions: env: GOPATH: /home/runner/go + GOBIN: /home/runner/go/bin GOPROXY: "https://proxy.golang.org" jobs: @@ -36,8 +37,10 @@ jobs: cache: true - name: Download dependencies run: go mod download + - name: List Go deps + run: ls $GOBIN - name: Test - run: PATH=$PATH:$GOPATH/bin make test + run: make test publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 From 4b6b730b5957d5889cc728e69b3e0bcb46f21ce0 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:14:46 +0100 Subject: [PATCH 193/198] update controller cd action --- .github/workflows/controller-cd.yaml | 4 ++-- controller/Makefile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 2635438fb..30fae68b7 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -37,8 +37,8 @@ jobs: cache: true - name: Download dependencies run: go mod download - - name: List Go deps - run: ls $GOBIN + - name: Download all tools + run: make tools - name: Test run: make test publish-docker: diff --git a/controller/Makefile b/controller/Makefile index 6c0a3158b..b7824f9b4 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -188,7 +188,7 @@ tools: --tool .PHONY: --tool %--tool: TOOL = .* --tool: # INTERNAL: installs tool with name provided via $(TOOL) variable or all tools. - @cat tools.go | grep _ | awk -F'"' '$$2 ~ /$(TOOL)/ {print $$2}' | xargs -I {} go install {} + cat tools.go | grep _ | awk -F'"' '$$2 ~ /$(TOOL)/ {print $$2}' | xargs -I {} go install {} .PHONY: controller-gen controller-gen: TOOL = controller-gen From 9841cc714c6df0b5090548b3456c7b5cfe004202 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:21:45 +0100 Subject: [PATCH 194/198] update controller cd action --- .github/workflows/controller-cd.yaml | 12 +++++++----- controller/Makefile | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 30fae68b7..abff29ea8 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -29,15 +29,15 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@v4.1.1 - name: Set up Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@v4.1.0 with: go-version-file: controller/go.mod cache: true - name: Download dependencies run: go mod download - - name: Download all tools + - name: Download tools run: make tools - name: Test run: make test @@ -55,7 +55,7 @@ jobs: packages: 'write' steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@v4.1.1 with: fetch-depth: 0 - name: Docker meta @@ -70,8 +70,10 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v5.1.0 with: context: "." file: "./Dockerfile" diff --git a/controller/Makefile b/controller/Makefile index b7824f9b4..6c0a3158b 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -188,7 +188,7 @@ tools: --tool .PHONY: --tool %--tool: TOOL = .* --tool: # INTERNAL: installs tool with name provided via $(TOOL) variable or all tools. - cat tools.go | grep _ | awk -F'"' '$$2 ~ /$(TOOL)/ {print $$2}' | xargs -I {} go install {} + @cat tools.go | grep _ | awk -F'"' '$$2 ~ /$(TOOL)/ {print $$2}' | xargs -I {} go install {} .PHONY: controller-gen controller-gen: TOOL = controller-gen From b48efe93a63b06047880b749c3d079bc71b0a70d Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:28:03 +0100 Subject: [PATCH 195/198] update controller cd action --- .github/workflows/controller-cd.yaml | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index abff29ea8..ddff7b461 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -19,28 +19,28 @@ env: GOPROXY: "https://proxy.golang.org" jobs: - test: - name: Unit test - runs-on: ubuntu-20.04 - defaults: - run: - shell: bash - working-directory: controller - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - name: Set up Go - uses: actions/setup-go@v4.1.0 - with: - go-version-file: controller/go.mod - cache: true - - name: Download dependencies - run: go mod download - - name: Download tools - run: make tools - - name: Test - run: make test +# test: +# name: Unit test +# runs-on: ubuntu-20.04 +# defaults: +# run: +# shell: bash +# working-directory: controller +# timeout-minutes: 5 +# steps: +# - name: Checkout +# uses: actions/checkout@v4.1.1 +# - name: Set up Go +# uses: actions/setup-go@v4.1.0 +# with: +# go-version-file: controller/go.mod +# cache: true +# - name: Download dependencies +# run: go mod download +# - name: Download tools +# run: make tools +# - name: Test +# run: make test publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 @@ -75,8 +75,8 @@ jobs: - name: Build and push uses: docker/build-push-action@v5.1.0 with: - context: "." - file: "./Dockerfile" + context: "./controller" + file: "./controller/Dockerfile" push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From cb1a3719322631f3496c5e863fffbf1926e1d2e9 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:29:55 +0100 Subject: [PATCH 196/198] update controller cd action --- .github/workflows/controller-cd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index ddff7b461..5457ba737 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -48,7 +48,7 @@ jobs: run: shell: bash working-directory: controller - needs: [ test ] +# needs: [ test ] permissions: contents: 'read' id-token: 'write' From 59f7f02a1c26170e99ca1cb5b9a1103d4daf4576 Mon Sep 17 00:00:00 2001 From: Sebastian Florek Date: Wed, 20 Dec 2023 13:51:53 +0100 Subject: [PATCH 197/198] update controller cd and values img repo --- .github/workflows/controller-cd.yaml | 48 +++++++++++++++------------- charts/controller/values.yaml | 2 +- controller/Makefile | 9 +++--- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/.github/workflows/controller-cd.yaml b/.github/workflows/controller-cd.yaml index 5457ba737..f82ade3a3 100644 --- a/.github/workflows/controller-cd.yaml +++ b/.github/workflows/controller-cd.yaml @@ -19,28 +19,28 @@ env: GOPROXY: "https://proxy.golang.org" jobs: -# test: -# name: Unit test -# runs-on: ubuntu-20.04 -# defaults: -# run: -# shell: bash -# working-directory: controller -# timeout-minutes: 5 -# steps: -# - name: Checkout -# uses: actions/checkout@v4.1.1 -# - name: Set up Go -# uses: actions/setup-go@v4.1.0 -# with: -# go-version-file: controller/go.mod -# cache: true -# - name: Download dependencies -# run: go mod download -# - name: Download tools -# run: make tools -# - name: Test -# run: make test + test: + name: Unit test + runs-on: ubuntu-20.04 + defaults: + run: + shell: bash + working-directory: controller + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + - name: Set up Go + uses: actions/setup-go@v4.1.0 + with: + go-version-file: controller/go.mod + cache: true + - name: Download dependencies + run: go mod download + - name: Download tools + run: make tools + - name: Test + run: make test publish-docker: name: Build and push controller container runs-on: ubuntu-20.04 @@ -48,7 +48,7 @@ jobs: run: shell: bash working-directory: controller -# needs: [ test ] + needs: [ test ] permissions: contents: 'read' id-token: 'write' @@ -70,6 +70,8 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.0.0 - name: Build and push diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml index 5c2896967..3c5ab3d15 100644 --- a/charts/controller/values.yaml +++ b/charts/controller/values.yaml @@ -35,7 +35,7 @@ controllerManager: drop: - ALL image: - repository: deployment-controller + repository: ghcr.io/pluralsh/deployment-controller tag: latest imagePullPolicy: Never resources: diff --git a/controller/Makefile b/controller/Makefile index 6c0a3158b..b27b5f71d 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -3,7 +3,8 @@ ROOT_DIRECTORY := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) include $(ROOT_DIRECTORY)/hack/include/build.mk # Image URL to use all building/pushing image targets -IMG ?= deployment-controller:latest +IMG_NAME ?= deployment-controller +IMG_TAG ?= pr-538 # Config variables used for testing DEFAULT_PLURAL_CONSOLE_URL := "https://console.aws-capi.onplural.sh") @@ -78,10 +79,10 @@ release: manifests generate fmt vet ## builds release version of the app. Requir # (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/ docker-build: ## build Docker image with the manager - docker build --no-cache -t ${IMG} . + docker build --no-cache -t ${IMG_NAME}:${IMG_TAG} . docker-push: ## push docker image with the manager - docker push ${IMG} + docker push ${IMG_NAME}:${IMG_TAG} # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: @@ -173,7 +174,7 @@ deploy: manifests kustomize envsubst ## Deploy controller to the K8s cluster spe deploy-helm: NAMESPACE := console deploy-helm: RELEASE_NAME := controller-manager deploy-helm: manifests - @$(HELM) upgrade --install --create-namespace --namespace $(NAMESPACE) --set consoleUrl=$(PLURAL_CONSOLE_URL) $(RELEASE_NAME) ../charts/controller + @$(HELM) upgrade --install --create-namespace --namespace $(NAMESPACE) --set consoleUrl=$(PLURAL_CONSOLE_URL) $(RELEASE_NAME) --set controllerManager.manager.image.tag=$(IMG_TAG) ../charts/controller .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. From def513ebf57b08c5a80f8a1c8b312197450376c4 Mon Sep 17 00:00:00 2001 From: Marcin Maciaszczyk Date: Wed, 20 Dec 2023 14:29:36 +0100 Subject: [PATCH 198/198] fix not found check --- controller/internal/client/cluster.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/controller/internal/client/cluster.go b/controller/internal/client/cluster.go index 65f48dfbd..dd58fbb26 100644 --- a/controller/internal/client/cluster.go +++ b/controller/internal/client/cluster.go @@ -2,6 +2,7 @@ package client import ( console "github.com/pluralsh/console-client-go" + internalerror "github.com/pluralsh/console/controller/internal/errors" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -34,6 +35,9 @@ func (c *client) CreateCluster(attrs console.ClusterAttributes) (*console.Cluste func (c *client) UpdateCluster(id string, attrs console.ClusterUpdateAttributes) (*console.ClusterFragment, error) { response, err := c.consoleClient.UpdateCluster(c.ctx, id, attrs) + if internalerror.IsNotFound(err) { + return nil, errors.NewNotFound(schema.GroupResource{}, id) + } if err == nil && (response == nil || response.UpdateCluster == nil) { return nil, errors.NewNotFound(schema.GroupResource{}, id) } @@ -46,6 +50,9 @@ func (c *client) UpdateCluster(id string, attrs console.ClusterUpdateAttributes) func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { response, err := c.consoleClient.GetCluster(c.ctx, id) + if internalerror.IsNotFound(err) { + return nil, errors.NewNotFound(schema.GroupResource{}, *id) + } if err == nil && (response == nil || response.Cluster == nil) { return nil, errors.NewNotFound(schema.GroupResource{}, *id) } @@ -58,6 +65,9 @@ func (c *client) GetCluster(id *string) (*console.ClusterFragment, error) { func (c *client) GetClusterByHandle(handle *string) (*console.ClusterFragment, error) { response, err := c.consoleClient.GetClusterByHandle(c.ctx, handle) + if internalerror.IsNotFound(err) { + return nil, errors.NewNotFound(schema.GroupResource{}, *handle) + } if err == nil && (response == nil || response.Cluster == nil) { return nil, errors.NewNotFound(schema.GroupResource{}, *handle) }