Created
March 2, 2025 11:07
-
-
Save sttts/0413e1cb2f0970d9a954cd75f1d055ce to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/.gitignore b/.gitignore | |
index 2ddc5a8b..4b09af17 100644 | |
--- a/.gitignore | |
+++ b/.gitignore | |
@@ -28,3 +28,5 @@ tools/setup-envtest/out | |
junit-report.xml | |
/artifacts | |
+ | |
+examples/kcp/.gitignore | |
diff --git a/.golangci.yml b/.golangci.yml | |
index 4c43665e..6dcbfb64 100644 | |
--- a/.golangci.yml | |
+++ b/.golangci.yml | |
@@ -13,7 +13,6 @@ linters: | |
- exhaustive | |
- exportloopref | |
- ginkgolinter | |
- - goconst | |
- gocritic | |
- gocyclo | |
- gofmt | |
diff --git a/.prow.yaml b/.prow.yaml | |
new file mode 100644 | |
index 00000000..1f9f775c | |
--- /dev/null | |
+++ b/.prow.yaml | |
@@ -0,0 +1,34 @@ | |
+presubmits: | |
+ - name: pull-controller-runtime-everything | |
+ always_run: true | |
+ decorate: true | |
+ clone_uri: "ssh://[email protected]/kcp-dev/controller-runtime.git" | |
+ labels: | |
+ preset-goproxy: "true" | |
+ spec: | |
+ containers: | |
+ - image: ghcr.io/kcp-dev/infra/build:1.22.2-1 | |
+ command: | |
+ - make | |
+ - test | |
+ | |
+ - name: pull-controller-runtime-example-e2e | |
+ decorate: true | |
+ # only run e2e tests if code changed. | |
+ run_if_changed: "(pkg|examples|go.mod|go.sum|Makefile|.prow.yaml)" | |
+ clone_uri: "https://github.com/kcp-dev/controller-runtime" | |
+ labels: | |
+ preset-goproxy: "true" | |
+ spec: | |
+ containers: | |
+ - image: ghcr.io/kcp-dev/infra/build:1.22.2-1 | |
+ env: | |
+ - name: KUBECONFIG | |
+ value: /home/prow/go/src/github.com/kcp-dev/controller-runtime/examples/kcp/.test/kcp.kubeconfig | |
+ command: | |
+ - make | |
+ - test-kcp-e2e | |
+ resources: | |
+ requests: | |
+ memory: 6Gi | |
+ cpu: 4 | |
diff --git a/DOWNSTREAM_OWNERS b/DOWNSTREAM_OWNERS | |
new file mode 100644 | |
index 00000000..3f5abef6 | |
--- /dev/null | |
+++ b/DOWNSTREAM_OWNERS | |
@@ -0,0 +1,5 @@ | |
+approvers: | |
+ - sttts | |
+ - xrstf | |
+ - mjudeikis | |
+ - embik | |
diff --git a/DOWNSTREAM_OWNERS_ALIASES b/DOWNSTREAM_OWNERS_ALIASES | |
new file mode 100644 | |
index 00000000..e69de29b | |
diff --git a/Makefile b/Makefile | |
index 9d92b977..0575eb81 100644 | |
--- a/Makefile | |
+++ b/Makefile | |
@@ -216,3 +216,6 @@ verify-apidiff: $(GO_APIDIFF) ## Check for API differences | |
go-version: ## Print the go version we use to compile our binaries and images | |
@echo $(GO_VERSION) | |
+.PHONY: test-kcp-e2e | |
+test-kcp-e2e: | |
+ cd examples/kcp && make kcp-server kcp-controller test | |
diff --git a/examples/builtins/main.go b/examples/builtins/main.go | |
index 5a6e313f..cd2ca0b4 100644 | |
--- a/examples/builtins/main.go | |
+++ b/examples/builtins/main.go | |
@@ -42,6 +42,7 @@ func main() { | |
// Setup a Manager | |
entryLog.Info("setting up manager") | |
+ | |
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{}) | |
if err != nil { | |
entryLog.Error(err, "unable to set up overall controller manager") | |
diff --git a/examples/kcp/Makefile b/examples/kcp/Makefile | |
new file mode 100644 | |
index 00000000..b0d167a9 | |
--- /dev/null | |
+++ b/examples/kcp/Makefile | |
@@ -0,0 +1,91 @@ | |
+SHELL := /bin/bash | |
+ | |
+.PHONY: help | |
+help: ## Display this help. | |
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\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) | |
+ | |
+GO_INSTALL = ./hack/go-install.sh | |
+ | |
+LOCALBIN ?= $(shell pwd)/bin | |
+TOOLS_DIR=hack/tools | |
+TOOLS_BIN_DIR := $(abspath $(TOOLS_DIR)/bin) | |
+ARTIFACT_DIR ?= .test | |
+ | |
+KCP ?= $(LOCALBIN)/kcp | |
+KUBECTL_KCP ?= $(LOCALBIN)/kubectl-kcp | |
+ | |
+KCP_VERSION ?= 0.23.0 | |
+CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen | |
+export CONTROLLER_GEN # so hack scripts can use it | |
+ | |
+KCP_APIGEN_VER := v0.21.0 | |
+KCP_APIGEN_BIN := apigen | |
+KCP_APIGEN_GEN := $(TOOLS_BIN_DIR)/$(KCP_APIGEN_BIN)-$(KCP_APIGEN_VER) | |
+export KCP_APIGEN_GEN # so hack scripts can use it | |
+ | |
+OS ?= $(shell go env GOOS ) | |
+ARCH ?= $(shell go env GOARCH ) | |
+ | |
+$(KCP): ## Download kcp locally if necessary. | |
+ mkdir -p $(LOCALBIN) | |
+ curl -L -s -o - https://github.com/kcp-dev/kcp/releases/download/v$(KCP_VERSION)/kcp_$(KCP_VERSION)_$(OS)_$(ARCH).tar.gz | tar --directory $(LOCALBIN)/../ -xvzf - bin/kcp | |
+ touch $(KCP) # we download an "old" file, so make will re-download to refresh it unless we make it newer than the owning dir | |
+ | |
+$(KUBECTL_KCP): ## Download kcp kubectl plugins locally if necessary. | |
+ curl -L -s -o - https://github.com/kcp-dev/kcp/releases/download/v$(KCP_VERSION)/kubectl-kcp-plugin_$(KCP_VERSION)_$(OS)_$(ARCH).tar.gz | tar --directory $(LOCALBIN)/../ -xvzf - bin | |
+ touch $(KUBECTL_KCP) # we download an "old" file, so make will re-download to refresh it unless we make it newer than the owning dir | |
+ | |
+$(KCP_APIGEN_GEN): | |
+ GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/kcp-dev/kcp/sdk/cmd/apigen $(KCP_APIGEN_BIN) $(KCP_APIGEN_VER) | |
+ | |
+$(CONTROLLER_GEN): $(TOOLS_DIR)/go.mod # Build controller-gen from tools folder. | |
+ cd $(TOOLS_DIR) && go build -tags=tools -o bin/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen | |
+ | |
+build: $(KCP) $(KUBECTL_KCP) build-controller | |
+ | |
+ifeq (,$(shell go env GOBIN)) | |
+GOBIN=$(shell go env GOPATH)/bin | |
+else | |
+GOBIN=$(shell go env GOBIN) | |
+endif | |
+ | |
+build-controller: ## Build the controller binary. | |
+ go build -o $(LOCALBIN)/kcp-controller ./main.go | |
+ | |
+.PHONY: kcp-server | |
+kcp-server: $(KCP) $(ARTIFACT_DIR)/kcp ## Run the kcp server. | |
+ @if [[ ! -s $(ARTIFACT_DIR)/kcp.log ]]; then ( $(KCP) start -v 5 --root-directory $(ARTIFACT_DIR)/kcp --kubeconfig-path $(ARTIFACT_DIR)/kcp.kubeconfig --audit-log-maxsize 1024 --audit-log-mode=batch --audit-log-batch-max-wait=1s --audit-log-batch-max-size=1000 --audit-log-batch-buffer-size=10000 --audit-log-batch-throttle-burst=15 --audit-log-batch-throttle-enable=true --audit-log-batch-throttle-qps=10 --audit-policy-file ./test/e2e/audit-policy.yaml --audit-log-path $(ARTIFACT_DIR)/audit.log >$(ARTIFACT_DIR)/kcp.log 2>&1 & ); fi | |
+ @echo "Waiting for kcp server to generate kubeconfig..." | |
+ @while true; do if [[ ! -s $(ARTIFACT_DIR)/kcp.kubeconfig ]]; then sleep 0.2; else break; fi; done | |
+ @echo "Waiting for kcp server to be ready..." | |
+ @while true; do if ! kubectl --kubeconfig $(ARTIFACT_DIR)/kcp.kubeconfig get --raw /readyz >$(ARTIFACT_DIR)/kcp.probe.log 2>&1; then sleep 0.2; else break; fi; done | |
+ @echo "kcp server is ready and running in the background. To stop run 'make test-cleanup'" | |
+ | |
+.PHONY: kcp-bootstrap | |
+kcp-bootstrap: ## Bootstrap the kcp server. | |
+ @go run ./config/main.go | |
+ | |
+.PHONY: kcp-controller | |
+kcp-controller: build kcp-bootstrap ## Run the kcp-controller. | |
+ @echo "Starting kcp-controller in the background. To stop run 'make test-cleanup'" | |
+ @if [[ ! -s $(ARTIFACT_DIR)/controller.log ]]; then ( ./bin/kcp-controller >$(ARTIFACT_DIR)/controller.log 2>&1 & ); fi | |
+ | |
+.PHONY: test-e2e-cleanup | |
+test-cleanup: ## Clean up processes and directories from an end-to-end test run. | |
+ rm -rf $(ARTIFACT_DIR) || true | |
+ pkill -sigterm kcp || true | |
+ pkill -sigterm kubectl || true | |
+ pkill -sigterm kcp-controller || true | |
+ | |
+$(ARTIFACT_DIR)/kcp: ## Create a directory for the kcp server data. | |
+ mkdir -p $(ARTIFACT_DIR)/kcp | |
+ | |
+generate: build $(CONTROLLER_GEN) $(KCP_APIGEN_GEN) # Generate code | |
+ ./hack/update-codegen-crds.sh | |
+ | |
+run-local: build-controller kcp-bootstrap | |
+ ./bin/kcp-controller | |
+ | |
+.PHONY: test # Run tests | |
+test: | |
+ go test ./... --workspace=root --kubeconfig=$(CURDIR)/$(ARTIFACT_DIR)/kcp.kubeconfig | |
diff --git a/examples/kcp/README.md b/examples/kcp/README.md | |
new file mode 100644 | |
index 00000000..760f1410 | |
--- /dev/null | |
+++ b/examples/kcp/README.md | |
@@ -0,0 +1,85 @@ | |
+# controller-runtime-example | |
+An example project that is multi-cluster aware and works with [kcp](https://github.com/kcp-dev/kcp) | |
+ | |
+## Description | |
+ | |
+In this example, we intentionally not using advanced kubebuilder patterns to keep the example simple and easy to understand. | |
+In the future, we will add more advanced examples. Example covers 3 parts: | |
+1. KCP bootstrapping - creating APIExport & consumer workspaces | |
+ 1. Creating WorkspaceType for particular exports | |
+2. Running controller with APIExport aware configuration and reconciling multiple consumer workspaces | |
+ | |
+ | |
+This example contains an example project that works with APIExports and multiple kcp workspaces. It demonstrates | |
+two reconcilers: | |
+ | |
+1. ConfigMap | |
+ 1. Get a ConfigMap for the key from the queue, from the correct logical cluster | |
+ 2. If the ConfigMap has labels["name"], set labels["response"] = "hello-$name" and save the changes | |
+ 3. List all ConfigMaps in the logical cluster and log each one's namespace and name | |
+ 4. If the ConfigMap from step 1 has data["namespace"] set, create a namespace whose name is the data value. | |
+ 5. If the ConfigMap from step 1 has data["secretData"] set, create a secret in the same namespace as the ConfigMap, | |
+ with an owner reference to the ConfigMap, and data["dataFromCM"] set to the data value. | |
+ | |
+2. Widget | |
+ 1. Show how to list all Widget instances across all logical clusters | |
+ 2. Get a Widget for the key from the queue, from the correct logical cluster | |
+ 3. List all Widgets in the same logical cluster | |
+ 4. Count the number of Widgets (list length) | |
+ 5. Make sure `.status.total` matches the current count (via a `patch`) | |
+ | |
+## Getting Started | |
+ | |
+### Running on kcp | |
+ | |
+1. Run KCP with the following command: | |
+ | |
+```sh | |
+make kcp-server | |
+``` | |
+ | |
+From this point onwards you can inspect kcp configuration using kubeconfig: | |
+ | |
+```sh | |
+export KUBECONFIG=.test/kcp.kubeconfig | |
+``` | |
+ | |
+1. Bootstrap the KCP server with the following command: | |
+ | |
+```sh | |
+export KUBECONFIG=./.test/kcp.kubeconfig | |
+make kcp-bootstrap | |
+``` | |
+ | |
+1. Run controller: | |
+ | |
+```sh | |
+export KUBECONFIG=./.test/kcp.kubeconfig | |
+make run-local | |
+``` | |
+ | |
+1. In separate shell you can run tests to exercise the controller: | |
+ | |
+```sh | |
+export KUBECONFIG=./.test/kcp.kubeconfig | |
+make test | |
+``` | |
+ | |
+### Uninstall resources | |
+To delete the resources from kcp: | |
+ | |
+```sh | |
+make test-clean | |
+``` | |
+ | |
+ | |
+ | |
+### How it works | |
+This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) | |
+ | |
+It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) | |
+which provides a reconcile function responsible for synchronizing resources until the desired state is reached. | |
+ | |
+ | |
+ | |
+ | |
diff --git a/examples/kcp/apis/v1alpha1/groupversion_info.go b/examples/kcp/apis/v1alpha1/groupversion_info.go | |
new file mode 100644 | |
index 00000000..5e15b53b | |
--- /dev/null | |
+++ b/examples/kcp/apis/v1alpha1/groupversion_info.go | |
@@ -0,0 +1,36 @@ | |
+/* | |
+Copyright 2024. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+// Package v1alpha1 contains API Schema definitions for the data v1alpha1 API group | |
+// +kubebuilder:object:generate=true | |
+// +groupName=data.my.domain | |
+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: "data.my.domain", 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/examples/kcp/apis/v1alpha1/widget_types.go b/examples/kcp/apis/v1alpha1/widget_types.go | |
new file mode 100644 | |
index 00000000..038a1cee | |
--- /dev/null | |
+++ b/examples/kcp/apis/v1alpha1/widget_types.go | |
@@ -0,0 +1,56 @@ | |
+/* | |
+Copyright 2024. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package v1alpha1 | |
+ | |
+import ( | |
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
+) | |
+ | |
+// WidgetSpec defines the desired state of Widget | |
+type WidgetSpec struct { | |
+ Foo string `json:"foo,omitempty"` | |
+} | |
+ | |
+// WidgetStatus defines the observed state of Widget | |
+type WidgetStatus struct { | |
+ Total int `json:"total,omitempty"` | |
+} | |
+ | |
+// +kubebuilder:object:root=true | |
+// +kubebuilder:subresource:status | |
+ | |
+// Widget is the Schema for the widgets API | |
+type Widget struct { | |
+ metav1.TypeMeta `json:",inline"` | |
+ metav1.ObjectMeta `json:"metadata,omitempty"` | |
+ | |
+ Spec WidgetSpec `json:"spec,omitempty"` | |
+ Status WidgetStatus `json:"status,omitempty"` | |
+} | |
+ | |
+// +kubebuilder:object:root=true | |
+ | |
+// WidgetList contains a list of Widget | |
+type WidgetList struct { | |
+ metav1.TypeMeta `json:",inline"` | |
+ metav1.ListMeta `json:"metadata,omitempty"` | |
+ Items []Widget `json:"items"` | |
+} | |
+ | |
+func init() { | |
+ SchemeBuilder.Register(&Widget{}, &WidgetList{}) | |
+} | |
diff --git a/examples/kcp/apis/v1alpha1/zz_generated.deepcopy.go b/examples/kcp/apis/v1alpha1/zz_generated.deepcopy.go | |
new file mode 100644 | |
index 00000000..e4892949 | |
--- /dev/null | |
+++ b/examples/kcp/apis/v1alpha1/zz_generated.deepcopy.go | |
@@ -0,0 +1,98 @@ | |
+//go:build !ignore_autogenerated | |
+ | |
+// Code generated by controller-gen. DO NOT EDIT. | |
+ | |
+package v1alpha1 | |
+ | |
+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 *Widget) DeepCopyInto(out *Widget) { | |
+ *out = *in | |
+ out.TypeMeta = in.TypeMeta | |
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) | |
+ out.Spec = in.Spec | |
+ out.Status = in.Status | |
+} | |
+ | |
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Widget. | |
+func (in *Widget) DeepCopy() *Widget { | |
+ if in == nil { | |
+ return nil | |
+ } | |
+ out := new(Widget) | |
+ in.DeepCopyInto(out) | |
+ return out | |
+} | |
+ | |
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. | |
+func (in *Widget) 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 *WidgetList) DeepCopyInto(out *WidgetList) { | |
+ *out = *in | |
+ out.TypeMeta = in.TypeMeta | |
+ in.ListMeta.DeepCopyInto(&out.ListMeta) | |
+ if in.Items != nil { | |
+ in, out := &in.Items, &out.Items | |
+ *out = make([]Widget, len(*in)) | |
+ for i := range *in { | |
+ (*in)[i].DeepCopyInto(&(*out)[i]) | |
+ } | |
+ } | |
+} | |
+ | |
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WidgetList. | |
+func (in *WidgetList) DeepCopy() *WidgetList { | |
+ if in == nil { | |
+ return nil | |
+ } | |
+ out := new(WidgetList) | |
+ in.DeepCopyInto(out) | |
+ return out | |
+} | |
+ | |
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. | |
+func (in *WidgetList) 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 *WidgetSpec) DeepCopyInto(out *WidgetSpec) { | |
+ *out = *in | |
+} | |
+ | |
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WidgetSpec. | |
+func (in *WidgetSpec) DeepCopy() *WidgetSpec { | |
+ if in == nil { | |
+ return nil | |
+ } | |
+ out := new(WidgetSpec) | |
+ in.DeepCopyInto(out) | |
+ return out | |
+} | |
+ | |
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. | |
+func (in *WidgetStatus) DeepCopyInto(out *WidgetStatus) { | |
+ *out = *in | |
+} | |
+ | |
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WidgetStatus. | |
+func (in *WidgetStatus) DeepCopy() *WidgetStatus { | |
+ if in == nil { | |
+ return nil | |
+ } | |
+ out := new(WidgetStatus) | |
+ in.DeepCopyInto(out) | |
+ return out | |
+} | |
diff --git a/examples/kcp/config/consumers/bootstrap.go b/examples/kcp/config/consumers/bootstrap.go | |
new file mode 100644 | |
index 00000000..ed68b8be | |
--- /dev/null | |
+++ b/examples/kcp/config/consumers/bootstrap.go | |
@@ -0,0 +1,43 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package consumers | |
+ | |
+import ( | |
+ "context" | |
+ "embed" | |
+ | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/log" | |
+ | |
+ confighelpers "github.com/kcp-dev/controller-runtime/examples/kcp/config/helpers" | |
+) | |
+ | |
+//go:embed *.yaml | |
+var fs embed.FS | |
+ | |
+// Bootstrap creates resources in this package by continuously retrying the list. | |
+// This is blocking, i.e. it only returns (with error) when the context is closed or with nil when | |
+// the bootstrapping is successfully completed. | |
+func Bootstrap( | |
+ ctx context.Context, | |
+ client client.Client, | |
+) error { | |
+ log := log.FromContext(ctx) | |
+ | |
+ log.Info("Bootstrapping consumers workspaces") | |
+ return confighelpers.Bootstrap(ctx, client, fs) | |
+} | |
diff --git a/examples/kcp/config/consumers/consumer1-workspace.yaml b/examples/kcp/config/consumers/consumer1-workspace.yaml | |
new file mode 100644 | |
index 00000000..3161d713 | |
--- /dev/null | |
+++ b/examples/kcp/config/consumers/consumer1-workspace.yaml | |
@@ -0,0 +1,14 @@ | |
+apiVersion: tenancy.kcp.io/v1alpha1 | |
+kind: Workspace | |
+metadata: | |
+ name: consumer1 | |
+ annotations: | |
+ bootstrap.kcp.io/create-only: "true" | |
+spec: | |
+ type: | |
+ name: widgets | |
+ path: root | |
+ location: | |
+ selector: | |
+ matchLabels: | |
+ name: root | |
diff --git a/examples/kcp/config/consumers/consumer2-workspace.yaml b/examples/kcp/config/consumers/consumer2-workspace.yaml | |
new file mode 100644 | |
index 00000000..e75e3121 | |
--- /dev/null | |
+++ b/examples/kcp/config/consumers/consumer2-workspace.yaml | |
@@ -0,0 +1,14 @@ | |
+apiVersion: tenancy.kcp.io/v1alpha1 | |
+kind: Workspace | |
+metadata: | |
+ name: consumer2 | |
+ annotations: | |
+ bootstrap.kcp.io/create-only: "true" | |
+spec: | |
+ type: | |
+ name: widgets | |
+ path: root | |
+ location: | |
+ selector: | |
+ matchLabels: | |
+ name: root | |
diff --git a/examples/kcp/config/crds/data.my.domain_widgets.yaml b/examples/kcp/config/crds/data.my.domain_widgets.yaml | |
new file mode 100644 | |
index 00000000..02c5feaa | |
--- /dev/null | |
+++ b/examples/kcp/config/crds/data.my.domain_widgets.yaml | |
@@ -0,0 +1,55 @@ | |
+--- | |
+apiVersion: apiextensions.k8s.io/v1 | |
+kind: CustomResourceDefinition | |
+metadata: | |
+ annotations: | |
+ controller-gen.kubebuilder.io/version: v0.14.0 | |
+ name: widgets.data.my.domain | |
+spec: | |
+ group: data.my.domain | |
+ names: | |
+ kind: Widget | |
+ listKind: WidgetList | |
+ plural: widgets | |
+ singular: widget | |
+ scope: Namespaced | |
+ versions: | |
+ - name: v1alpha1 | |
+ schema: | |
+ openAPIV3Schema: | |
+ description: Widget is the Schema for the widgets API | |
+ properties: | |
+ apiVersion: | |
+ description: |- | |
+ APIVersion defines the versioned schema of this representation of an object. | |
+ Servers should convert recognized schemas to the latest internal value, and | |
+ may reject unrecognized values. | |
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | |
+ type: string | |
+ kind: | |
+ description: |- | |
+ Kind is a string value representing the REST resource this object represents. | |
+ Servers may infer this from the endpoint the client submits requests to. | |
+ Cannot be updated. | |
+ In CamelCase. | |
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | |
+ type: string | |
+ metadata: | |
+ type: object | |
+ spec: | |
+ description: WidgetSpec defines the desired state of Widget | |
+ properties: | |
+ foo: | |
+ type: string | |
+ type: object | |
+ status: | |
+ description: WidgetStatus defines the observed state of Widget | |
+ properties: | |
+ total: | |
+ type: integer | |
+ type: object | |
+ type: object | |
+ served: true | |
+ storage: true | |
+ subresources: | |
+ status: {} | |
diff --git a/examples/kcp/config/helpers/bootstrap.go b/examples/kcp/config/helpers/bootstrap.go | |
new file mode 100644 | |
index 00000000..93dd2305 | |
--- /dev/null | |
+++ b/examples/kcp/config/helpers/bootstrap.go | |
@@ -0,0 +1,143 @@ | |
+package helpers | |
+ | |
+import ( | |
+ "bufio" | |
+ "bytes" | |
+ "context" | |
+ "embed" | |
+ "errors" | |
+ "fmt" | |
+ "io" | |
+ "text/template" | |
+ "time" | |
+ | |
+ extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" | |
+ apierrors "k8s.io/apimachinery/pkg/api/errors" | |
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | |
+ "k8s.io/apimachinery/pkg/types" | |
+ apimachineryerrors "k8s.io/apimachinery/pkg/util/errors" | |
+ "k8s.io/apimachinery/pkg/util/wait" | |
+ kubeyaml "k8s.io/apimachinery/pkg/util/yaml" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/log" | |
+) | |
+ | |
+// Bootstrap creates resources in a package's fs by | |
+// continuously retrying the list. This is blocking, i.e. it only returns (with error) | |
+// when the context is closed or with nil when the bootstrapping is successfully completed. | |
+func Bootstrap(ctx context.Context, client client.Client, fs embed.FS) error { | |
+ // bootstrap non-crd resources | |
+ return wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) { | |
+ if err := CreateResourcesFromFS(ctx, client, fs); err != nil { | |
+ log.FromContext(ctx).WithValues("err", err).Info("failed to bootstrap resources, retrying") | |
+ return false, nil | |
+ } | |
+ return true, nil | |
+ }) | |
+} | |
+ | |
+// CreateResourcesFromFS creates all resources from a filesystem. | |
+func CreateResourcesFromFS(ctx context.Context, client client.Client, fs embed.FS) error { | |
+ files, err := fs.ReadDir(".") | |
+ if err != nil { | |
+ return err | |
+ } | |
+ | |
+ var errs []error | |
+ for _, f := range files { | |
+ if f.IsDir() { | |
+ continue | |
+ } | |
+ if err := CreateResourceFromFS(ctx, client, f.Name(), fs); err != nil { | |
+ errs = append(errs, err) | |
+ } | |
+ } | |
+ return apimachineryerrors.NewAggregate(errs) | |
+} | |
+ | |
+// CreateResourceFromFS creates given resource file. | |
+func CreateResourceFromFS(ctx context.Context, client client.Client, filename string, fs embed.FS) error { | |
+ raw, err := fs.ReadFile(filename) | |
+ if err != nil { | |
+ return fmt.Errorf("could not read %s: %w", filename, err) | |
+ } | |
+ | |
+ if len(raw) == 0 { | |
+ return nil // ignore empty files | |
+ } | |
+ | |
+ d := kubeyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(raw))) | |
+ var errs []error | |
+ for i := 1; ; i++ { | |
+ doc, err := d.Read() | |
+ if errors.Is(err, io.EOF) { | |
+ break | |
+ } else if err != nil { | |
+ return err | |
+ } | |
+ if len(bytes.TrimSpace(doc)) == 0 { | |
+ continue | |
+ } | |
+ | |
+ if err := createResourceFromFS(ctx, client, doc); err != nil { | |
+ errs = append(errs, fmt.Errorf("failed to create resource %s doc %d: %w", filename, i, err)) | |
+ } | |
+ } | |
+ return apimachineryerrors.NewAggregate(errs) | |
+} | |
+ | |
+func createResourceFromFS(ctx context.Context, client client.Client, raw []byte) error { | |
+ log := log.FromContext(ctx) | |
+ | |
+ type Input struct { | |
+ Batteries map[string]bool | |
+ } | |
+ input := Input{ | |
+ Batteries: map[string]bool{}, | |
+ } | |
+ tmpl, err := template.New("manifest").Parse(string(raw)) | |
+ if err != nil { | |
+ return fmt.Errorf("failed to parse manifest: %w", err) | |
+ } | |
+ var buf bytes.Buffer | |
+ if err := tmpl.Execute(&buf, input); err != nil { | |
+ return fmt.Errorf("failed to execute manifest: %w", err) | |
+ } | |
+ | |
+ obj, gvk, err := extensionsapiserver.Codecs.UniversalDeserializer().Decode(buf.Bytes(), nil, &unstructured.Unstructured{}) | |
+ if err != nil { | |
+ return fmt.Errorf("could not decode raw: %w", err) | |
+ } | |
+ u, ok := obj.(*unstructured.Unstructured) | |
+ if !ok { | |
+ return fmt.Errorf("decoded into incorrect type, got %T, wanted %T", obj, &unstructured.Unstructured{}) | |
+ } | |
+ | |
+ key := types.NamespacedName{ | |
+ Namespace: u.GetNamespace(), | |
+ Name: u.GetName(), | |
+ } | |
+ err = client.Create(ctx, u) | |
+ if err != nil { | |
+ if apierrors.IsAlreadyExists(err) { | |
+ err = client.Get(ctx, key, u) | |
+ if err != nil { | |
+ return err | |
+ } | |
+ | |
+ u.SetResourceVersion(u.GetResourceVersion()) | |
+ err = client.Update(ctx, u) | |
+ if err != nil { | |
+ return fmt.Errorf("could not update %s %s: %w", gvk.Kind, key.String(), err) | |
+ } else { | |
+ log.WithValues("resource", u.GetName(), "kind", gvk.Kind).Info("updated object") | |
+ return nil | |
+ } | |
+ } | |
+ return err | |
+ } | |
+ | |
+ log.WithValues("resource", u.GetName(), "kind", gvk.Kind).Info("created object") | |
+ | |
+ return nil | |
+} | |
diff --git a/examples/kcp/config/main.go b/examples/kcp/config/main.go | |
new file mode 100644 | |
index 00000000..f41a4931 | |
--- /dev/null | |
+++ b/examples/kcp/config/main.go | |
@@ -0,0 +1,113 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package main | |
+ | |
+import ( | |
+ "os" | |
+ | |
+ kcpclienthelper "github.com/kcp-dev/apimachinery/v2/pkg/client" | |
+ apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" | |
+ "github.com/kcp-dev/kcp/sdk/apis/core" | |
+ corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" | |
+ tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime" | |
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme" | |
+ "k8s.io/client-go/rest" | |
+ ctrl "sigs.k8s.io/controller-runtime" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/client/config" | |
+ "sigs.k8s.io/controller-runtime/pkg/log/zap" | |
+ | |
+ "github.com/kcp-dev/controller-runtime/examples/kcp/config/consumers" | |
+ "github.com/kcp-dev/controller-runtime/examples/kcp/config/widgets" | |
+ widgetresources "github.com/kcp-dev/controller-runtime/examples/kcp/config/widgets/resources" | |
+ ctrllog "sigs.k8s.io/controller-runtime/pkg/log" | |
+) | |
+ | |
+// config is bootstrap set of assets for the controller-runtime examples. | |
+// It includes the following assets: | |
+// - crds/* for the Widget type - autogenerated from the Widget type definition | |
+// - widgets/resources/* - a set of Widget resources for KCP to manage. Automatically generated by kcp apigen | |
+// see Makefile & hack/update-codegen-crds.sh for more details | |
+ | |
+// It is intended to be running with higher privileges than the examples themselves | |
+// to ensure system (kcp) is bootstrapped. In real world scenarios, this would be | |
+// done by the platform operator to enable service providers to deploy their | |
+// controllers. | |
+ | |
+func init() { | |
+ utilruntime.Must(tenancyv1alpha1.AddToScheme(clientgoscheme.Scheme)) | |
+ utilruntime.Must(clientgoscheme.AddToScheme(clientgoscheme.Scheme)) | |
+ utilruntime.Must(corev1alpha1.AddToScheme(clientgoscheme.Scheme)) | |
+ utilruntime.Must(apisv1alpha1.AddToScheme(clientgoscheme.Scheme)) | |
+} | |
+ | |
+var ( | |
+ // clusterName is the workspace to host common APIs. | |
+ clusterName = logicalcluster.NewPath("root:widgets") | |
+) | |
+ | |
+func main() { | |
+ opts := zap.Options{ | |
+ Development: true, | |
+ } | |
+ ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) | |
+ | |
+ ctx := ctrl.SetupSignalHandler() | |
+ log := ctrllog.FromContext(ctx) | |
+ | |
+ restConfig, err := config.GetConfigWithContext("base") | |
+ if err != nil { | |
+ log.Error(err, "unable to get config") | |
+ os.Exit(1) | |
+ } | |
+ | |
+ restCopy := rest.CopyConfig(restConfig) | |
+ restRoot := rest.AddUserAgent(kcpclienthelper.SetCluster(restCopy, core.RootCluster.Path()), "bootstrap-root") | |
+ rootClient, err := client.New(restRoot, client.Options{}) | |
+ if err != nil { | |
+ log.Error(err, "unable to create client") | |
+ os.Exit(1) | |
+ } | |
+ | |
+ restCopy = rest.CopyConfig(restConfig) | |
+ restWidgets := rest.AddUserAgent(kcpclienthelper.SetCluster(restCopy, clusterName), "bootstrap-widgets") | |
+ widgetsClient, err := client.New(restWidgets, client.Options{}) | |
+ if err != nil { | |
+ log.Error(err, "unable to create client") | |
+ os.Exit(1) | |
+ } | |
+ | |
+ err = widgets.Bootstrap(ctx, rootClient) | |
+ if err != nil { | |
+ log.Error(err, "failed to bootstrap widgets") | |
+ os.Exit(1) | |
+ } | |
+ | |
+ err = widgetresources.Bootstrap(ctx, widgetsClient) | |
+ if err != nil { | |
+ log.Error(err, "failed to bootstrap resources") | |
+ os.Exit(1) | |
+ } | |
+ | |
+ err = consumers.Bootstrap(ctx, rootClient) | |
+ if err != nil { | |
+ log.Error(err, "failed to bootstrap consumers") | |
+ os.Exit(1) | |
+ } | |
+} | |
diff --git a/examples/kcp/config/widgets/bootstrap.go b/examples/kcp/config/widgets/bootstrap.go | |
new file mode 100644 | |
index 00000000..f68414ea | |
--- /dev/null | |
+++ b/examples/kcp/config/widgets/bootstrap.go | |
@@ -0,0 +1,43 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package widgets | |
+ | |
+import ( | |
+ "context" | |
+ "embed" | |
+ | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/log" | |
+ | |
+ confighelpers "github.com/kcp-dev/controller-runtime/examples/kcp/config/helpers" | |
+) | |
+ | |
+//go:embed *.yaml | |
+var fs embed.FS | |
+ | |
+// Bootstrap creates resources in this package by continuously retrying the list. | |
+// This is blocking, i.e. it only returns (with error) when the context is closed or with nil when | |
+// the bootstrapping is successfully completed. | |
+func Bootstrap( | |
+ ctx context.Context, | |
+ client client.Client, | |
+) error { | |
+ log := log.FromContext(ctx) | |
+ | |
+ log.Info("Bootstrapping widgets workspace") | |
+ return confighelpers.Bootstrap(ctx, client, fs) | |
+} | |
diff --git a/examples/kcp/config/widgets/resources/apiexport-data.my.domain.yaml b/examples/kcp/config/widgets/resources/apiexport-data.my.domain.yaml | |
new file mode 100644 | |
index 00000000..2b7bd641 | |
--- /dev/null | |
+++ b/examples/kcp/config/widgets/resources/apiexport-data.my.domain.yaml | |
@@ -0,0 +1,16 @@ | |
+apiVersion: apis.kcp.io/v1alpha1 | |
+kind: APIExport | |
+metadata: | |
+ creationTimestamp: null | |
+ name: data.my.domain | |
+spec: | |
+ latestResourceSchemas: | |
+ - v240406-90e42b7b.widgets.data.my.domain | |
+ permissionClaims: | |
+ - all: true | |
+ resource: configmaps | |
+ - all: true | |
+ resource: secrets | |
+ - all: true | |
+ resource: namespaces | |
+status: {} | |
diff --git a/examples/kcp/config/widgets/resources/apiresourceschema-widgets.data.my.domain.yaml b/examples/kcp/config/widgets/resources/apiresourceschema-widgets.data.my.domain.yaml | |
new file mode 100644 | |
index 00000000..a3d5bfbb | |
--- /dev/null | |
+++ b/examples/kcp/config/widgets/resources/apiresourceschema-widgets.data.my.domain.yaml | |
@@ -0,0 +1,52 @@ | |
+apiVersion: apis.kcp.io/v1alpha1 | |
+kind: APIResourceSchema | |
+metadata: | |
+ creationTimestamp: null | |
+ name: v240406-90e42b7b.widgets.data.my.domain | |
+spec: | |
+ group: data.my.domain | |
+ names: | |
+ kind: Widget | |
+ listKind: WidgetList | |
+ plural: widgets | |
+ singular: widget | |
+ scope: Namespaced | |
+ versions: | |
+ - name: v1alpha1 | |
+ schema: | |
+ description: Widget is the Schema for the widgets API | |
+ properties: | |
+ apiVersion: | |
+ description: |- | |
+ APIVersion defines the versioned schema of this representation of an object. | |
+ Servers should convert recognized schemas to the latest internal value, and | |
+ may reject unrecognized values. | |
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | |
+ type: string | |
+ kind: | |
+ description: |- | |
+ Kind is a string value representing the REST resource this object represents. | |
+ Servers may infer this from the endpoint the client submits requests to. | |
+ Cannot be updated. | |
+ In CamelCase. | |
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | |
+ type: string | |
+ metadata: | |
+ type: object | |
+ spec: | |
+ description: WidgetSpec defines the desired state of Widget | |
+ properties: | |
+ foo: | |
+ type: string | |
+ type: object | |
+ status: | |
+ description: WidgetStatus defines the observed state of Widget | |
+ properties: | |
+ total: | |
+ type: integer | |
+ type: object | |
+ type: object | |
+ served: true | |
+ storage: true | |
+ subresources: | |
+ status: {} | |
diff --git a/examples/kcp/config/widgets/resources/bootstrap.go b/examples/kcp/config/widgets/resources/bootstrap.go | |
new file mode 100644 | |
index 00000000..50daebb9 | |
--- /dev/null | |
+++ b/examples/kcp/config/widgets/resources/bootstrap.go | |
@@ -0,0 +1,47 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package resources | |
+ | |
+import ( | |
+ "context" | |
+ "embed" | |
+ | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/log" | |
+ | |
+ confighelpers "github.com/kcp-dev/controller-runtime/examples/kcp/config/helpers" | |
+) | |
+ | |
+//go:embed *.yaml | |
+var fs embed.FS | |
+ | |
+// Bootstrap creates resources in this package by continuously retrying the list. | |
+// This is blocking, i.e. it only returns (with error) when the context is closed or with nil when | |
+// the bootstrapping is successfully completed. | |
+func Bootstrap( | |
+ ctx context.Context, | |
+ client client.Client, | |
+) error { | |
+ log := log.FromContext(ctx) | |
+ | |
+ log.Info("Bootstrapping widgets resources") | |
+ if err := confighelpers.Bootstrap(ctx, client, fs); err != nil { | |
+ return err | |
+ } | |
+ | |
+ return nil | |
+} | |
diff --git a/examples/kcp/config/widgets/widgets-workspace.yaml b/examples/kcp/config/widgets/widgets-workspace.yaml | |
new file mode 100644 | |
index 00000000..7b3c863c | |
--- /dev/null | |
+++ b/examples/kcp/config/widgets/widgets-workspace.yaml | |
@@ -0,0 +1,14 @@ | |
+apiVersion: tenancy.kcp.io/v1alpha1 | |
+kind: Workspace | |
+metadata: | |
+ name: widgets | |
+ annotations: | |
+ bootstrap.kcp.io/create-only: "true" | |
+spec: | |
+ type: | |
+ name: universal | |
+ path: root | |
+ location: | |
+ selector: | |
+ matchLabels: | |
+ name: root | |
diff --git a/examples/kcp/config/widgets/widgets-workspacetype.yaml b/examples/kcp/config/widgets/widgets-workspacetype.yaml | |
new file mode 100644 | |
index 00000000..c9290aa3 | |
--- /dev/null | |
+++ b/examples/kcp/config/widgets/widgets-workspacetype.yaml | |
@@ -0,0 +1,12 @@ | |
+apiVersion: tenancy.kcp.io/v1alpha1 | |
+kind: WorkspaceType | |
+metadata: | |
+ name: widgets | |
+spec: | |
+ extend: | |
+ with: | |
+ - name: universal | |
+ path: root | |
+ defaultAPIBindings: | |
+ - path: root:widgets | |
+ export: data.my.domain | |
diff --git a/examples/kcp/controllers/configmap/reconciler.go b/examples/kcp/controllers/configmap/reconciler.go | |
new file mode 100644 | |
index 00000000..b08c3279 | |
--- /dev/null | |
+++ b/examples/kcp/controllers/configmap/reconciler.go | |
@@ -0,0 +1,145 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package configmap | |
+ | |
+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" | |
+ "sigs.k8s.io/controller-runtime/pkg/kcp" | |
+ | |
+ 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/kontext" | |
+ "sigs.k8s.io/controller-runtime/pkg/log" | |
+) | |
+ | |
+type Reconciler struct { | |
+ Client client.Client | |
+} | |
+ | |
+func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | |
+ log := log.FromContext(ctx).WithValues("cluster", req.ClusterName) | |
+ | |
+ // Test get | |
+ var cm corev1.ConfigMap | |
+ if err := r.Client.Get(ctx, req.NamespacedName, &cm); err != nil { | |
+ log.Error(err, "unable to get configmap") | |
+ return ctrl.Result{}, nil | |
+ } | |
+ | |
+ log.Info("Get: retrieved configMap") | |
+ if cm.Labels["name"] != "" { | |
+ response := fmt.Sprintf("hello-%s", cm.Labels["name"]) | |
+ | |
+ if cm.Labels["response"] != response { | |
+ cm.Labels["response"] = response | |
+ | |
+ // Test Update | |
+ if err := r.Client.Update(ctx, &cm); err != nil { | |
+ return ctrl.Result{}, err | |
+ } | |
+ | |
+ log.Info("Update: updated configMap") | |
+ return ctrl.Result{}, nil | |
+ } | |
+ } | |
+ | |
+ // Test list | |
+ var cms corev1.ConfigMapList | |
+ if err := r.Client.List(ctx, &cms); err != nil { | |
+ log.Error(err, "unable to list configmaps") | |
+ return ctrl.Result{}, nil | |
+ } | |
+ log.Info("List: got", "itemCount", len(cms.Items)) | |
+ found := false | |
+ for _, other := range cms.Items { | |
+ cluster, ok := kontext.ClusterFrom(ctx) | |
+ if !ok { | |
+ log.Info("List: got", "clusterName", cluster.String(), "namespace", other.Namespace, "name", other.Name) | |
+ } else if other.Name == cm.Name && other.Namespace == cm.Namespace { | |
+ if found { | |
+ return ctrl.Result{}, fmt.Errorf("there should be listed only one configmap with the given name '%s' for the given namespace '%s' when the clusterName is not available", cm.Name, cm.Namespace) | |
+ } | |
+ found = true | |
+ log.Info("Found in listed configmaps", "namespace", cm.Namespace, "name", cm.Name) | |
+ } | |
+ } | |
+ | |
+ // If the configmap has a namespace field, create the corresponding namespace | |
+ nsName, exists := cm.Data["namespace"] | |
+ if exists { | |
+ var namespace corev1.Namespace | |
+ if err := r.Client.Get(ctx, types.NamespacedName{Name: nsName}, &namespace); err != nil { | |
+ if !apierrors.IsNotFound(err) { | |
+ log.Error(err, "unable to get namespace") | |
+ return ctrl.Result{}, err | |
+ } | |
+ | |
+ // Need to create ns | |
+ namespace.SetName(nsName) | |
+ if err = r.Client.Create(ctx, &namespace); err != nil { | |
+ log.Error(err, "unable to create namespace") | |
+ return ctrl.Result{}, err | |
+ } | |
+ log.Info("Create: created ", "namespace", nsName) | |
+ return ctrl.Result{Requeue: true}, nil | |
+ } | |
+ log.Info("Exists", "namespace", nsName) | |
+ } | |
+ | |
+ // If the configmap has a secretData field, create a secret in the same namespace | |
+ // If the secret already exists but is out of sync, it will be non-destructively patched | |
+ secretData, exists := cm.Data["secretData"] | |
+ if exists { | |
+ var secret corev1.Secret | |
+ secret.SetName(cm.GetName()) | |
+ secret.SetNamespace(cm.GetNamespace()) | |
+ secret.SetOwnerReferences([]metav1.OwnerReference{{ | |
+ Name: cm.GetName(), | |
+ UID: cm.GetUID(), | |
+ APIVersion: "v1", | |
+ Kind: "ConfigMap", | |
+ Controller: func() *bool { x := true; return &x }(), | |
+ }}) | |
+ secret.Data = map[string][]byte{"dataFromCM": []byte(secretData)} | |
+ | |
+ operationResult, err := controllerutil.CreateOrPatch(ctx, r.Client, &secret, func() error { | |
+ secret.Data["dataFromCM"] = []byte(secretData) | |
+ return nil | |
+ }) | |
+ if err != nil { | |
+ log.Error(err, "unable to create or patch secret") | |
+ return ctrl.Result{}, err | |
+ } | |
+ log.Info(string(operationResult), "secret", secret.GetName()) | |
+ } | |
+ | |
+ return ctrl.Result{}, nil | |
+} | |
+ | |
+func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { | |
+ return ctrl.NewControllerManagedBy(mgr). | |
+ For(&corev1.ConfigMap{}). | |
+ Owns(&corev1.Secret{}). | |
+ Complete(kcp.WithClusterInContext(r)) | |
+} | |
diff --git a/examples/kcp/controllers/widget/reconciler.go b/examples/kcp/controllers/widget/reconciler.go | |
new file mode 100644 | |
index 00000000..0e33f2f8 | |
--- /dev/null | |
+++ b/examples/kcp/controllers/widget/reconciler.go | |
@@ -0,0 +1,83 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package widget | |
+ | |
+import ( | |
+ "context" | |
+ | |
+ "k8s.io/apimachinery/pkg/api/errors" | |
+ ctrl "sigs.k8s.io/controller-runtime" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/kcp" | |
+ "sigs.k8s.io/controller-runtime/pkg/log" | |
+ | |
+ datav1alpha1 "github.com/kcp-dev/controller-runtime/examples/kcp/apis/v1alpha1" | |
+) | |
+ | |
+// Reconciler reconciles a Widget object | |
+type Reconciler struct { | |
+ Client client.Client | |
+} | |
+ | |
+// Reconcile TODO | |
+func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | |
+ log := log.FromContext(ctx) | |
+ | |
+ // Include the clusterName from req.ObjectKey in the logger, similar to the namespace and name keys that are already | |
+ // there. | |
+ log = log.WithValues("clusterName", req.ClusterName) | |
+ | |
+ // You probably wouldn't need to do this, but if you wanted to list all instances across all logical clusters: | |
+ var allWidgets datav1alpha1.WidgetList | |
+ if err := r.Client.List(ctx, &allWidgets); err != nil { | |
+ return ctrl.Result{}, err | |
+ } | |
+ | |
+ log.Info("Listed all widgets across all workspaces", "count", len(allWidgets.Items)) | |
+ | |
+ log.Info("Getting widget") | |
+ var w datav1alpha1.Widget | |
+ if err := r.Client.Get(ctx, req.NamespacedName, &w); err != nil { | |
+ if errors.IsNotFound(err) { | |
+ // Normal - was deleted | |
+ return ctrl.Result{}, nil | |
+ } | |
+ return ctrl.Result{}, err | |
+ } | |
+ | |
+ log.Info("Listing all widgets in the current logical cluster") | |
+ var list datav1alpha1.WidgetList | |
+ if err := r.Client.List(ctx, &list); err != nil { | |
+ return ctrl.Result{}, err | |
+ } | |
+ | |
+ log.Info("Patching widget status to store total widget count in the current logical cluster") | |
+ orig := w.DeepCopy() | |
+ w.Status.Total = len(list.Items) | |
+ if err := r.Client.Status().Patch(ctx, &w, client.MergeFromWithOptions(orig, client.MergeFromWithOptimisticLock{})); err != nil { | |
+ return ctrl.Result{}, err | |
+ } | |
+ | |
+ return ctrl.Result{}, nil | |
+} | |
+ | |
+// SetupWithManager sets up the controller with the Manager. | |
+func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { | |
+ return ctrl.NewControllerManagedBy(mgr). | |
+ For(&datav1alpha1.Widget{}). | |
+ Complete(kcp.WithClusterInContext(r)) | |
+} | |
diff --git a/examples/kcp/go.mod b/examples/kcp/go.mod | |
new file mode 100644 | |
index 00000000..27002ea5 | |
--- /dev/null | |
+++ b/examples/kcp/go.mod | |
@@ -0,0 +1,112 @@ | |
+module github.com/kcp-dev/controller-runtime/examples/kcp | |
+ | |
+go 1.22.0 | |
+ | |
+// IMPORTANT: This is only an example replace directive. This is so examples can be run with the latest version of controller-runtime. | |
+// In your own projects, you should not use replace directives like this. Instead, you should replace, but with kcp-dev/controller-runtime instead of ../../ | |
+replace sigs.k8s.io/controller-runtime => ../../ | |
+ | |
+require ( | |
+ github.com/google/go-cmp v0.6.0 | |
+ github.com/kcp-dev/apimachinery/v2 v2.0.0 | |
+ github.com/kcp-dev/kcp/sdk v0.24.0 | |
+ github.com/kcp-dev/logicalcluster/v3 v3.0.5 | |
+ k8s.io/api v0.31.1 | |
+ k8s.io/apiextensions-apiserver v0.31.1 | |
+ k8s.io/apimachinery v0.31.1 | |
+ k8s.io/client-go v0.31.1 | |
+ k8s.io/klog/v2 v2.130.1 | |
+ sigs.k8s.io/controller-runtime v0.19.0 | |
+) | |
+ | |
+require ( | |
+ github.com/NYTimes/gziphandler v1.1.1 // indirect | |
+ github.com/antlr4-go/antlr/v4 v4.13.0 // indirect | |
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect | |
+ github.com/beorn7/perks v1.0.1 // indirect | |
+ github.com/blang/semver/v4 v4.0.0 // indirect | |
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect | |
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect | |
+ github.com/coreos/go-semver v0.3.1 // indirect | |
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect | |
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | |
+ github.com/emicklei/go-restful/v3 v3.12.1 // indirect | |
+ github.com/evanphx/json-patch/v5 v5.9.0 // indirect | |
+ github.com/felixge/httpsnoop v1.0.4 // indirect | |
+ github.com/fsnotify/fsnotify v1.7.0 // indirect | |
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect | |
+ github.com/go-logr/logr v1.4.2 // indirect | |
+ github.com/go-logr/stdr v1.2.2 // indirect | |
+ github.com/go-logr/zapr v1.3.0 // indirect | |
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect | |
+ github.com/go-openapi/jsonreference v0.21.0 // indirect | |
+ github.com/go-openapi/swag v0.23.0 // indirect | |
+ github.com/gogo/protobuf v1.3.2 // indirect | |
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | |
+ github.com/golang/protobuf v1.5.4 // indirect | |
+ github.com/google/cel-go v0.20.1 // indirect | |
+ github.com/google/gnostic-models v0.6.8 // indirect | |
+ github.com/google/gofuzz v1.2.0 // indirect | |
+ github.com/google/uuid v1.6.0 // indirect | |
+ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect | |
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect | |
+ github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect | |
+ github.com/imdario/mergo v0.3.16 // indirect | |
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
+ github.com/josharian/intern v1.0.0 // indirect | |
+ github.com/json-iterator/go v1.1.12 // indirect | |
+ github.com/mailru/easyjson v0.7.7 // indirect | |
+ github.com/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/gomega v1.33.1 // indirect | |
+ github.com/pkg/errors v0.9.1 // indirect | |
+ github.com/prometheus/client_golang v1.19.1 // indirect | |
+ github.com/prometheus/client_model v0.6.1 // indirect | |
+ github.com/prometheus/common v0.55.0 // indirect | |
+ github.com/prometheus/procfs v0.15.1 // indirect | |
+ github.com/spf13/cobra v1.8.1 // indirect | |
+ github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace // indirect | |
+ github.com/stoewer/go-strcase v1.3.0 // indirect | |
+ github.com/x448/float16 v0.8.4 // indirect | |
+ go.etcd.io/etcd/api/v3 v3.5.14 // indirect | |
+ go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect | |
+ go.etcd.io/etcd/client/v3 v3.5.14 // indirect | |
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect | |
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect | |
+ go.opentelemetry.io/otel v1.28.0 // indirect | |
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect | |
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect | |
+ go.opentelemetry.io/otel/metric v1.28.0 // indirect | |
+ go.opentelemetry.io/otel/sdk v1.28.0 // indirect | |
+ go.opentelemetry.io/otel/trace v1.28.0 // indirect | |
+ go.opentelemetry.io/proto/otlp v1.3.1 // indirect | |
+ go.uber.org/multierr v1.11.0 // indirect | |
+ go.uber.org/zap v1.26.0 // indirect | |
+ golang.org/x/crypto v0.27.0 // indirect | |
+ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect | |
+ golang.org/x/net v0.29.0 // indirect | |
+ golang.org/x/oauth2 v0.23.0 // indirect | |
+ golang.org/x/sync v0.8.0 // indirect | |
+ golang.org/x/sys v0.25.0 // indirect | |
+ golang.org/x/term v0.24.0 // indirect | |
+ golang.org/x/text v0.18.0 // indirect | |
+ golang.org/x/time v0.6.0 // indirect | |
+ gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect | |
+ google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect | |
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect | |
+ google.golang.org/grpc v1.65.0 // indirect | |
+ google.golang.org/protobuf v1.34.2 // indirect | |
+ gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect | |
+ gopkg.in/inf.v0 v0.9.1 // indirect | |
+ gopkg.in/yaml.v2 v2.4.0 // indirect | |
+ gopkg.in/yaml.v3 v3.0.1 // indirect | |
+ k8s.io/apiserver v0.31.1 // indirect | |
+ k8s.io/component-base v0.31.1 // indirect | |
+ k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect | |
+ k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect | |
+ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect | |
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect | |
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect | |
+ sigs.k8s.io/yaml v1.4.0 // indirect | |
+) | |
diff --git a/examples/kcp/go.sum b/examples/kcp/go.sum | |
new file mode 100644 | |
index 00000000..8a043a34 | |
--- /dev/null | |
+++ b/examples/kcp/go.sum | |
@@ -0,0 +1,308 @@ | |
+github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= | |
+github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= | |
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= | |
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= | |
+github.com/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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | |
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | |
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= | |
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= | |
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | |
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | |
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | |
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | |
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= | |
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= | |
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= | |
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | |
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | |
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
+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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | |
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | |
+github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= | |
+github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= | |
+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.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= | |
+github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= | |
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= | |
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | |
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | |
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | |
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= | |
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= | |
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | |
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= | |
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | |
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | |
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | |
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= | |
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= | |
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= | |
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= | |
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= | |
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= | |
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= | |
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= | |
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= | |
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= | |
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | |
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | |
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | |
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= | |
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= | |
+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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | |
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | |
+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.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= | |
+github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= | |
+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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | |
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | |
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | |
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | |
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | |
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | |
+github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= | |
+github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= | |
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | |
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | |
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | |
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | |
+github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= | |
+github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= | |
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= | |
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | |
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= | |
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= | |
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= | |
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= | |
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= | |
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= | |
+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/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= | |
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= | |
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= | |
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | |
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | |
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | |
+github.com/kcp-dev/apimachinery/v2 v2.0.0 h1:hQuhBBh+AvUYYMRG+nDzo1VXxNCdMAE95wSD2uB7nxw= | |
+github.com/kcp-dev/apimachinery/v2 v2.0.0/go.mod h1:cXCx7fku8/rYK23PNEBRLQ5ByoABoA+CZeJNC81TO0g= | |
+github.com/kcp-dev/kcp/sdk v0.24.0 h1:ZTfStDOQshVU2cnrqjgMo9xb0VNblkmrgMRtl0PCQEY= | |
+github.com/kcp-dev/kcp/sdk v0.24.0/go.mod h1:Pd2xxw/qhgfF2xgHolVwheq9VOJwPtNrBmxgBlYmjfk= | |
+github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU= | |
+github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY= | |
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | |
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | |
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | |
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | |
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | |
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | |
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= | |
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= | |
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | |
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | |
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | |
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | |
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | |
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= | |
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= | |
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= | |
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= | |
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= | |
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= | |
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | |
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | |
+github.com/pmezard/go-difflib v1.0.0/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/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= | |
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= | |
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | |
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | |
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= | |
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= | |
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | |
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | |
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= | |
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= | |
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | |
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | |
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | |
+github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= | |
+github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= | |
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= | |
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= | |
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | |
+github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= | |
+github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | |
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= | |
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= | |
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | |
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | |
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | |
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | |
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | |
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | |
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | |
+github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= | |
+github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= | |
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | |
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | |
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= | |
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= | |
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= | |
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= | |
+go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= | |
+go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= | |
+go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ= | |
+go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI= | |
+go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8= | |
+go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg= | |
+go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg= | |
+go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk= | |
+go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M= | |
+go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0= | |
+go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA= | |
+go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw= | |
+go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok= | |
+go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= | |
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= | |
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= | |
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= | |
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= | |
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= | |
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= | |
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= | |
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= | |
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= | |
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= | |
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= | |
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= | |
+go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= | |
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= | |
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= | |
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= | |
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= | |
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= | |
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | |
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | |
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= | |
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | |
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= | |
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= | |
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | |
+golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= | |
+golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= | |
+golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= | |
+golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= | |
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | |
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= | |
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= | |
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= | |
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | |
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= | |
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | |
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= | |
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | |
+golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= | |
+golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= | |
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
+golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= | |
+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= | |
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= | |
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | |
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | |
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | |
+golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= | |
+golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= | |
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= | |
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= | |
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= | |
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= | |
+google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= | |
+google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= | |
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= | |
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= | |
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= | |
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= | |
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | |
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | |
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | |
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | |
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= | |
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= | |
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= | |
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | |
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= | |
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= | |
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | |
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | |
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
+k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= | |
+k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= | |
+k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= | |
+k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= | |
+k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= | |
+k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= | |
+k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= | |
+k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= | |
+k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= | |
+k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= | |
+k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= | |
+k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= | |
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= | |
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= | |
+k8s.io/kms v0.31.1 h1:cGLyV3cIwb0ovpP/jtyIe2mEuQ/MkbhmeBF2IYCA9Io= | |
+k8s.io/kms v0.31.1/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94= | |
+k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= | |
+k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= | |
+k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= | |
+k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= | |
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= | |
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= | |
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= | |
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= | |
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= | |
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= | |
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= | |
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= | |
diff --git a/examples/kcp/hack/go-install.sh b/examples/kcp/hack/go-install.sh | |
new file mode 100755 | |
index 00000000..dd323c56 | |
--- /dev/null | |
+++ b/examples/kcp/hack/go-install.sh | |
@@ -0,0 +1,62 @@ | |
+#!/usr/bin/env bash | |
+ | |
+# Copyright 2024 The KCP Authors. | |
+# | |
+# Licensed under the Apache License, Version 2.0 (the "License"); | |
+# you may not use this file except in compliance with the License. | |
+# You may obtain a copy of the License at | |
+# | |
+# http://www.apache.org/licenses/LICENSE-2.0 | |
+# | |
+# Unless required by applicable law or agreed to in writing, software | |
+# distributed under the License is distributed on an "AS IS" BASIS, | |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+# See the License for the specific language governing permissions and | |
+# limitations under the License. | |
+ | |
+# Originally copied from | |
+# https://github.com/kubernetes-sigs/cluster-api-provider-gcp/blob/c26a68b23e9317323d5d37660fe9d29b3d2ff40c/scripts/go_install.sh | |
+ | |
+set -o errexit | |
+set -o nounset | |
+set -o pipefail | |
+ | |
+if [[ -z "${1:-}" ]]; then | |
+ echo "must provide module as first parameter" | |
+ exit 1 | |
+fi | |
+ | |
+if [[ -z "${2:-}" ]]; then | |
+ echo "must provide binary name as second parameter" | |
+ exit 1 | |
+fi | |
+ | |
+if [[ -z "${3:-}" ]]; then | |
+ echo "must provide version as third parameter" | |
+ exit 1 | |
+fi | |
+ | |
+if [[ -z "${GOBIN:-}" ]]; then | |
+ echo "GOBIN is not set. Must set GOBIN to install the bin in a specified directory." | |
+ exit 1 | |
+fi | |
+ | |
+mkdir -p "${GOBIN}" | |
+ | |
+tmp_dir=$(mktemp -d -t goinstall_XXXXXXXXXX) | |
+function clean { | |
+ rm -rf "${tmp_dir}" | |
+} | |
+trap clean EXIT | |
+ | |
+rm "${GOBIN}/${2}"* > /dev/null 2>&1 || true | |
+ | |
+cd "${tmp_dir}" | |
+ | |
+# create a new module in the tmp directory | |
+go mod init fake/mod | |
+ | |
+# install the golang module specified as the first argument | |
+go install -tags kcptools "${1}@${3}" | |
+mv "${GOBIN}/${2}" "${GOBIN}/${2}-${3}" | |
+ln -sf "${GOBIN}/${2}-${3}" "${GOBIN}/${2}" | |
diff --git a/examples/kcp/hack/tools/go.mod b/examples/kcp/hack/tools/go.mod | |
new file mode 100644 | |
index 00000000..e2c3eac3 | |
--- /dev/null | |
+++ b/examples/kcp/hack/tools/go.mod | |
@@ -0,0 +1,61 @@ | |
+module sigs.k8s.io/controller-runtime/hack/tools | |
+ | |
+go 1.21 | |
+ | |
+toolchain go1.21.5 | |
+ | |
+require ( | |
+ github.com/joelanford/go-apidiff v0.8.2 | |
+ sigs.k8s.io/controller-tools v0.14.0 | |
+) | |
+ | |
+require ( | |
+ dario.cat/mergo v1.0.0 // indirect | |
+ github.com/Microsoft/go-winio v0.6.1 // indirect | |
+ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect | |
+ github.com/cloudflare/circl v1.3.7 // indirect | |
+ github.com/cyphar/filepath-securejoin v0.2.4 // indirect | |
+ github.com/emirpasic/gods v1.18.1 // indirect | |
+ github.com/fatih/color v1.16.0 // indirect | |
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | |
+ github.com/go-git/go-billy/v5 v5.5.0 // indirect | |
+ github.com/go-git/go-git/v5 v5.11.0 // indirect | |
+ github.com/go-logr/logr v1.3.0 // indirect | |
+ github.com/gobuffalo/flect v1.0.2 // indirect | |
+ github.com/gogo/protobuf v1.3.2 // indirect | |
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | |
+ github.com/google/gofuzz v1.2.0 // indirect | |
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect | |
+ github.com/json-iterator/go v1.1.12 // indirect | |
+ github.com/kevinburke/ssh_config v1.2.0 // indirect | |
+ github.com/mattn/go-colorable v0.1.13 // indirect | |
+ github.com/mattn/go-isatty v0.0.20 // indirect | |
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | |
+ github.com/modern-go/reflect2 v1.0.2 // indirect | |
+ github.com/pjbgf/sha1cd v0.3.0 // indirect | |
+ github.com/sergi/go-diff v1.1.0 // indirect | |
+ github.com/skeema/knownhosts v1.2.1 // indirect | |
+ github.com/spf13/cobra v1.8.0 // indirect | |
+ github.com/spf13/pflag v1.0.5 // indirect | |
+ github.com/xanzy/ssh-agent v0.3.3 // indirect | |
+ golang.org/x/crypto v0.18.0 // indirect | |
+ golang.org/x/exp v0.0.0-20230811145653-3b0b5b66b5f1 // indirect | |
+ golang.org/x/mod v0.14.0 // indirect | |
+ golang.org/x/net v0.20.0 // indirect | |
+ golang.org/x/sys v0.16.0 // indirect | |
+ golang.org/x/text v0.14.0 // indirect | |
+ golang.org/x/tools v0.17.0 // indirect | |
+ gopkg.in/inf.v0 v0.9.1 // indirect | |
+ gopkg.in/warnings.v0 v0.1.2 // indirect | |
+ gopkg.in/yaml.v2 v2.4.0 // indirect | |
+ gopkg.in/yaml.v3 v3.0.1 // indirect | |
+ k8s.io/api v0.29.0 // indirect | |
+ k8s.io/apiextensions-apiserver v0.29.0 // indirect | |
+ k8s.io/apimachinery v0.29.0 // indirect | |
+ k8s.io/klog/v2 v2.110.1 // indirect | |
+ k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect | |
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect | |
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect | |
+ sigs.k8s.io/yaml v1.4.0 // indirect | |
+) | |
diff --git a/examples/kcp/hack/tools/go.sum b/examples/kcp/hack/tools/go.sum | |
new file mode 100644 | |
index 00000000..151b3772 | |
--- /dev/null | |
+++ b/examples/kcp/hack/tools/go.sum | |
@@ -0,0 +1,240 @@ | |
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= | |
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= | |
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= | |
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= | |
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= | |
+github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= | |
+github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= | |
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= | |
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= | |
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= | |
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= | |
+github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= | |
+github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= | |
+github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= | |
+github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= | |
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | |
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= | |
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= | |
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= | |
+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= | |
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= | |
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= | |
+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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | |
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | |
+github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= | |
+github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= | |
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= | |
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= | |
+github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= | |
+github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= | |
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= | |
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= | |
+github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= | |
+github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= | |
+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/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= | |
+github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= | |
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | |
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | |
+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/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | |
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | |
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | |
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | |
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | |
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | |
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | |
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | |
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= | |
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= | |
+github.com/joelanford/go-apidiff v0.8.2 h1:AvHPY3vYINr6I2xGMHqhDKoszpdsDmH4VHZtit6NJKk= | |
+github.com/joelanford/go-apidiff v0.8.2/go.mod h1:3fPoVVLpPCaU8aOuR7X1xDABzcWbLGKeeMerR2Pxulk= | |
+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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= | |
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= | |
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | |
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | |
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | |
+github.com/kr/pretty v0.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/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.16/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | |
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | |
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | |
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | |
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | |
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | |
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= | |
+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/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= | |
+github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= | |
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= | |
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= | |
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | |
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | |
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | |
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | |
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | |
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= | |
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= | |
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= | |
+github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= | |
+github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= | |
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= | |
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= | |
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | |
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | |
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | |
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | |
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | |
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | |
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | |
+github.com/stretchr/testify v1.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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= | |
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= | |
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | |
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | |
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | |
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | |
+golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= | |
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= | |
+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= | |
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= | |
+golang.org/x/exp v0.0.0-20230811145653-3b0b5b66b5f1 h1:EFPukSCgigmk1W0azH8EMt97AoMjMOgtJ3Z3sGM9AGw= | |
+golang.org/x/exp v0.0.0-20230811145653-3b0b5b66b5f1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= | |
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | |
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | |
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= | |
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | |
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | |
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | |
+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= | |
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= | |
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | |
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= | |
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= | |
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= | |
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | |
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | |
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= | |
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | |
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | |
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | |
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= | |
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | |
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= | |
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= | |
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= | |
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
+golang.org/x/text v0.3.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | |
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | |
+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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | |
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | |
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | |
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | |
+golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= | |
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= | |
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | |
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | |
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= | |
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | |
+gopkg.in/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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= | |
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= | |
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | |
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | |
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
+k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= | |
+k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= | |
+k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= | |
+k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= | |
+k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= | |
+k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= | |
+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/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= | |
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= | |
+sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A= | |
+sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc= | |
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= | |
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= | |
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= | |
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= | |
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= | |
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= | |
diff --git a/examples/kcp/hack/tools/tools.go b/examples/kcp/hack/tools/tools.go | |
new file mode 100644 | |
index 00000000..481a7c6f | |
--- /dev/null | |
+++ b/examples/kcp/hack/tools/tools.go | |
@@ -0,0 +1,25 @@ | |
+// +build tools | |
+ | |
+/* | |
+Copyright 2019 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. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+// This package imports things required by build scripts, to force `go mod` to see them as dependencies | |
+package tools | |
+ | |
+import ( | |
+ _ "github.com/joelanford/go-apidiff" | |
+ _ "sigs.k8s.io/controller-tools/cmd/controller-gen" | |
+) | |
diff --git a/examples/kcp/hack/update-codegen-crds.sh b/examples/kcp/hack/update-codegen-crds.sh | |
new file mode 100755 | |
index 00000000..03524149 | |
--- /dev/null | |
+++ b/examples/kcp/hack/update-codegen-crds.sh | |
@@ -0,0 +1,50 @@ | |
+#!/usr/bin/env bash | |
+ | |
+# Copyright 2024 The KCP Authors. | |
+# | |
+# Licensed under the Apache License, Version 2.0 (the "License"); | |
+# you may not use this file except in compliance with the License. | |
+# You may obtain a copy of the License at | |
+# | |
+# http://www.apache.org/licenses/LICENSE-2.0 | |
+# | |
+# Unless required by applicable law or agreed to in writing, software | |
+# distributed under the License is distributed on an "AS IS" BASIS, | |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+# See the License for the specific language governing permissions and | |
+# limitations under the License. | |
+ | |
+set -o errexit | |
+set -o nounset | |
+set -o pipefail | |
+set -o xtrace | |
+ | |
+if [[ -z "${CONTROLLER_GEN:-}" ]]; then | |
+ echo "You must either set CONTROLLER_GEN to the path to controller-gen or invoke via make" | |
+ exit 1 | |
+fi | |
+ | |
+REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | |
+ | |
+# Update generated CRD YAML | |
+( | |
+ cd "${REPO_ROOT}/apis" | |
+ "${CONTROLLER_GEN}" \ | |
+ crd \ | |
+ rbac:roleName=manager-role \ | |
+ webhook \ | |
+ paths="./..." \ | |
+ output:crd:artifacts:config="${REPO_ROOT}"/config/crds | |
+) | |
+ | |
+for CRD in "${REPO_ROOT}"/config/crds/*.yaml; do | |
+ if [ -f "${CRD}-patch" ]; then | |
+ echo "Applying ${CRD}" | |
+ ${YAML_PATCH} -o "${CRD}-patch" < "${CRD}" > "${CRD}.patched" | |
+ mv "${CRD}.patched" "${CRD}" | |
+ fi | |
+done | |
+ | |
+( | |
+ ${KCP_APIGEN_GEN} --input-dir "${REPO_ROOT}"/config/crds --output-dir "${REPO_ROOT}"/config/widgets/resources | |
+) | |
diff --git a/examples/kcp/main.go b/examples/kcp/main.go | |
new file mode 100644 | |
index 00000000..64806434 | |
--- /dev/null | |
+++ b/examples/kcp/main.go | |
@@ -0,0 +1,199 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package main | |
+ | |
+import ( | |
+ "context" | |
+ "flag" | |
+ "fmt" | |
+ "os" | |
+ | |
+ kcpclienthelper "github.com/kcp-dev/apimachinery/v2/pkg/client" | |
+ "github.com/kcp-dev/controller-runtime/examples/kcp/controllers/configmap" | |
+ "github.com/kcp-dev/controller-runtime/examples/kcp/controllers/widget" | |
+ "k8s.io/apimachinery/pkg/types" | |
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime" | |
+ "k8s.io/client-go/discovery" | |
+ "k8s.io/client-go/kubernetes/scheme" | |
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme" | |
+ "k8s.io/client-go/rest" | |
+ "k8s.io/klog/v2" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/client/config" | |
+ | |
+ datav1alpha1 "github.com/kcp-dev/controller-runtime/examples/kcp/apis/v1alpha1" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ | |
+ // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) | |
+ // to ensure that exec-entrypoint and run can make use of them. | |
+ _ "k8s.io/client-go/plugin/pkg/client/auth" | |
+ ctrl "sigs.k8s.io/controller-runtime" | |
+ "sigs.k8s.io/controller-runtime/pkg/healthz" | |
+ "sigs.k8s.io/controller-runtime/pkg/kcp" | |
+ | |
+ apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" | |
+) | |
+ | |
+func init() { | |
+ utilruntime.Must(clientgoscheme.AddToScheme(scheme.Scheme)) | |
+ utilruntime.Must(datav1alpha1.AddToScheme(scheme.Scheme)) | |
+ utilruntime.Must(apisv1alpha1.AddToScheme(scheme.Scheme)) | |
+} | |
+ | |
+func main() { | |
+ var opts Options | |
+ opts.addFlags(flag.CommandLine) | |
+ flag.Parse() | |
+ flag.Lookup("v").Value.Set("6") | |
+ | |
+ ctx := ctrl.SetupSignalHandler() | |
+ if err := runController(ctx, opts); err != nil { | |
+ fmt.Fprintf(os.Stderr, "%v\n", err) | |
+ os.Exit(1) | |
+ } | |
+} | |
+ | |
+type Options struct { | |
+ MetricsAddr string | |
+ EnableLeaderElection bool | |
+ ProbeAddr string | |
+ APIExportName string | |
+ KubeconfigContext string | |
+} | |
+ | |
+func (o *Options) addFlags(fs *flag.FlagSet) { | |
+ fs.StringVar(&o.KubeconfigContext, "context", "", "kubeconfig context") | |
+ fs.StringVar(&o.APIExportName, "api-export-name", "data.my.domain", "The name of the APIExport.") | |
+ fs.StringVar(&o.MetricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") | |
+ fs.StringVar(&o.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") | |
+ fs.BoolVar(&o.EnableLeaderElection, "leader-elect", false, | |
+ "Enable leader election for controller manager. "+ | |
+ "Enabling this will ensure there is only one active controller manager.") | |
+ | |
+ klog.InitFlags(fs) | |
+} | |
+ | |
+func runController(ctx context.Context, opts Options) error { | |
+ log := ctrl.Log.WithName("setup").WithValues("api-export-name", opts.APIExportName) | |
+ | |
+ // Important: We use non-controller-runtime client loader so we can always | |
+ // be sure we have correct kubeconfig file. This ease the development and maintenance | |
+ // of the example. In production, you should use the controller-runtime client loader | |
+ // to load the kubeconfig file dedicated to workspace where APIExport is located. | |
+ // restConfig := ctrl.GetConfigOrDie() | |
+ widgetsCluster := logicalcluster.NewPath("root:widgets") | |
+ widgetsConfig, err := config.GetConfigWithContext("base") | |
+ if err != nil { | |
+ return fmt.Errorf("unable to get config: %w", err) | |
+ } | |
+ widgetsConfig = rest.AddUserAgent(kcpclienthelper.SetCluster(widgetsConfig, widgetsCluster), "kcp-controller-runtime-example") | |
+ | |
+ ctrlOpts := ctrl.Options{ | |
+ HealthProbeBindAddress: opts.ProbeAddr, | |
+ LeaderElection: opts.EnableLeaderElection, | |
+ LeaderElectionID: "68a0532d.my.domain", | |
+ LeaderElectionConfig: widgetsConfig, | |
+ } | |
+ | |
+ // create a manager, either with or without kcp support | |
+ var mgr ctrl.Manager | |
+ if isKcp, err := kcpAPIsGroupPresent(widgetsConfig); err != nil { | |
+ return fmt.Errorf("error checking for kcp APIs group: %w", err) | |
+ } else if isKcp { | |
+ log.Info("Looking up virtual workspace URL") | |
+ exportConfig, err := restConfigForAPIExport(ctx, widgetsConfig, opts.APIExportName) | |
+ if err != nil { | |
+ return fmt.Errorf("error looking up virtual workspace URL: %w", err) | |
+ } | |
+ log.Info("Using virtual workspace URL", "url", exportConfig.Host) | |
+ | |
+ mgr, err = kcp.NewClusterAwareManager(exportConfig, ctrlOpts) | |
+ if err != nil { | |
+ return fmt.Errorf("unable to create cluster aware manager: %w", err) | |
+ } | |
+ } else { | |
+ log.Info("The apis.kcp.dev group is not present - creating standard manager") | |
+ mgr, err = ctrl.NewManager(widgetsConfig, ctrlOpts) | |
+ if err != nil { | |
+ return fmt.Errorf("unable to create manager: %w", err) | |
+ } | |
+ } | |
+ | |
+ // create controllers | |
+ if err = (&configmap.Reconciler{Client: mgr.GetClient()}).SetupWithManager(mgr); err != nil { | |
+ return fmt.Errorf("unable to create configmap controller: %w", err) | |
+ } | |
+ if err = (&widget.Reconciler{Client: mgr.GetClient()}).SetupWithManager(mgr); err != nil { | |
+ return fmt.Errorf("unable to create widget controller: %w", err) | |
+ } | |
+ | |
+ if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { | |
+ return fmt.Errorf("unable to set up health check: %w", err) | |
+ } | |
+ if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { | |
+ return fmt.Errorf("unable to set up ready check: %w", err) | |
+ } | |
+ | |
+ log.Info("Starting manager") | |
+ return mgr.Start(ctx) | |
+} | |
+ | |
+// restConfigForAPIExport returns a *rest.Config properly configured to communicate with the endpoint for the | |
+// APIExport's virtual workspace. | |
+func restConfigForAPIExport(ctx context.Context, cfg *rest.Config, apiExportName string) (*rest.Config, error) { | |
+ apiExportClient, err := client.New(cfg, client.Options{}) | |
+ if err != nil { | |
+ return nil, fmt.Errorf("error creating APIExport client: %w", err) | |
+ } | |
+ | |
+ var apiExport apisv1alpha1.APIExport | |
+ if err := apiExportClient.Get(ctx, types.NamespacedName{Name: apiExportName}, &apiExport); err != nil { | |
+ return nil, fmt.Errorf("error getting APIExport %q: %w", apiExportName, err) | |
+ } | |
+ | |
+ if len(apiExport.Status.VirtualWorkspaces) < 1 { | |
+ return nil, fmt.Errorf("APIExport %q status.virtualWorkspaces is empty", apiExportName) | |
+ } | |
+ | |
+ // create a new rest.Config with the APIExport's virtual workspace URL | |
+ exportConfig := rest.CopyConfig(cfg) | |
+ exportConfig.Host = apiExport.Status.VirtualWorkspaces[0].URL // TODO(ncdc): sharding support | |
+ | |
+ return exportConfig, nil | |
+} | |
+ | |
+func kcpAPIsGroupPresent(cfg *rest.Config) (bool, error) { | |
+ discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg) | |
+ if err != nil { | |
+ return false, fmt.Errorf("failed to create discovery client: %w", err) | |
+ } | |
+ apiGroupList, err := discoveryClient.ServerGroups() | |
+ if err != nil { | |
+ return false, fmt.Errorf("failed to get server groups: %w", err) | |
+ } | |
+ | |
+ for _, group := range apiGroupList.Groups { | |
+ if group.Name == apisv1alpha1.SchemeGroupVersion.Group { | |
+ for _, version := range group.Versions { | |
+ if version.Version == apisv1alpha1.SchemeGroupVersion.Version { | |
+ return true, nil | |
+ } | |
+ } | |
+ } | |
+ } | |
+ return false, nil | |
+} | |
diff --git a/examples/kcp/test/e2e/audit-policy.yaml b/examples/kcp/test/e2e/audit-policy.yaml | |
new file mode 100644 | |
index 00000000..9b1b0384 | |
--- /dev/null | |
+++ b/examples/kcp/test/e2e/audit-policy.yaml | |
@@ -0,0 +1,30 @@ | |
+apiVersion: audit.k8s.io/v1 | |
+kind: Policy | |
+omitStages: | |
+ - RequestReceived | |
+omitManagedFields: true | |
+rules: | |
+ - level: None | |
+ nonResourceURLs: | |
+ - "/api*" | |
+ - "/version" | |
+ | |
+ - level: Metadata | |
+ resources: | |
+ - group: "" | |
+ resources: ["secrets", "configmaps"] | |
+ - group: "authorization.k8s.io" | |
+ resources: ["subjectaccessreviews"] | |
+ | |
+ - level: Metadata | |
+ verbs: ["list", "watch"] | |
+ | |
+ - level: Metadata | |
+ verbs: ["get", "delete"] | |
+ omitStages: | |
+ - ResponseStarted | |
+ | |
+ - level: RequestResponse | |
+ verbs: ["create", "update", "patch"] | |
+ omitStages: | |
+ - ResponseStarted | |
diff --git a/examples/kcp/test/e2e/controller_test.go b/examples/kcp/test/e2e/controller_test.go | |
new file mode 100644 | |
index 00000000..8a28ddf0 | |
--- /dev/null | |
+++ b/examples/kcp/test/e2e/controller_test.go | |
@@ -0,0 +1,336 @@ | |
+package e2e | |
+ | |
+import ( | |
+ "context" | |
+ "flag" | |
+ "fmt" | |
+ "math/rand" | |
+ "strings" | |
+ "testing" | |
+ "time" | |
+ | |
+ "github.com/google/go-cmp/cmp" | |
+ apierrors "k8s.io/apimachinery/pkg/api/errors" | |
+ | |
+ kcpclienthelper "github.com/kcp-dev/apimachinery/v2/pkg/client" | |
+ apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" | |
+ corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" | |
+ tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" | |
+ "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/util/conditions" | |
+ | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ | |
+ corev1 "k8s.io/api/core/v1" | |
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
+ "k8s.io/apimachinery/pkg/runtime" | |
+ "k8s.io/apimachinery/pkg/util/errors" | |
+ "k8s.io/apimachinery/pkg/util/wait" | |
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme" | |
+ "k8s.io/client-go/rest" | |
+ | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/client/config" | |
+ | |
+ datav1alpha1 "github.com/kcp-dev/controller-runtime/examples/kcp/apis/v1alpha1" | |
+) | |
+ | |
+// The tests in this package expect to be called when: | |
+// - kcp is running | |
+// - the controller-manager from this repo is running | |
+// | |
+// We can then check that the controllers defined here are working as expected. | |
+ | |
+var workspaceName string | |
+ | |
+func init() { | |
+ rand.Seed(time.Now().Unix()) | |
+ flag.StringVar(&workspaceName, "workspace", "", "Workspace in which to run these tests.") | |
+} | |
+ | |
+func parentWorkspace(t *testing.T) logicalcluster.Path { | |
+ if workspaceName == "" { | |
+ t.Fatal("--workspace cannot be empty") | |
+ } | |
+ | |
+ return logicalcluster.NewPath(workspaceName) | |
+} | |
+ | |
+func loadClusterConfig(t *testing.T, clusterName logicalcluster.Path) *rest.Config { | |
+ t.Helper() | |
+ restConfig, err := config.GetConfigWithContext("base") | |
+ if err != nil { | |
+ t.Fatalf("failed to load *rest.Config: %v", err) | |
+ } | |
+ return rest.AddUserAgent(kcpclienthelper.SetCluster(restConfig, clusterName), t.Name()) | |
+} | |
+ | |
+func loadClient(t *testing.T, clusterName logicalcluster.Path) client.Client { | |
+ t.Helper() | |
+ scheme := runtime.NewScheme() | |
+ if err := clientgoscheme.AddToScheme(scheme); err != nil { | |
+ t.Fatalf("failed to add client go to scheme: %v", err) | |
+ } | |
+ if err := tenancyv1alpha1.AddToScheme(scheme); err != nil { | |
+ t.Fatalf("failed to add %s to scheme: %v", tenancyv1alpha1.SchemeGroupVersion, err) | |
+ } | |
+ if err := datav1alpha1.AddToScheme(scheme); err != nil { | |
+ t.Fatalf("failed to add %s to scheme: %v", datav1alpha1.GroupVersion, err) | |
+ } | |
+ if err := apisv1alpha1.AddToScheme(scheme); err != nil { | |
+ t.Fatalf("failed to add %s to scheme: %v", apisv1alpha1.SchemeGroupVersion, err) | |
+ } | |
+ tenancyClient, err := client.New(loadClusterConfig(t, clusterName), client.Options{Scheme: scheme}) | |
+ if err != nil { | |
+ t.Fatalf("failed to create a client: %v", err) | |
+ } | |
+ return tenancyClient | |
+} | |
+ | |
+func createWorkspace(t *testing.T, clusterName logicalcluster.Path) client.Client { | |
+ t.Helper() | |
+ parent, ok := clusterName.Parent() | |
+ if !ok { | |
+ t.Fatalf("cluster %s has no parent", clusterName) | |
+ } | |
+ c := loadClient(t, parent) | |
+ t.Logf("creating workspace %s", clusterName) | |
+ if err := c.Create(context.TODO(), &tenancyv1alpha1.Workspace{ | |
+ ObjectMeta: metav1.ObjectMeta{ | |
+ Name: clusterName.Base(), | |
+ }, | |
+ Spec: tenancyv1alpha1.WorkspaceSpec{ | |
+ Type: tenancyv1alpha1.WorkspaceTypeReference{ | |
+ Name: "widgets", | |
+ Path: "root", | |
+ }, | |
+ }, | |
+ }); err != nil { | |
+ t.Fatalf("failed to create workspace: %s: %v", clusterName, err) | |
+ } | |
+ | |
+ t.Logf("waiting for workspace %s to be ready", clusterName) | |
+ var workspace tenancyv1alpha1.Workspace | |
+ if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { | |
+ fetchErr := c.Get(context.TODO(), client.ObjectKey{Name: clusterName.Base()}, &workspace) | |
+ if fetchErr != nil { | |
+ t.Logf("failed to get workspace %s: %v", clusterName, err) | |
+ return false, fetchErr | |
+ } | |
+ var reason string | |
+ if actual, expected := workspace.Status.Phase, corev1alpha1.LogicalClusterPhaseReady; actual != expected { | |
+ reason = fmt.Sprintf("phase is %s, not %s", actual, expected) | |
+ t.Logf("not done waiting for workspace %s to be ready: %s", clusterName, reason) | |
+ } | |
+ return reason == "", nil | |
+ }); err != nil { | |
+ t.Fatalf("workspace %s never ready: %v", clusterName, err) | |
+ } | |
+ | |
+ return waitingForAPIBinding(t, clusterName) | |
+} | |
+ | |
+func waitingForAPIBinding(t *testing.T, workspaceCluster logicalcluster.Path) client.Client { | |
+ c := loadClient(t, workspaceCluster) | |
+ ctx := context.TODO() | |
+ apiNamePrefix := "data.my.domain" // matches bootstrapped name | |
+ | |
+ list := &apisv1alpha1.APIBindingList{} | |
+ err := c.List(ctx, list) | |
+ if err != nil { | |
+ t.Fatalf("failed to list APIBindings: %v", err) | |
+ } | |
+ | |
+ apiName := "" | |
+ for _, apiBinding := range list.Items { | |
+ if strings.HasPrefix(apiBinding.Name, apiNamePrefix) { | |
+ apiName = apiBinding.Name | |
+ break | |
+ } | |
+ } | |
+ | |
+ t.Logf("waiting for APIBinding %s|%s to be bound", workspaceCluster, apiName) | |
+ var apiBinding apisv1alpha1.APIBinding | |
+ if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { | |
+ fetchErr := c.Get(context.TODO(), client.ObjectKey{Name: apiName}, &apiBinding) | |
+ if fetchErr != nil { | |
+ t.Logf("failed to get APIBinding %s|%s: %v", workspaceCluster, apiName, err) | |
+ return false, fetchErr | |
+ } | |
+ var reason string | |
+ if !conditions.IsTrue(&apiBinding, apisv1alpha1.InitialBindingCompleted) { | |
+ condition := conditions.Get(&apiBinding, apisv1alpha1.InitialBindingCompleted) | |
+ if condition != nil { | |
+ reason = fmt.Sprintf("%s: %s", condition.Reason, condition.Message) | |
+ } else { | |
+ reason = "no condition present" | |
+ } | |
+ t.Logf("not done waiting for APIBinding %s|%s to be bound: %s", workspaceCluster, apiName, reason) | |
+ } | |
+ return conditions.IsTrue(&apiBinding, apisv1alpha1.InitialBindingCompleted), nil | |
+ }); err != nil { | |
+ t.Fatalf("APIBinding %s|%s never bound: %v", workspaceCluster, apiName, err) | |
+ } | |
+ | |
+ return c | |
+} | |
+ | |
+const characters = "abcdefghijklmnopqrstuvwxyz" | |
+ | |
+func randomName() string { | |
+ b := make([]byte, 10) | |
+ for i := range b { | |
+ b[i] = characters[rand.Intn(len(characters))] | |
+ } | |
+ return string(b) | |
+} | |
+ | |
+// TestConfigMapController verifies that our ConfigMap behavior works. | |
+func TestConfigMapController(t *testing.T) { | |
+ t.Parallel() | |
+ for i := 0; i < 3; i++ { | |
+ t.Run(fmt.Sprintf("attempt-%d", i), func(t *testing.T) { | |
+ t.Parallel() | |
+ workspaceCluster := parentWorkspace(t).Join(randomName()) | |
+ c := createWorkspace(t, workspaceCluster) | |
+ | |
+ namespaceName := randomName() | |
+ t.Logf("creating namespace %s|%s", workspaceCluster, namespaceName) | |
+ if err := c.Create(context.TODO(), &corev1.Namespace{ | |
+ ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, | |
+ }); err != nil { | |
+ t.Fatalf("failed to create a namespace: %v", err) | |
+ } | |
+ | |
+ otherNamespaceName := randomName() | |
+ data := randomName() | |
+ configmapName := randomName() | |
+ t.Logf("creating configmap %s|%s/%s", workspaceCluster, namespaceName, configmapName) | |
+ if err := c.Create(context.TODO(), &corev1.ConfigMap{ | |
+ ObjectMeta: metav1.ObjectMeta{ | |
+ Name: configmapName, | |
+ Namespace: namespaceName, | |
+ Labels: map[string]string{ | |
+ "name": "timothy", | |
+ }, | |
+ }, | |
+ Data: map[string]string{ | |
+ "namespace": otherNamespaceName, | |
+ "secretData": data, | |
+ }, | |
+ }); err != nil { | |
+ t.Fatalf("failed to create a configmap: %v", err) | |
+ } | |
+ | |
+ t.Logf("waiting for configmap %s|%s to have a response", workspaceCluster, configmapName) | |
+ var configmap corev1.ConfigMap | |
+ if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { | |
+ fetchErr := c.Get(context.TODO(), client.ObjectKey{Namespace: namespaceName, Name: configmapName}, &configmap) | |
+ if fetchErr != nil { | |
+ t.Logf("failed to get configmap %s|%s/%s: %v", workspaceCluster, namespaceName, configmapName, err) | |
+ return false, fetchErr | |
+ } | |
+ response, ok := configmap.Labels["response"] | |
+ if !ok { | |
+ t.Logf("configmap %s|%s/%s has no response set", workspaceCluster, namespaceName, configmapName) | |
+ } | |
+ diff := cmp.Diff(response, "hello-timothy") | |
+ if ok && diff != "" { | |
+ t.Logf("configmap %s|%s/%s has an invalid response: %v", workspaceCluster, namespaceName, configmapName, diff) | |
+ } | |
+ return diff == "", nil | |
+ }); err != nil { | |
+ t.Fatalf("configmap %s|%s/%s never got a response: %v", workspaceCluster, namespaceName, configmapName, err) | |
+ } | |
+ | |
+ t.Logf("waiting for namespace %s|%s to exist", workspaceCluster, otherNamespaceName) | |
+ var otherNamespace corev1.Namespace | |
+ if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { | |
+ fetchErr := c.Get(context.TODO(), client.ObjectKey{Name: otherNamespaceName}, &otherNamespace) | |
+ if fetchErr != nil && !apierrors.IsNotFound(fetchErr) { | |
+ t.Logf("failed to get namespace %s|%s: %v", workspaceCluster, otherNamespaceName, fetchErr) | |
+ return false, fetchErr | |
+ } | |
+ return fetchErr == nil, nil | |
+ }); err != nil { | |
+ t.Fatalf("namespace %s|%s never created: %v", workspaceCluster, otherNamespaceName, err) | |
+ } | |
+ | |
+ t.Logf("waiting for secret %s|%s/%s to exist and have correct data", workspaceCluster, namespaceName, configmapName) | |
+ var secret corev1.Secret | |
+ if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { | |
+ fetchErr := c.Get(context.TODO(), client.ObjectKey{Namespace: namespaceName, Name: configmapName}, &secret) | |
+ if fetchErr != nil && !apierrors.IsNotFound(fetchErr) { | |
+ t.Logf("failed to get secret %s|%s/%s: %v", workspaceCluster, namespaceName, configmapName, fetchErr) | |
+ return false, fetchErr | |
+ } | |
+ response, ok := secret.Data["dataFromCM"] | |
+ if !ok { | |
+ t.Logf("secret %s|%s/%s has no data set", workspaceCluster, namespaceName, configmapName) | |
+ } | |
+ diff := cmp.Diff(string(response), data) | |
+ if ok && diff != "" { | |
+ t.Logf("secret %s|%s/%s has invalid data: %v", workspaceCluster, namespaceName, configmapName, diff) | |
+ } | |
+ return diff == "", nil | |
+ }); err != nil { | |
+ t.Fatalf("secret %s|%s/%s never created: %v", workspaceCluster, namespaceName, configmapName, err) | |
+ } | |
+ }) | |
+ } | |
+} | |
+ | |
+// TestWidgetController verifies that our ConfigMap behavior works. | |
+func TestWidgetController(t *testing.T) { | |
+ t.Parallel() | |
+ for i := 0; i < 3; i++ { | |
+ t.Run(fmt.Sprintf("attempt-%d", i), func(t *testing.T) { | |
+ t.Parallel() | |
+ workspaceCluster := parentWorkspace(t).Join(randomName()) | |
+ c := createWorkspace(t, workspaceCluster) | |
+ | |
+ var totalWidgets int | |
+ for i := 0; i < 3; i++ { | |
+ namespaceName := randomName() | |
+ t.Logf("creating namespace %s|%s", workspaceCluster, namespaceName) | |
+ if err := c.Create(context.TODO(), &corev1.Namespace{ | |
+ ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, | |
+ }); err != nil { | |
+ t.Fatalf("failed to create a namespace: %v", err) | |
+ } | |
+ numWidgets := rand.Intn(10) | |
+ for i := 0; i < numWidgets; i++ { | |
+ if err := c.Create(context.TODO(), &datav1alpha1.Widget{ | |
+ ObjectMeta: metav1.ObjectMeta{Namespace: namespaceName, Name: fmt.Sprintf("widget-%d", i)}, | |
+ Spec: datav1alpha1.WidgetSpec{Foo: fmt.Sprintf("intended-%d", i)}, | |
+ }); err != nil { | |
+ t.Fatalf("failed to create widget: %v", err) | |
+ } | |
+ } | |
+ totalWidgets += numWidgets | |
+ } | |
+ | |
+ t.Logf("waiting for all widgets in cluster %s to have a correct status", workspaceCluster) | |
+ var allWidgets datav1alpha1.WidgetList | |
+ if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { | |
+ fetchErr := c.List(context.TODO(), &allWidgets) | |
+ if fetchErr != nil { | |
+ t.Logf("failed to get widgets in cluster %s: %v", workspaceCluster, err) | |
+ return false, fetchErr | |
+ } | |
+ var errs []error | |
+ for _, widget := range allWidgets.Items { | |
+ if actual, expected := widget.Status.Total, totalWidgets; actual != expected { | |
+ errs = append(errs, fmt.Errorf("widget %s|%s .status.total incorrect: %d != %d", workspaceCluster, widget.Name, actual, expected)) | |
+ } | |
+ } | |
+ validationErr := errors.NewAggregate(errs) | |
+ if validationErr != nil { | |
+ t.Logf("widgets in cluster %s invalid: %v", workspaceCluster, validationErr) | |
+ } | |
+ return validationErr == nil, nil | |
+ }); err != nil { | |
+ t.Fatalf("widgets in cluster %s never got correct statuses: %v", workspaceCluster, err) | |
+ } | |
+ }) | |
+ } | |
+} | |
diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod | |
deleted file mode 100644 | |
index dceb4d12..00000000 | |
--- a/examples/scratch-env/go.mod | |
+++ /dev/null | |
@@ -1,69 +0,0 @@ | |
-module sigs.k8s.io/controller-runtime/examples/scratch-env | |
- | |
-go 1.22.0 | |
- | |
-require ( | |
- github.com/spf13/pflag v1.0.5 | |
- go.uber.org/zap v1.26.0 | |
- sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000 | |
-) | |
- | |
-require ( | |
- github.com/beorn7/perks v1.0.1 // indirect | |
- github.com/cespare/xxhash/v2 v2.3.0 // indirect | |
- github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | |
- github.com/emicklei/go-restful/v3 v3.11.0 // indirect | |
- github.com/evanphx/json-patch/v5 v5.9.0 // indirect | |
- github.com/fsnotify/fsnotify v1.7.0 // indirect | |
- github.com/fxamacker/cbor/v2 v2.7.0 // indirect | |
- github.com/go-logr/logr v1.4.2 // indirect | |
- github.com/go-logr/zapr v1.3.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.4 // indirect | |
- github.com/gogo/protobuf v1.3.2 // indirect | |
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | |
- github.com/golang/protobuf v1.5.4 // indirect | |
- github.com/google/gnostic-models v0.6.8 // indirect | |
- github.com/google/go-cmp v0.6.0 // indirect | |
- github.com/google/gofuzz v1.2.0 // indirect | |
- github.com/google/uuid v1.6.0 // indirect | |
- github.com/imdario/mergo v0.3.6 // indirect | |
- github.com/josharian/intern v1.0.0 // indirect | |
- github.com/json-iterator/go v1.1.12 // indirect | |
- github.com/mailru/easyjson v0.7.7 // indirect | |
- github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | |
- github.com/modern-go/reflect2 v1.0.2 // indirect | |
- github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | |
- github.com/pkg/errors v0.9.1 // indirect | |
- github.com/prometheus/client_golang v1.19.1 // indirect | |
- github.com/prometheus/client_model v0.6.1 // indirect | |
- github.com/prometheus/common v0.55.0 // indirect | |
- github.com/prometheus/procfs v0.15.1 // indirect | |
- github.com/x448/float16 v0.8.4 // indirect | |
- go.uber.org/multierr v1.11.0 // indirect | |
- golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect | |
- golang.org/x/net v0.26.0 // indirect | |
- golang.org/x/oauth2 v0.21.0 // indirect | |
- golang.org/x/sys v0.21.0 // indirect | |
- golang.org/x/term v0.21.0 // indirect | |
- golang.org/x/text v0.16.0 // indirect | |
- golang.org/x/time v0.3.0 // indirect | |
- gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect | |
- google.golang.org/protobuf v1.34.2 // indirect | |
- gopkg.in/inf.v0 v0.9.1 // indirect | |
- gopkg.in/yaml.v2 v2.4.0 // indirect | |
- gopkg.in/yaml.v3 v3.0.1 // indirect | |
- k8s.io/api v0.31.0 // indirect | |
- k8s.io/apiextensions-apiserver v0.31.0 // indirect | |
- k8s.io/apimachinery v0.31.0 // indirect | |
- k8s.io/client-go v0.31.0 // indirect | |
- k8s.io/klog/v2 v2.130.1 // indirect | |
- k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect | |
- k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect | |
- sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect | |
- sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect | |
- sigs.k8s.io/yaml v1.4.0 // indirect | |
-) | |
- | |
-replace sigs.k8s.io/controller-runtime => ../.. | |
diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum | |
deleted file mode 100644 | |
index 89d30c15..00000000 | |
--- a/examples/scratch-env/go.sum | |
+++ /dev/null | |
@@ -1,192 +0,0 @@ | |
-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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | |
-github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | |
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | |
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
-github.com/davecgh/go-spew v1.1.1/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/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/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= | |
-github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= | |
-github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= | |
-github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= | |
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | |
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | |
-github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= | |
-github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= | |
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= | |
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | |
-github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= | |
-github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= | |
-github.com/go-openapi/jsonpointer v0.19.6 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/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= | |
-github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= | |
-github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= | |
-github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= | |
-github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= | |
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | |
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | |
-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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | |
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | |
-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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | |
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | |
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | |
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | |
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | |
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | |
-github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= | |
-github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= | |
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | |
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | |
-github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= | |
-github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= | |
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= | |
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | |
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | |
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | |
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | |
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | |
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | |
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | |
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | |
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | |
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | |
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | |
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | |
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= | |
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= | |
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | |
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | |
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | |
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | |
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | |
-github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= | |
-github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= | |
-github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= | |
-github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= | |
-github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= | |
-github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= | |
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | |
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | |
-github.com/pmezard/go-difflib v1.0.0/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/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= | |
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= | |
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | |
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | |
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= | |
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= | |
-github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | |
-github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | |
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= | |
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= | |
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | |
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | |
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | |
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | |
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | |
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | |
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | |
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | |
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | |
-github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | |
-github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | |
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | |
-go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | |
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= | |
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | |
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= | |
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= | |
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | |
-golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= | |
-golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= | |
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | |
-golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= | |
-golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= | |
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= | |
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | |
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= | |
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | |
-golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= | |
-golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= | |
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= | |
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= | |
-golang.org/x/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | |
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | |
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= | |
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | |
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
-gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= | |
-gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= | |
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | |
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | |
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | |
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | |
-gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= | |
-gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= | |
-gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= | |
-gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | |
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | |
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | |
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
-k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= | |
-k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= | |
-k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= | |
-k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= | |
-k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= | |
-k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= | |
-k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= | |
-k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= | |
-k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= | |
-k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= | |
-k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= | |
-k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= | |
-k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= | |
-k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= | |
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= | |
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= | |
-sigs.k8s.io/structured-merge-diff/v4 v4.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/examples/scratch-env/main.go b/examples/scratch-env/main.go | |
deleted file mode 100644 | |
index b8305ffe..00000000 | |
--- a/examples/scratch-env/main.go | |
+++ /dev/null | |
@@ -1,132 +0,0 @@ | |
-/* | |
-Copyright 2021 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. | |
-You may obtain a copy of the License at | |
- | |
- http://www.apache.org/licenses/LICENSE-2.0 | |
- | |
-Unless required by applicable law or agreed to in writing, software | |
-distributed under the License is distributed on an "AS IS" BASIS, | |
-WITHOUT 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 ( | |
- goflag "flag" | |
- "os" | |
- | |
- flag "github.com/spf13/pflag" | |
- "go.uber.org/zap" | |
- | |
- ctrl "sigs.k8s.io/controller-runtime" | |
- "sigs.k8s.io/controller-runtime/pkg/envtest" | |
- logzap "sigs.k8s.io/controller-runtime/pkg/log/zap" | |
-) | |
- | |
-var ( | |
- crdPaths = flag.StringSlice("crd-paths", nil, "paths to files or directories containing CRDs to install on start") | |
- webhookPaths = flag.StringSlice("webhook-paths", nil, "paths to files or directories containing webhook configurations to install on start") | |
- attachControlPlaneOut = flag.Bool("debug-env", false, "attach to test env (apiserver & etcd) output -- just a convinience flag to force KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT=true") | |
-) | |
- | |
-// have a separate function so we can return an exit code w/o skipping defers | |
-func runMain() int { | |
- loggerOpts := &logzap.Options{ | |
- Development: true, // a sane default | |
- ZapOpts: []zap.Option{zap.AddCaller()}, | |
- } | |
- { | |
- var goFlagSet goflag.FlagSet | |
- loggerOpts.BindFlags(&goFlagSet) | |
- flag.CommandLine.AddGoFlagSet(&goFlagSet) | |
- } | |
- flag.Parse() | |
- ctrl.SetLogger(logzap.New(logzap.UseFlagOptions(loggerOpts))) | |
- ctrl.Log.Info("Starting...") | |
- | |
- log := ctrl.Log.WithName("main") | |
- | |
- env := &envtest.Environment{} | |
- env.CRDInstallOptions.Paths = *crdPaths | |
- env.WebhookInstallOptions.Paths = *webhookPaths | |
- | |
- if *attachControlPlaneOut { | |
- os.Setenv("KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT", "true") | |
- } | |
- | |
- log.Info("Starting apiserver & etcd") | |
- cfg, err := env.Start() | |
- if err != nil { | |
- log.Error(err, "unable to start the test environment") | |
- // shut down the environment in case we started it and failed while | |
- // installing CRDs or provisioning users. | |
- if err := env.Stop(); err != nil { | |
- log.Error(err, "unable to stop the test environment after an error (this might be expected, but just though you should know)") | |
- } | |
- return 1 | |
- } | |
- | |
- log.Info("apiserver running", "host", cfg.Host) | |
- | |
- // NB(directxman12): this group is unfortunately named, but various | |
- // kubernetes versions require us to use it to get "admin" access. | |
- user, err := env.ControlPlane.AddUser(envtest.User{ | |
- Name: "envtest-admin", | |
- Groups: []string{"system:masters"}, | |
- }, nil) | |
- if err != nil { | |
- log.Error(err, "unable to provision admin user, continuing on without it") | |
- return 1 | |
- } | |
- | |
- // TODO(directxman12): add support for writing to a new context in an existing file | |
- kubeconfigFile, err := os.CreateTemp("", "scratch-env-kubeconfig-") | |
- if err != nil { | |
- log.Error(err, "unable to create kubeconfig file, continuing on without it") | |
- return 1 | |
- } | |
- defer os.Remove(kubeconfigFile.Name()) | |
- | |
- { | |
- log := log.WithValues("path", kubeconfigFile.Name()) | |
- log.V(1).Info("Writing kubeconfig") | |
- | |
- kubeConfig, err := user.KubeConfig() | |
- if err != nil { | |
- log.Error(err, "unable to create kubeconfig") | |
- } | |
- | |
- if _, err := kubeconfigFile.Write(kubeConfig); err != nil { | |
- log.Error(err, "unable to save kubeconfig") | |
- return 1 | |
- } | |
- | |
- log.Info("Wrote kubeconfig") | |
- } | |
- | |
- if opts := env.WebhookInstallOptions; opts.LocalServingPort != 0 { | |
- log.Info("webhooks configured for", "host", opts.LocalServingHost, "port", opts.LocalServingPort, "dir", opts.LocalServingCertDir) | |
- } | |
- | |
- ctx := ctrl.SetupSignalHandler() | |
- <-ctx.Done() | |
- | |
- log.Info("Shutting down apiserver & etcd") | |
- err = env.Stop() | |
- if err != nil { | |
- log.Error(err, "unable to stop the test environment") | |
- return 1 | |
- } | |
- | |
- log.Info("Shutdown successful") | |
- return 0 | |
-} | |
- | |
-func main() { | |
- os.Exit(runMain()) | |
-} | |
diff --git a/go.mod b/go.mod | |
index 3fd1aa95..3aaf5814 100644 | |
--- a/go.mod | |
+++ b/go.mod | |
@@ -30,6 +30,12 @@ require ( | |
sigs.k8s.io/yaml v1.4.0 | |
) | |
+require ( | |
+ github.com/hashicorp/golang-lru/v2 v2.0.7 | |
+ github.com/kcp-dev/apimachinery/v2 v2.0.0-alpha.0.0.20230926071920-57d168bcbe34 | |
+ github.com/kcp-dev/logicalcluster/v3 v3.0.5 | |
+) | |
+ | |
require ( | |
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect | |
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect | |
diff --git a/go.sum b/go.sum | |
index cb957a9e..f5e45379 100644 | |
--- a/go.sum | |
+++ b/go.sum | |
@@ -66,6 +66,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | |
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | |
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= | |
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= | |
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= | |
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= | |
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= | |
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= | |
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | |
@@ -74,6 +76,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm | |
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | |
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | |
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | |
+github.com/kcp-dev/apimachinery/v2 v2.0.0-alpha.0.0.20230926071920-57d168bcbe34 h1:tom0JX5OmAeOOmkGv8LaYHDtA1xAKDiQL5U0vhYYgdM= | |
+github.com/kcp-dev/apimachinery/v2 v2.0.0-alpha.0.0.20230926071920-57d168bcbe34/go.mod h1:cWoaYGHl1nlzdEM2xvMzIASkEZJZLSf5nhe17M7wDhw= | |
+github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU= | |
+github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY= | |
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | |
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | |
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | |
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go | |
index 706f9c6c..e89cddac 100644 | |
--- a/pkg/cache/cache.go | |
+++ b/pkg/cache/cache.go | |
@@ -21,6 +21,7 @@ import ( | |
"fmt" | |
"net/http" | |
"sort" | |
+ "strings" | |
"time" | |
"golang.org/x/exp/maps" | |
@@ -169,6 +170,14 @@ type Options struct { | |
// instead of `reconcile.Result{}`. | |
SyncPeriod *time.Duration | |
+ // NewInformerFunc is a function that is used to create SharedIndexInformers. | |
+ // Defaults to cache.NewSharedIndexInformer from client-go | |
+ NewInformerFunc client.NewInformerFunc | |
+ | |
+ // Indexers is the indexers that the informers will be configured to use. | |
+ // Will always have the standard NamespaceIndex. | |
+ Indexers toolscache.Indexers | |
+ | |
// ReaderFailOnMissingInformer configures the cache to return a ErrResourceNotCached error when a user | |
// requests, using Get() and List(), a resource the cache does not already have an informer for. | |
// | |
@@ -225,9 +234,6 @@ type Options struct { | |
// ByObject restricts the cache's ListWatch to the desired fields per GVK at the specified object. | |
// If unset, this will fall through to the Default* settings. | |
ByObject map[client.Object]ByObject | |
- | |
- // newInformer allows overriding of NewSharedIndexInformer for testing. | |
- newInformer *func(toolscache.ListerWatcher, runtime.Object, time.Duration, toolscache.Indexers) toolscache.SharedIndexInformer | |
} | |
// ByObject offers more fine-grained control over the cache's ListWatch by object. | |
@@ -398,9 +404,10 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc { | |
Transform: config.Transform, | |
WatchErrorHandler: opts.DefaultWatchErrorHandler, | |
UnsafeDisableDeepCopy: ptr.Deref(config.UnsafeDisableDeepCopy, false), | |
- NewInformer: opts.newInformer, | |
+ NewInformer: opts.NewInformerFunc, | |
}), | |
readerFailOnMissingInformer: opts.ReaderFailOnMissingInformer, | |
+ clusterIndexes: strings.HasSuffix(restConfig.Host, "/clusters/*"), | |
} | |
} | |
} | |
@@ -507,6 +514,10 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { | |
if opts.SyncPeriod == nil { | |
opts.SyncPeriod = &defaultSyncPeriod | |
} | |
+ | |
+ if opts.NewInformerFunc == nil { | |
+ opts.NewInformerFunc = toolscache.NewSharedIndexInformer | |
+ } | |
return opts, nil | |
} | |
diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go | |
index 7a21c87c..1b37a2d9 100644 | |
--- a/pkg/cache/cache_test.go | |
+++ b/pkg/cache/cache_test.go | |
@@ -544,14 +544,9 @@ func NonBlockingGetTest(createCacheFunc func(config *rest.Config, opts cache.Opt | |
Expect(err).NotTo(HaveOccurred()) | |
By("creating the informer cache") | |
- v := reflect.ValueOf(&opts).Elem() | |
- newInformerField := v.FieldByName("newInformer") | |
- newFakeInformer := func(_ kcache.ListerWatcher, _ runtime.Object, _ time.Duration, _ kcache.Indexers) kcache.SharedIndexInformer { | |
+ opts.NewInformerFunc = func(_ kcache.ListerWatcher, _ runtime.Object, _ time.Duration, _ kcache.Indexers) kcache.SharedIndexInformer { | |
return &controllertest.FakeInformer{Synced: false} | |
} | |
- reflect.NewAt(newInformerField.Type(), newInformerField.Addr().UnsafePointer()). | |
- Elem(). | |
- Set(reflect.ValueOf(&newFakeInformer)) | |
informerCache, err = createCacheFunc(cfg, opts) | |
Expect(err).NotTo(HaveOccurred()) | |
By("running the cache and waiting for it to sync") | |
diff --git a/pkg/cache/defaulting_test.go b/pkg/cache/defaulting_test.go | |
index 3c01bf84..930a1535 100644 | |
--- a/pkg/cache/defaulting_test.go | |
+++ b/pkg/cache/defaulting_test.go | |
@@ -401,6 +401,9 @@ func TestDefaultOpts(t *testing.T) { | |
t.Fatal(err) | |
} | |
+ // We cannot reference kcp.NewInformerWithClusterIndexes due to import cycle. | |
+ defaulted.NewInformerFunc = nil | |
+ | |
if diff := tc.verification(defaulted); diff != "" { | |
t.Errorf("expected config differs from actual: %s", diff) | |
} | |
diff --git a/pkg/cache/informer_cache.go b/pkg/cache/informer_cache.go | |
index 091667b7..71b94da1 100644 | |
--- a/pkg/cache/informer_cache.go | |
+++ b/pkg/cache/informer_cache.go | |
@@ -31,6 +31,8 @@ import ( | |
"sigs.k8s.io/controller-runtime/pkg/cache/internal" | |
"sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/client/apiutil" | |
+ | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
) | |
var ( | |
@@ -68,6 +70,7 @@ type informerCache struct { | |
scheme *runtime.Scheme | |
*internal.Informers | |
readerFailOnMissingInformer bool | |
+ clusterIndexes bool | |
} | |
// Get implements Reader. | |
@@ -217,10 +220,10 @@ func (ic *informerCache) IndexField(ctx context.Context, obj client.Object, fiel | |
if err != nil { | |
return err | |
} | |
- return indexByField(informer, field, extractValue) | |
+ return indexByField(informer, field, extractValue, ic.clusterIndexes) | |
} | |
-func indexByField(informer Informer, field string, extractValue client.IndexerFunc) error { | |
+func indexByField(informer Informer, field string, extractValue client.IndexerFunc, clusterIndexes bool) error { | |
indexFunc := func(objRaw interface{}) ([]string, error) { | |
// TODO(directxman12): check if this is the correct type? | |
obj, isObj := objRaw.(client.Object) | |
@@ -233,6 +236,13 @@ func indexByField(informer Informer, field string, extractValue client.IndexerFu | |
} | |
ns := meta.GetNamespace() | |
+ keyFunc := internal.KeyToNamespacedKey | |
+ if clusterName := logicalcluster.From(obj); clusterIndexes && !clusterName.Empty() { | |
+ keyFunc = func(ns, val string) string { | |
+ return internal.KeyToClusteredKey(clusterName.String(), ns, val) | |
+ } | |
+ } | |
+ | |
rawVals := extractValue(obj) | |
var vals []string | |
if ns == "" { | |
@@ -242,14 +252,15 @@ func indexByField(informer Informer, field string, extractValue client.IndexerFu | |
// if we need to add non-namespaced versions too, double the length | |
vals = make([]string, len(rawVals)*2) | |
} | |
+ | |
for i, rawVal := range rawVals { | |
// save a namespaced variant, so that we can ask | |
// "what are all the object matching a given index *in a given namespace*" | |
- vals[i] = internal.KeyToNamespacedKey(ns, rawVal) | |
+ vals[i] = keyFunc(ns, rawVal) | |
if ns != "" { | |
// if we have a namespace, also inject a special index key for listing | |
// regardless of the object namespace | |
- vals[i+len(rawVals)] = internal.KeyToNamespacedKey("", rawVal) | |
+ vals[i+len(rawVals)] = keyFunc("", rawVal) | |
} | |
} | |
diff --git a/pkg/cache/internal/cache_reader.go b/pkg/cache/internal/cache_reader.go | |
index 81ee960b..aaf0966f 100644 | |
--- a/pkg/cache/internal/cache_reader.go | |
+++ b/pkg/cache/internal/cache_reader.go | |
@@ -21,6 +21,9 @@ import ( | |
"fmt" | |
"reflect" | |
+ kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ | |
apierrors "k8s.io/apimachinery/pkg/api/errors" | |
apimeta "k8s.io/apimachinery/pkg/api/meta" | |
"k8s.io/apimachinery/pkg/fields" | |
@@ -31,6 +34,7 @@ import ( | |
"sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/internal/field/selector" | |
+ "sigs.k8s.io/controller-runtime/pkg/kontext" | |
) | |
// CacheReader is a client.Reader. | |
@@ -54,12 +58,22 @@ type CacheReader struct { | |
} | |
// Get checks the indexer for the object and writes a copy of it if found. | |
-func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object, _ ...client.GetOption) error { | |
+func (c *CacheReader) Get(ctx context.Context, key client.ObjectKey, out client.Object, _ ...client.GetOption) error { | |
if c.scopeName == apimeta.RESTScopeNameRoot { | |
key.Namespace = "" | |
} | |
storeKey := objectKeyToStoreKey(key) | |
+ // create cluster-aware key for KCP | |
+ _, isClusterAware := c.indexer.GetIndexers()[kcpcache.ClusterAndNamespaceIndexName] | |
+ clusterName, _ := kontext.ClusterFrom(ctx) | |
+ if isClusterAware && clusterName.Empty() { | |
+ return fmt.Errorf("cluster-aware cache requires a cluster in context") | |
+ } | |
+ if isClusterAware { | |
+ storeKey = clusterName.String() + "|" + storeKey | |
+ } | |
+ | |
// Lookup the object from the indexer cache | |
obj, exists, err := c.indexer.GetByKey(storeKey) | |
if err != nil { | |
@@ -105,7 +119,7 @@ func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Ob | |
} | |
// List lists items out of the indexer and writes them to out. | |
-func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...client.ListOption) error { | |
+func (c *CacheReader) List(ctx context.Context, out client.ObjectList, opts ...client.ListOption) error { | |
var objs []interface{} | |
var err error | |
@@ -116,6 +130,9 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli | |
return fmt.Errorf("continue list option is not supported by the cache") | |
} | |
+ _, isClusterAware := c.indexer.GetIndexers()[kcpcache.ClusterAndNamespaceIndexName] | |
+ clusterName, _ := kontext.ClusterFrom(ctx) | |
+ | |
switch { | |
case listOpts.FieldSelector != nil: | |
requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector) | |
@@ -125,11 +142,19 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli | |
// list all objects by the field selector. If this is namespaced and we have one, ask for the | |
// namespaced index key. Otherwise, ask for the non-namespaced variant by using the fake "all namespaces" | |
// namespace. | |
- objs, err = byIndexes(c.indexer, listOpts.FieldSelector.Requirements(), listOpts.Namespace) | |
+ objs, err = byIndexes(c.indexer, listOpts.FieldSelector.Requirements(), clusterName, listOpts.Namespace) | |
case listOpts.Namespace != "": | |
- objs, err = c.indexer.ByIndex(cache.NamespaceIndex, listOpts.Namespace) | |
+ if isClusterAware && !clusterName.Empty() { | |
+ objs, err = c.indexer.ByIndex(kcpcache.ClusterAndNamespaceIndexName, kcpcache.ClusterAndNamespaceIndexKey(clusterName, listOpts.Namespace)) | |
+ } else { | |
+ objs, err = c.indexer.ByIndex(cache.NamespaceIndex, listOpts.Namespace) | |
+ } | |
default: | |
- objs = c.indexer.List() | |
+ if isClusterAware && !clusterName.Empty() { | |
+ objs, err = c.indexer.ByIndex(kcpcache.ClusterIndexName, kcpcache.ClusterIndexKey(clusterName)) | |
+ } else { | |
+ objs = c.indexer.List() | |
+ } | |
} | |
if err != nil { | |
return err | |
@@ -177,16 +202,22 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli | |
return apimeta.SetList(out, runtimeObjs) | |
} | |
-func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace string) ([]interface{}, error) { | |
+func byIndexes(indexer cache.Indexer, requires fields.Requirements, clusterName logicalcluster.Name, namespace string) ([]interface{}, error) { | |
var ( | |
err error | |
objs []interface{} | |
vals []string | |
) | |
indexers := indexer.GetIndexers() | |
+ _, isClusterAware := indexers[kcpcache.ClusterAndNamespaceIndexName] | |
for idx, req := range requires { | |
indexName := FieldIndexName(req.Field) | |
- indexedValue := KeyToNamespacedKey(namespace, req.Value) | |
+ var indexedValue string | |
+ if isClusterAware { | |
+ indexedValue = KeyToClusteredKey(clusterName.String(), namespace, req.Value) | |
+ } else { | |
+ indexedValue = KeyToNamespacedKey(namespace, req.Value) | |
+ } | |
if idx == 0 { | |
// we use first require to get snapshot data | |
// TODO(halfcrazy): use complicated index when client-go provides byIndexes | |
@@ -253,3 +284,9 @@ func KeyToNamespacedKey(ns string, baseKey string) string { | |
} | |
return allNamespacesNamespace + "/" + baseKey | |
} | |
+ | |
+// KeyToClusteredKey prefixes the given index key with a cluster name | |
+// for use in field selector indexes. | |
+func KeyToClusteredKey(clusterName string, ns string, baseKey string) string { | |
+ return clusterName + "|" + KeyToNamespacedKey(ns, baseKey) | |
+} | |
diff --git a/pkg/cache/internal/informers.go b/pkg/cache/internal/informers.go | |
index cd8c6774..ae10b2f6 100644 | |
--- a/pkg/cache/internal/informers.go | |
+++ b/pkg/cache/internal/informers.go | |
@@ -47,7 +47,7 @@ type InformersOpts struct { | |
Mapper meta.RESTMapper | |
ResyncPeriod time.Duration | |
Namespace string | |
- NewInformer *func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer | |
+ NewInformer func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer | |
Selector Selector | |
Transform cache.TransformFunc | |
UnsafeDisableDeepCopy bool | |
@@ -58,7 +58,7 @@ type InformersOpts struct { | |
func NewInformers(config *rest.Config, options *InformersOpts) *Informers { | |
newInformer := cache.NewSharedIndexInformer | |
if options.NewInformer != nil { | |
- newInformer = *options.NewInformer | |
+ newInformer = options.NewInformer | |
} | |
return &Informers{ | |
config: config, | |
diff --git a/pkg/cache/kcp_test.go b/pkg/cache/kcp_test.go | |
new file mode 100644 | |
index 00000000..11c48457 | |
--- /dev/null | |
+++ b/pkg/cache/kcp_test.go | |
@@ -0,0 +1,138 @@ | |
+/* | |
+Copyright 2024 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. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT 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 cache_test | |
+ | |
+import ( | |
+ "context" | |
+ | |
+ . "github.com/onsi/ginkgo/v2" | |
+ . "github.com/onsi/gomega" | |
+ corev1 "k8s.io/api/core/v1" | |
+ "k8s.io/apimachinery/pkg/fields" | |
+ "sigs.k8s.io/controller-runtime/pkg/cache" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+) | |
+ | |
+var _ = Describe("informer cache against a kube cluster", func() { | |
+ BeforeEach(func() { | |
+ By("Annotating the default namespace with kcp.io/cluster") | |
+ cl, err := client.New(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ ns := &corev1.Namespace{} | |
+ err = cl.Get(context.Background(), client.ObjectKey{Name: "default"}, ns) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ ns.Annotations = map[string]string{"kcp.io/cluster": "cluster1"} | |
+ err = cl.Update(context.Background(), ns) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ }) | |
+ | |
+ Describe("KCP cluster-unaware informer cache", func() { | |
+ // Test whether we can have a cluster-unaware informer cache against a single workspace. | |
+ // I.e. every object has a kcp.io/cluster annotation, but it should not be taken | |
+ // into consideration by the cache to compute the key. | |
+ It("should be able to get the default namespace despite kcp.io/cluster annotation", func() { | |
+ ctx, cancel := context.WithCancel(context.Background()) | |
+ defer cancel() | |
+ | |
+ c, err := cache.New(cfg, cache.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ go c.Start(ctx) //nolint:errcheck // Start is blocking, and error not relevant here. | |
+ c.WaitForCacheSync(ctx) | |
+ | |
+ By("By getting the default namespace with the informer") | |
+ ns := &corev1.Namespace{} | |
+ err = c.Get(ctx, client.ObjectKey{Name: "default"}, ns) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ }) | |
+ | |
+ It("should support indexes with cluster-less keys", func() { | |
+ ctx, cancel := context.WithCancel(context.Background()) | |
+ defer cancel() | |
+ | |
+ c, err := cache.New(cfg, cache.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ By("Indexing the default namespace by name") | |
+ err = c.IndexField(ctx, &corev1.Namespace{}, "name-clusterless", func(obj client.Object) []string { | |
+ return []string{"key-" + obj.GetName()} | |
+ }) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ go c.Start(ctx) //nolint:errcheck // Start is blocking, and error not relevant here. | |
+ c.WaitForCacheSync(ctx) | |
+ | |
+ By("By getting the default namespace via the custom index") | |
+ nss := &corev1.NamespaceList{} | |
+ err = c.List(ctx, nss, client.MatchingFieldsSelector{ | |
+ Selector: fields.OneTermEqualSelector("name-clusterless", "key-default"), | |
+ }) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ Expect(nss.Items).To(HaveLen(1)) | |
+ }) | |
+ }) | |
+ | |
+ // TODO: get envtest in place with kcp | |
+ /* | |
+ Describe("KCP cluster-aware informer cache", func() { | |
+ It("should be able to get the default namespace with kcp.io/cluster annotation", func() { | |
+ ctx, cancel := context.WithCancel(context.Background()) | |
+ defer cancel() | |
+ | |
+ c, err := kcp.NewClusterAwareCache(cfg, cache.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ go c.Start(ctx) //nolint:errcheck // Start is blocking, and error not relevant here. | |
+ c.WaitForCacheSync(ctx) | |
+ | |
+ By("By getting the default namespace with the informer, but cluster-less key should fail") | |
+ ns := &corev1.Namespace{} | |
+ err = c.Get(ctx, client.ObjectKey{Name: "default"}, ns) | |
+ Expect(err).To(HaveOccurred()) | |
+ | |
+ By("By getting the default namespace with the informer, but cluster-aware key should succeed") | |
+ err = c.Get(kontext.WithCluster(ctx, "cluster1"), client.ObjectKey{Name: "default", Namespace: "cluster1"}, ns) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ }) | |
+ | |
+ It("should support indexes with cluster-aware keys", func() { | |
+ ctx, cancel := context.WithCancel(context.Background()) | |
+ defer cancel() | |
+ | |
+ c, err := kcp.NewClusterAwareCache(cfg, cache.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ By("Indexing the default namespace by name") | |
+ err = c.IndexField(ctx, &corev1.Namespace{}, "name-clusteraware", func(obj client.Object) []string { | |
+ return []string{"key-" + obj.GetName()} | |
+ }) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ go c.Start(ctx) //nolint:errcheck // Start is blocking, and error not relevant here. | |
+ c.WaitForCacheSync(ctx) | |
+ | |
+ By("By getting the default namespace via the custom index") | |
+ nss := &corev1.NamespaceList{} | |
+ err = c.List(ctx, nss, client.MatchingFieldsSelector{ | |
+ Selector: fields.OneTermEqualSelector("name-clusteraware", "key-default"), | |
+ }) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ Expect(nss.Items).To(HaveLen(1)) | |
+ }) | |
+ }) | |
+ */ | |
+}) | |
diff --git a/pkg/client/client.go b/pkg/client/client.go | |
index fe9862b8..ab1e7bd3 100644 | |
--- a/pkg/client/client.go | |
+++ b/pkg/client/client.go | |
@@ -23,6 +23,8 @@ import ( | |
"net/http" | |
"strings" | |
+ lru "github.com/hashicorp/golang-lru/v2" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
"k8s.io/apimachinery/pkg/api/meta" | |
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
"k8s.io/apimachinery/pkg/runtime" | |
@@ -47,11 +49,19 @@ type Options struct { | |
// Mapper, if provided, will be used to map GroupVersionKinds to Resources | |
Mapper meta.RESTMapper | |
+ // MapperWithContext, if provided, will be used to map GroupVersionKinds to Resources. | |
+ // This overrides Mapper if set. | |
+ MapperWithContext func(context.Context) (meta.RESTMapper, error) | |
+ | |
// Cache, if provided, is used to read objects from the cache. | |
Cache *CacheOptions | |
// DryRun instructs the client to only perform dry run requests. | |
DryRun *bool | |
+ | |
+ // KcpClusterDiscoveryCacheSize is the size of the cache for cluster discovery | |
+ // information backing the client's REST mapper. | |
+ KcpClusterDiscoveryCacheSize int | |
} | |
// CacheOptions are options for creating a cache-backed client. | |
@@ -71,6 +81,14 @@ type CacheOptions struct { | |
// NewClientFunc allows a user to define how to create a client. | |
type NewClientFunc func(config *rest.Config, options Options) (Client, error) | |
+// NewAPIReaderFunc allows a user to define how to create an API server reader. | |
+type NewAPIReaderFunc func(config *rest.Config, options Options) (Reader, error) | |
+ | |
+// NewAPIReader creates a new API server reader. | |
+func NewAPIReader(config *rest.Config, options Options) (Reader, error) { | |
+ return New(config, options) | |
+} | |
+ | |
// New returns a new Client using the provided config and Options. | |
// | |
// By default, the client surfaces warnings returned by the server. To | |
@@ -145,16 +163,27 @@ func newClient(config *rest.Config, options Options) (*client, error) { | |
} | |
} | |
+ if options.KcpClusterDiscoveryCacheSize == 0 { | |
+ options.KcpClusterDiscoveryCacheSize = 1000 | |
+ } | |
+ | |
+ // Init a MapperWithContext if none provided | |
+ if options.MapperWithContext == nil { | |
+ options.MapperWithContext = func(context.Context) (meta.RESTMapper, error) { return options.Mapper, nil } | |
+ } | |
+ | |
resources := &clientRestResources{ | |
httpClient: options.HTTPClient, | |
config: config, | |
scheme: options.Scheme, | |
- mapper: options.Mapper, | |
+ mapper: options.MapperWithContext, | |
codecs: serializer.NewCodecFactory(options.Scheme), | |
- | |
- structuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), | |
- unstructuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), | |
} | |
+ cr, err := lru.New[logicalcluster.Path, clusterResources](options.KcpClusterDiscoveryCacheSize) | |
+ if err != nil { | |
+ return nil, err | |
+ } | |
+ resources.clusterResources = cr | |
rawMetaClient, err := metadata.NewForConfigAndClient(metadata.ConfigFor(config), options.HTTPClient) | |
if err != nil { | |
@@ -172,11 +201,16 @@ func newClient(config *rest.Config, options Options) (*client, error) { | |
}, | |
metadataClient: metadataClient{ | |
client: rawMetaClient, | |
- restMapper: options.Mapper, | |
+ restMapper: options.MapperWithContext, | |
}, | |
scheme: options.Scheme, | |
mapper: options.Mapper, | |
} | |
+ mapperCache, err := lru.New[logicalcluster.Name, meta.RESTMapper](options.KcpClusterDiscoveryCacheSize) | |
+ if err != nil { | |
+ return nil, err | |
+ } | |
+ c.metadataClient.mapperCache = mapperCache | |
if options.Cache == nil || options.Cache.Reader == nil { | |
return c, nil | |
} | |
diff --git a/pkg/client/client_rest_resources.go b/pkg/client/client_rest_resources.go | |
index 2d078795..1247879e 100644 | |
--- a/pkg/client/client_rest_resources.go | |
+++ b/pkg/client/client_rest_resources.go | |
@@ -17,10 +17,13 @@ limitations under the License. | |
package client | |
import ( | |
+ "context" | |
"net/http" | |
"strings" | |
"sync" | |
+ lru "github.com/hashicorp/golang-lru/v2" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
"k8s.io/apimachinery/pkg/api/meta" | |
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
"k8s.io/apimachinery/pkg/runtime" | |
@@ -28,8 +31,18 @@ import ( | |
"k8s.io/apimachinery/pkg/runtime/serializer" | |
"k8s.io/client-go/rest" | |
"sigs.k8s.io/controller-runtime/pkg/client/apiutil" | |
+ "sigs.k8s.io/controller-runtime/pkg/kontext" | |
) | |
+type clusterResources struct { | |
+ mapper meta.RESTMapper | |
+ | |
+ // structuredResourceByType stores structured type metadata | |
+ structuredResourceByType map[schema.GroupVersionKind]*resourceMeta | |
+ // unstructuredResourceByType stores unstructured type metadata | |
+ unstructuredResourceByType map[schema.GroupVersionKind]*resourceMeta | |
+} | |
+ | |
// clientRestResources creates and stores rest clients and metadata for Kubernetes types. | |
type clientRestResources struct { | |
// httpClient is the http client to use for requests | |
@@ -42,21 +55,18 @@ type clientRestResources struct { | |
scheme *runtime.Scheme | |
// mapper maps GroupVersionKinds to Resources | |
- mapper meta.RESTMapper | |
+ mapper func(ctx context.Context) (meta.RESTMapper, error) | |
// codecs are used to create a REST client for a gvk | |
codecs serializer.CodecFactory | |
- // structuredResourceByType stores structured type metadata | |
- structuredResourceByType map[schema.GroupVersionKind]*resourceMeta | |
- // unstructuredResourceByType stores unstructured type metadata | |
- unstructuredResourceByType map[schema.GroupVersionKind]*resourceMeta | |
- mu sync.RWMutex | |
+ clusterResources *lru.Cache[logicalcluster.Path, clusterResources] | |
+ mu sync.RWMutex | |
} | |
// newResource maps obj to a Kubernetes Resource and constructs a client for that Resource. | |
// If the object is a list, the resource represents the item's type instead. | |
-func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, isList, isUnstructured bool) (*resourceMeta, error) { | |
+func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, isList, isUnstructured bool, mapper meta.RESTMapper) (*resourceMeta, error) { | |
if strings.HasSuffix(gvk.Kind, "List") && isList { | |
// if this was a list, treat it as a request for the item's resource | |
gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] | |
@@ -66,7 +76,7 @@ func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, isList, i | |
if err != nil { | |
return nil, err | |
} | |
- mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) | |
+ mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) | |
if err != nil { | |
return nil, err | |
} | |
@@ -75,7 +85,7 @@ func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, isList, i | |
// getResource returns the resource meta information for the given type of object. | |
// If the object is a list, the resource represents the item's type instead. | |
-func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, error) { | |
+func (c *clientRestResources) getResource(ctx context.Context, obj runtime.Object) (*resourceMeta, error) { | |
gvk, err := apiutil.GVKForObject(obj, c.scheme) | |
if err != nil { | |
return nil, err | |
@@ -86,9 +96,25 @@ func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, er | |
// It's better to do creation work twice than to not let multiple | |
// people make requests at once | |
c.mu.RLock() | |
- resourceByType := c.structuredResourceByType | |
+ cluster, _ := kontext.ClusterFrom(ctx) | |
+ cr, found := c.clusterResources.Get(cluster.Path()) | |
+ if !found { | |
+ m, err := c.mapper(ctx) | |
+ if err != nil { | |
+ c.mu.RUnlock() | |
+ return nil, err | |
+ } | |
+ cr = clusterResources{ | |
+ mapper: m, | |
+ structuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), | |
+ unstructuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), | |
+ } | |
+ c.clusterResources.Purge() | |
+ c.clusterResources.Add(cluster.Path(), cr) | |
+ } | |
+ resourceByType := cr.structuredResourceByType | |
if isUnstructured { | |
- resourceByType = c.unstructuredResourceByType | |
+ resourceByType = cr.unstructuredResourceByType | |
} | |
r, known := resourceByType[gvk] | |
c.mu.RUnlock() | |
@@ -100,7 +126,7 @@ func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, er | |
// Initialize a new Client | |
c.mu.Lock() | |
defer c.mu.Unlock() | |
- r, err = c.newResource(gvk, meta.IsListType(obj), isUnstructured) | |
+ r, err = c.newResource(gvk, meta.IsListType(obj), isUnstructured, cr.mapper) | |
if err != nil { | |
return nil, err | |
} | |
@@ -109,8 +135,8 @@ func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, er | |
} | |
// getObjMeta returns objMeta containing both type and object metadata and state. | |
-func (c *clientRestResources) getObjMeta(obj runtime.Object) (*objMeta, error) { | |
- r, err := c.getResource(obj) | |
+func (c *clientRestResources) getObjMeta(ctx context.Context, obj runtime.Object) (*objMeta, error) { | |
+ r, err := c.getResource(ctx, obj) | |
if err != nil { | |
return nil, err | |
} | |
diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go | |
index 3cd745e4..6c47fc35 100644 | |
--- a/pkg/client/interfaces.go | |
+++ b/pkg/client/interfaces.go | |
@@ -18,9 +18,11 @@ package client | |
import ( | |
"context" | |
+ "time" | |
apierrors "k8s.io/apimachinery/pkg/api/errors" | |
"k8s.io/apimachinery/pkg/runtime/schema" | |
+ "k8s.io/client-go/tools/cache" | |
"k8s.io/apimachinery/pkg/api/meta" | |
"k8s.io/apimachinery/pkg/runtime" | |
@@ -44,6 +46,10 @@ type Patch interface { | |
Data(obj Object) ([]byte, error) | |
} | |
+// NewInformerFunc describes a function that creates SharedIndexInformers. | |
+// Its signature matches cache.NewSharedIndexInformer from client-go. | |
+type NewInformerFunc func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer | |
+ | |
// TODO(directxman12): is there a sane way to deal with get/delete options? | |
// Reader knows how to read and list Kubernetes objects. | |
diff --git a/pkg/client/metadata_client.go b/pkg/client/metadata_client.go | |
index d0c6b8e1..83680356 100644 | |
--- a/pkg/client/metadata_client.go | |
+++ b/pkg/client/metadata_client.go | |
@@ -20,11 +20,15 @@ import ( | |
"context" | |
"fmt" | |
"strings" | |
+ "sync" | |
+ lru "github.com/hashicorp/golang-lru/v2" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
"k8s.io/apimachinery/pkg/api/meta" | |
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
"k8s.io/apimachinery/pkg/runtime/schema" | |
"k8s.io/client-go/metadata" | |
+ "sigs.k8s.io/controller-runtime/pkg/kontext" | |
) | |
// TODO(directxman12): we could rewrite this on top of the low-level REST | |
@@ -34,12 +38,28 @@ import ( | |
// metadataClient is a client that reads & writes metadata-only requests to/from the API server. | |
type metadataClient struct { | |
- client metadata.Interface | |
- restMapper meta.RESTMapper | |
+ client metadata.Interface | |
+ restMapper func(ctx context.Context) (meta.RESTMapper, error) | |
+ mu sync.Mutex | |
+ mapperCache *lru.Cache[logicalcluster.Name, meta.RESTMapper] | |
} | |
-func (mc *metadataClient) getResourceInterface(gvk schema.GroupVersionKind, ns string) (metadata.ResourceInterface, error) { | |
- mapping, err := mc.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) | |
+func (mc *metadataClient) getResourceInterface(ctx context.Context, gvk schema.GroupVersionKind, ns string) (metadata.ResourceInterface, error) { | |
+ cluster, _ := kontext.ClusterFrom(ctx) | |
+ mc.mu.Lock() | |
+ mapper, _ := mc.mapperCache.Get(cluster) | |
+ if mapper == nil { | |
+ var err error | |
+ mapper, err = mc.restMapper(ctx) | |
+ if err != nil { | |
+ mc.mu.Unlock() | |
+ return nil, err | |
+ } | |
+ mc.mapperCache.Add(cluster, mapper) | |
+ } | |
+ mc.mu.Unlock() | |
+ | |
+ mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) | |
if err != nil { | |
return nil, err | |
} | |
@@ -56,7 +76,7 @@ func (mc *metadataClient) Delete(ctx context.Context, obj Object, opts ...Delete | |
return fmt.Errorf("metadata client did not understand object: %T", obj) | |
} | |
- resInt, err := mc.getResourceInterface(metadata.GroupVersionKind(), metadata.Namespace) | |
+ resInt, err := mc.getResourceInterface(ctx, metadata.GroupVersionKind(), metadata.Namespace) | |
if err != nil { | |
return err | |
} | |
@@ -77,7 +97,7 @@ func (mc *metadataClient) DeleteAllOf(ctx context.Context, obj Object, opts ...D | |
deleteAllOfOpts := DeleteAllOfOptions{} | |
deleteAllOfOpts.ApplyOptions(opts) | |
- resInt, err := mc.getResourceInterface(metadata.GroupVersionKind(), deleteAllOfOpts.ListOptions.Namespace) | |
+ resInt, err := mc.getResourceInterface(ctx, metadata.GroupVersionKind(), deleteAllOfOpts.ListOptions.Namespace) | |
if err != nil { | |
return err | |
} | |
@@ -93,7 +113,7 @@ func (mc *metadataClient) Patch(ctx context.Context, obj Object, patch Patch, op | |
} | |
gvk := metadata.GroupVersionKind() | |
- resInt, err := mc.getResourceInterface(gvk, metadata.Namespace) | |
+ resInt, err := mc.getResourceInterface(ctx, gvk, metadata.Namespace) | |
if err != nil { | |
return err | |
} | |
@@ -127,7 +147,7 @@ func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object, op | |
getOpts := GetOptions{} | |
getOpts.ApplyOptions(opts) | |
- resInt, err := mc.getResourceInterface(gvk, key.Namespace) | |
+ resInt, err := mc.getResourceInterface(ctx, gvk, key.Namespace) | |
if err != nil { | |
return err | |
} | |
@@ -154,7 +174,7 @@ func (mc *metadataClient) List(ctx context.Context, obj ObjectList, opts ...List | |
listOpts := ListOptions{} | |
listOpts.ApplyOptions(opts) | |
- resInt, err := mc.getResourceInterface(gvk, listOpts.Namespace) | |
+ resInt, err := mc.getResourceInterface(ctx, gvk, listOpts.Namespace) | |
if err != nil { | |
return err | |
} | |
@@ -175,7 +195,7 @@ func (mc *metadataClient) PatchSubResource(ctx context.Context, obj Object, subR | |
} | |
gvk := metadata.GroupVersionKind() | |
- resInt, err := mc.getResourceInterface(gvk, metadata.Namespace) | |
+ resInt, err := mc.getResourceInterface(ctx, gvk, metadata.Namespace) | |
if err != nil { | |
return err | |
} | |
diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go | |
index 92afd9a9..aff18d84 100644 | |
--- a/pkg/client/typed_client.go | |
+++ b/pkg/client/typed_client.go | |
@@ -32,7 +32,7 @@ type typedClient struct { | |
// Create implements client.Client. | |
func (c *typedClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -51,7 +51,7 @@ func (c *typedClient) Create(ctx context.Context, obj Object, opts ...CreateOpti | |
// Update implements client.Client. | |
func (c *typedClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -71,7 +71,7 @@ func (c *typedClient) Update(ctx context.Context, obj Object, opts ...UpdateOpti | |
// Delete implements client.Client. | |
func (c *typedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -90,7 +90,7 @@ func (c *typedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOpti | |
// DeleteAllOf implements client.Client. | |
func (c *typedClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -109,7 +109,7 @@ func (c *typedClient) DeleteAllOf(ctx context.Context, obj Object, opts ...Delet | |
// Patch implements client.Client. | |
func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -134,7 +134,7 @@ func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts . | |
// Get implements client.Client. | |
func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { | |
- r, err := c.resources.getResource(obj) | |
+ r, err := c.resources.getResource(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -149,7 +149,7 @@ func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts . | |
// List implements client.Client. | |
func (c *typedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error { | |
- r, err := c.resources.getResource(obj) | |
+ r, err := c.resources.getResource(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -166,7 +166,7 @@ func (c *typedClient) List(ctx context.Context, obj ObjectList, opts ...ListOpti | |
} | |
func (c *typedClient) GetSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceGetOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -189,7 +189,7 @@ func (c *typedClient) GetSubResource(ctx context.Context, obj, subResourceObj Ob | |
} | |
func (c *typedClient) CreateSubResource(ctx context.Context, obj Object, subResourceObj Object, subResource string, opts ...SubResourceCreateOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -214,7 +214,7 @@ func (c *typedClient) CreateSubResource(ctx context.Context, obj Object, subReso | |
// UpdateSubResource used by SubResourceWriter to write status. | |
func (c *typedClient) UpdateSubResource(ctx context.Context, obj Object, subResource string, opts ...SubResourceUpdateOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -249,7 +249,7 @@ func (c *typedClient) UpdateSubResource(ctx context.Context, obj Object, subReso | |
// PatchSubResource used by SubResourceWriter to write subresource. | |
func (c *typedClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error { | |
- o, err := c.resources.getObjMeta(obj) | |
+ o, err := c.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go | |
index 0d969517..872f3160 100644 | |
--- a/pkg/client/unstructured_client.go | |
+++ b/pkg/client/unstructured_client.go | |
@@ -41,7 +41,7 @@ func (uc *unstructuredClient) Create(ctx context.Context, obj Object, opts ...Cr | |
gvk := u.GetObjectKind().GroupVersionKind() | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -70,7 +70,7 @@ func (uc *unstructuredClient) Update(ctx context.Context, obj Object, opts ...Up | |
gvk := u.GetObjectKind().GroupVersionKind() | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -97,7 +97,7 @@ func (uc *unstructuredClient) Delete(ctx context.Context, obj Object, opts ...De | |
return fmt.Errorf("unstructured client did not understand object: %T", obj) | |
} | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -120,7 +120,7 @@ func (uc *unstructuredClient) DeleteAllOf(ctx context.Context, obj Object, opts | |
return fmt.Errorf("unstructured client did not understand object: %T", obj) | |
} | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -143,7 +143,7 @@ func (uc *unstructuredClient) Patch(ctx context.Context, obj Object, patch Patch | |
return fmt.Errorf("unstructured client did not understand object: %T", obj) | |
} | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -178,7 +178,7 @@ func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object | |
getOpts := GetOptions{} | |
getOpts.ApplyOptions(opts) | |
- r, err := uc.resources.getResource(obj) | |
+ r, err := uc.resources.getResource(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -206,7 +206,7 @@ func (uc *unstructuredClient) List(ctx context.Context, obj ObjectList, opts ... | |
gvk := u.GetObjectKind().GroupVersionKind() | |
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") | |
- r, err := uc.resources.getResource(obj) | |
+ r, err := uc.resources.getResource(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -235,7 +235,7 @@ func (uc *unstructuredClient) GetSubResource(ctx context.Context, obj, subResour | |
subResourceObj.SetName(obj.GetName()) | |
} | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -266,7 +266,7 @@ func (uc *unstructuredClient) CreateSubResource(ctx context.Context, obj, subRes | |
subResourceObj.SetName(obj.GetName()) | |
} | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -290,7 +290,7 @@ func (uc *unstructuredClient) UpdateSubResource(ctx context.Context, obj Object, | |
return fmt.Errorf("unstructured client did not understand object: %T", obj) | |
} | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
@@ -328,7 +328,7 @@ func (uc *unstructuredClient) PatchSubResource(ctx context.Context, obj Object, | |
gvk := u.GetObjectKind().GroupVersionKind() | |
- o, err := uc.resources.getObjMeta(obj) | |
+ o, err := uc.resources.getObjMeta(ctx, obj) | |
if err != nil { | |
return err | |
} | |
diff --git a/pkg/client/watch.go b/pkg/client/watch.go | |
index 181b22a6..317c87ff 100644 | |
--- a/pkg/client/watch.go | |
+++ b/pkg/client/watch.go | |
@@ -67,7 +67,7 @@ func (w *watchingClient) metadataWatch(ctx context.Context, obj *metav1.PartialO | |
listOpts := w.listOpts(opts...) | |
- resInt, err := w.client.metadataClient.getResourceInterface(gvk, listOpts.Namespace) | |
+ resInt, err := w.client.metadataClient.getResourceInterface(ctx, gvk, listOpts.Namespace) | |
if err != nil { | |
return nil, err | |
} | |
@@ -76,7 +76,7 @@ func (w *watchingClient) metadataWatch(ctx context.Context, obj *metav1.PartialO | |
} | |
func (w *watchingClient) unstructuredWatch(ctx context.Context, obj runtime.Unstructured, opts ...ListOption) (watch.Interface, error) { | |
- r, err := w.client.unstructuredClient.resources.getResource(obj) | |
+ r, err := w.client.unstructuredClient.resources.getResource(ctx, obj) | |
if err != nil { | |
return nil, err | |
} | |
@@ -91,7 +91,7 @@ func (w *watchingClient) unstructuredWatch(ctx context.Context, obj runtime.Unst | |
} | |
func (w *watchingClient) typedWatch(ctx context.Context, obj ObjectList, opts ...ListOption) (watch.Interface, error) { | |
- r, err := w.client.typedClient.resources.getResource(obj) | |
+ r, err := w.client.typedClient.resources.getResource(ctx, obj) | |
if err != nil { | |
return nil, err | |
} | |
diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go | |
index 248893ea..8836658d 100644 | |
--- a/pkg/cluster/cluster.go | |
+++ b/pkg/cluster/cluster.go | |
@@ -121,6 +121,11 @@ type Options struct { | |
// By default, the client will use the cache for reads and direct calls for writes. | |
Client client.Options | |
+ // NewAPIReaderFunc is the function that creates the APIReader client to be | |
+ // used by the manager. If not set this will use the default new APIReader | |
+ // function. | |
+ NewAPIReader client.NewAPIReaderFunc | |
+ | |
// NewClient is the func that creates the client to be used by the manager. | |
// If not set this will create a Client backed by a Cache for read operations | |
// and a direct Client for write operations. | |
@@ -230,7 +235,7 @@ func New(config *rest.Config, opts ...Option) (Cluster, error) { | |
} | |
// Create the API Reader, a client with no cache. | |
- clientReader, err := client.New(config, client.Options{ | |
+ clientReader, err := options.NewAPIReader(config, client.Options{ | |
HTTPClient: options.HTTPClient, | |
Scheme: options.Scheme, | |
Mapper: mapper, | |
@@ -280,6 +285,10 @@ func setOptionsDefaults(options Options, config *rest.Config) (Options, error) { | |
options.MapperProvider = apiutil.NewDynamicRESTMapper | |
} | |
+ if options.NewAPIReader == nil { | |
+ options.NewAPIReader = client.NewAPIReader | |
+ } | |
+ | |
// Allow users to define how to create a new client | |
if options.NewClient == nil { | |
options.NewClient = client.New | |
diff --git a/pkg/handler/enqueue.go b/pkg/handler/enqueue.go | |
index 1a1d1ab2..8c0e3733 100644 | |
--- a/pkg/handler/enqueue.go | |
+++ b/pkg/handler/enqueue.go | |
@@ -20,6 +20,8 @@ import ( | |
"context" | |
"reflect" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ | |
"k8s.io/apimachinery/pkg/types" | |
"k8s.io/client-go/util/workqueue" | |
"sigs.k8s.io/controller-runtime/pkg/client" | |
@@ -52,25 +54,16 @@ func (e *TypedEnqueueRequestForObject[T]) Create(ctx context.Context, evt event. | |
enqueueLog.Error(nil, "CreateEvent received with no metadata", "event", evt) | |
return | |
} | |
- q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ | |
- Name: evt.Object.GetName(), | |
- Namespace: evt.Object.GetNamespace(), | |
- }}) | |
+ q.Add(request(evt.Object)) | |
} | |
// Update implements EventHandler. | |
func (e *TypedEnqueueRequestForObject[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { | |
switch { | |
case !isNil(evt.ObjectNew): | |
- q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ | |
- Name: evt.ObjectNew.GetName(), | |
- Namespace: evt.ObjectNew.GetNamespace(), | |
- }}) | |
+ q.Add(request(evt.ObjectNew)) | |
case !isNil(evt.ObjectOld): | |
- q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ | |
- Name: evt.ObjectOld.GetName(), | |
- Namespace: evt.ObjectOld.GetNamespace(), | |
- }}) | |
+ q.Add(request(evt.ObjectOld)) | |
default: | |
enqueueLog.Error(nil, "UpdateEvent received with no metadata", "event", evt) | |
} | |
@@ -82,10 +75,7 @@ func (e *TypedEnqueueRequestForObject[T]) Delete(ctx context.Context, evt event. | |
enqueueLog.Error(nil, "DeleteEvent received with no metadata", "event", evt) | |
return | |
} | |
- q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ | |
- Name: evt.Object.GetName(), | |
- Namespace: evt.Object.GetNamespace(), | |
- }}) | |
+ q.Add(request(evt.Object)) | |
} | |
// Generic implements EventHandler. | |
@@ -94,10 +84,18 @@ func (e *TypedEnqueueRequestForObject[T]) Generic(ctx context.Context, evt event | |
enqueueLog.Error(nil, "GenericEvent received with no metadata", "event", evt) | |
return | |
} | |
- q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ | |
- Name: evt.Object.GetName(), | |
- Namespace: evt.Object.GetNamespace(), | |
- }}) | |
+ q.Add(request(evt.Object)) | |
+} | |
+ | |
+func request(obj client.Object) reconcile.Request { | |
+ return reconcile.Request{ | |
+ // TODO(kcp) Need to implement a non-kcp-specific way to support this | |
+ ClusterName: logicalcluster.From(obj).String(), | |
+ NamespacedName: types.NamespacedName{ | |
+ Namespace: obj.GetNamespace(), | |
+ Name: obj.GetName(), | |
+ }, | |
+ } | |
} | |
func isNil(arg any) bool { | |
diff --git a/pkg/handler/enqueue_owner.go b/pkg/handler/enqueue_owner.go | |
index 1680043b..bf0aa43d 100644 | |
--- a/pkg/handler/enqueue_owner.go | |
+++ b/pkg/handler/enqueue_owner.go | |
@@ -20,6 +20,8 @@ import ( | |
"context" | |
"fmt" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ | |
"k8s.io/apimachinery/pkg/api/meta" | |
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
"k8s.io/apimachinery/pkg/runtime" | |
@@ -181,9 +183,12 @@ func (e *enqueueRequestForOwner[object]) getOwnerReconcileRequest(obj metav1.Obj | |
// object in the event. | |
if ref.Kind == e.groupKind.Kind && refGV.Group == e.groupKind.Group { | |
// Match found - add a Request for the object referred to in the OwnerReference | |
- request := reconcile.Request{NamespacedName: types.NamespacedName{ | |
- Name: ref.Name, | |
- }} | |
+ request := reconcile.Request{ | |
+ ClusterName: logicalcluster.From(obj).String(), | |
+ NamespacedName: types.NamespacedName{ | |
+ Name: ref.Name, | |
+ }, | |
+ } | |
// if owner is not namespaced then we should not set the namespace | |
mapping, err := e.mapper.RESTMapping(e.groupKind, refGV.Version) | |
diff --git a/pkg/kcp/helper.go b/pkg/kcp/helper.go | |
new file mode 100644 | |
index 00000000..85d39001 | |
--- /dev/null | |
+++ b/pkg/kcp/helper.go | |
@@ -0,0 +1,34 @@ | |
+/* | |
+Copyright 2024 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package kcp | |
+ | |
+import ( | |
+ "context" | |
+ | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+ "sigs.k8s.io/controller-runtime/pkg/kontext" | |
+ "sigs.k8s.io/controller-runtime/pkg/reconcile" | |
+) | |
+ | |
+// WithClusterInContext injects a cluster name into a context such that | |
+// cluster clients and cache work out of the box. | |
+func WithClusterInContext(r reconcile.Reconciler) reconcile.Reconciler { | |
+ return reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { | |
+ ctx = kontext.WithCluster(ctx, logicalcluster.Name(req.ClusterName)) | |
+ return r.Reconcile(ctx, req) | |
+ }) | |
+} | |
diff --git a/pkg/kcp/kcp_suite_test.go b/pkg/kcp/kcp_suite_test.go | |
new file mode 100644 | |
index 00000000..b81a2019 | |
--- /dev/null | |
+++ b/pkg/kcp/kcp_suite_test.go | |
@@ -0,0 +1,35 @@ | |
+/* | |
+Copyright 2018 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. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT 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 kcp_test | |
+ | |
+import ( | |
+ "testing" | |
+ | |
+ . "github.com/onsi/ginkgo/v2" | |
+ . "github.com/onsi/gomega" | |
+ logf "sigs.k8s.io/controller-runtime/pkg/log" | |
+ "sigs.k8s.io/controller-runtime/pkg/log/zap" | |
+) | |
+ | |
+func TestKCP(t *testing.T) { | |
+ RegisterFailHandler(Fail) | |
+ RunSpecs(t, "kcp Suite") | |
+} | |
+ | |
+var _ = BeforeSuite(func() { | |
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) | |
+}) | |
diff --git a/pkg/kcp/kcp_test.go b/pkg/kcp/kcp_test.go | |
new file mode 100644 | |
index 00000000..a4bfce85 | |
--- /dev/null | |
+++ b/pkg/kcp/kcp_test.go | |
@@ -0,0 +1,339 @@ | |
+package kcp | |
+ | |
+import ( | |
+ "context" | |
+ "fmt" | |
+ "net/http" | |
+ "net/http/httptest" | |
+ "sync" | |
+ "time" | |
+ | |
+ . "github.com/onsi/ginkgo/v2" | |
+ . "github.com/onsi/gomega" | |
+ corev1 "k8s.io/api/core/v1" | |
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | |
+ "k8s.io/apimachinery/pkg/types" | |
+ "k8s.io/apimachinery/pkg/util/sets" | |
+ "k8s.io/client-go/rest" | |
+ "sigs.k8s.io/controller-runtime/pkg/cache" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/kontext" | |
+) | |
+ | |
+var _ = Describe("NewClusterAwareClient", Ordered, func() { | |
+ var ( | |
+ srv *httptest.Server | |
+ mu sync.Mutex | |
+ paths []string | |
+ cfg *rest.Config | |
+ ) | |
+ | |
+ BeforeAll(func() { | |
+ srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | |
+ mu.Lock() | |
+ paths = append(paths, req.URL.Path) | |
+ mu.Unlock() | |
+ | |
+ switch req.URL.Path { | |
+ case "/api/v1", "/clusters/root/api/v1", "/clusters/*/api/v1": | |
+ w.Header().Set("Content-Type", "application/json") | |
+ w.WriteHeader(http.StatusOK) | |
+ _, _ = w.Write([]byte(`{"kind":"APIResourceList","groupVersion":"v1","resources":[{"name":"pods","singularName":"pod","namespaced":true,"kind":"Pod","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["po"],"categories":["all"],"storageVersionHash":"xPOwRZ+Yhw8="}]}`)) | |
+ case "/api/v1/pods", "/clusters/root/api/v1/pods", "/clusters/*/api/v1/pods": | |
+ if req.URL.Query().Get("watch") != "true" { | |
+ w.Header().Set("Content-Type", "application/json") | |
+ w.WriteHeader(http.StatusOK) | |
+ _, _ = w.Write([]byte(`{"kind": "PodList","apiVersion": "v1","metadata": {"resourceVersion": "184126176"}, "items": [{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"default","resourceVersion":"184126176"}}]}`)) | |
+ return | |
+ } | |
+ fallthrough | |
+ case "/api/v1/namespaces/default/pods/foo", "/clusters/root/api/v1/namespaces/default/pods/foo": | |
+ w.Header().Set("Content-Type", "application/json") | |
+ w.WriteHeader(http.StatusOK) | |
+ _, _ = w.Write([]byte(`{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"default","resourceVersion":"184126176"}}`)) | |
+ default: | |
+ _, _ = w.Write([]byte(fmt.Sprintf("Not found %q", req.RequestURI))) | |
+ w.WriteHeader(http.StatusNotFound) | |
+ } | |
+ })) | |
+ | |
+ cfg = &rest.Config{ | |
+ Host: srv.URL, | |
+ } | |
+ Expect(rest.SetKubernetesDefaults(cfg)).To(Succeed()) | |
+ }) | |
+ | |
+ BeforeEach(func() { | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ paths = []string{} | |
+ }) | |
+ | |
+ AfterAll(func() { | |
+ srv.Close() | |
+ }) | |
+ | |
+ Describe("with typed list", func() { | |
+ It("should work with no cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &corev1.PodList{} | |
+ err = cl.List(ctx, pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pod := &corev1.Pod{} | |
+ err = cl.Get(ctx, types.NamespacedName{Namespace: "default", Name: "foo"}, pod) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/api/v1", "/api/v1/pods", "/api/v1/namespaces/default/pods/foo"})) | |
+ }) | |
+ | |
+ It("should work with a cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &corev1.PodList{} | |
+ err = cl.List(kontext.WithCluster(ctx, "root"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pod := &corev1.Pod{} | |
+ err = cl.Get(kontext.WithCluster(ctx, "root"), types.NamespacedName{Namespace: "default", Name: "foo"}, pod) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/root/api/v1", "/clusters/root/api/v1/pods", "/clusters/root/api/v1/namespaces/default/pods/foo"})) | |
+ }) | |
+ | |
+ It("should work with a wildcard cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &corev1.PodList{} | |
+ err = cl.List(kontext.WithCluster(ctx, "*"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/*/api/v1", "/clusters/*/api/v1/pods"})) | |
+ }) | |
+ }) | |
+ | |
+ Describe("with unstructured list", func() { | |
+ It("should work with no cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &unstructured.UnstructuredList{} | |
+ pods.SetAPIVersion("v1") | |
+ pods.SetKind("PodList") | |
+ err = cl.List(ctx, pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pod := &unstructured.Unstructured{} | |
+ pod.SetAPIVersion("v1") | |
+ pod.SetKind("Pod") | |
+ err = cl.Get(ctx, types.NamespacedName{Namespace: "default", Name: "foo"}, pod) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/api/v1", "/api/v1/pods", "/api/v1/namespaces/default/pods/foo"})) | |
+ }) | |
+ | |
+ It("should work with a cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &unstructured.UnstructuredList{} | |
+ pods.SetAPIVersion("v1") | |
+ pods.SetKind("PodList") | |
+ err = cl.List(kontext.WithCluster(ctx, "root"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pod := &unstructured.Unstructured{} | |
+ pod.SetAPIVersion("v1") | |
+ pod.SetKind("Pod") | |
+ err = cl.Get(kontext.WithCluster(ctx, "root"), types.NamespacedName{Namespace: "default", Name: "foo"}, pod) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/root/api/v1", "/clusters/root/api/v1/pods", "/clusters/root/api/v1/namespaces/default/pods/foo"})) | |
+ }) | |
+ | |
+ It("should work with a wildcard cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &unstructured.UnstructuredList{} | |
+ pods.SetAPIVersion("v1") | |
+ pods.SetKind("PodList") | |
+ err = cl.List(kontext.WithCluster(ctx, "*"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/*/api/v1", "/clusters/*/api/v1/pods"})) | |
+ }) | |
+ }) | |
+ | |
+ Describe("with a metadata object", func() { | |
+ It("should work with no cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &metav1.PartialObjectMetadataList{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "PodList"}} | |
+ err = cl.List(ctx, pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/api/v1", "/api/v1/pods"})) | |
+ }) | |
+ | |
+ It("should work with a cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &metav1.PartialObjectMetadataList{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "PodList"}} | |
+ err = cl.List(kontext.WithCluster(ctx, "root"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/root/api/v1", "/clusters/root/api/v1/pods"})) | |
+ }) | |
+ | |
+ It("should work with a wildcard cluster in the kontext", func(ctx context.Context) { | |
+ cl, err := NewClusterAwareClient(cfg, client.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ pods := &metav1.PartialObjectMetadataList{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "PodList"}} | |
+ err = cl.List(kontext.WithCluster(ctx, "*"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/*/api/v1", "/clusters/*/api/v1/pods"})) | |
+ }) | |
+ }) | |
+}) | |
+ | |
+var _ = Describe("NewClusterAwareCache", Ordered, func() { | |
+ var ( | |
+ cancelCtx context.CancelFunc | |
+ srv *httptest.Server | |
+ mu sync.Mutex | |
+ paths []string | |
+ cfg *rest.Config | |
+ c cache.Cache | |
+ ) | |
+ | |
+ BeforeAll(func() { | |
+ var ctx context.Context | |
+ ctx, cancelCtx = context.WithCancel(context.Background()) | |
+ | |
+ srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | |
+ mu.Lock() | |
+ pth := req.URL.Path | |
+ if req.URL.Query().Get("watch") == "true" { | |
+ pth += "?watch=true" | |
+ } | |
+ paths = append(paths, pth) | |
+ mu.Unlock() | |
+ | |
+ switch { | |
+ case req.URL.Path == "/clusters/*/api/v1": | |
+ w.Header().Set("Content-Type", "application/json") | |
+ w.WriteHeader(http.StatusOK) | |
+ _, _ = w.Write([]byte(`{"kind":"APIResourceList","groupVersion":"v1","resources":[{"name":"pods","singularName":"pod","namespaced":true,"kind":"Pod","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["po"],"categories":["all"],"storageVersionHash":"xPOwRZ+Yhw8="}]}`)) | |
+ case req.URL.Path == "/clusters/*/api/v1/pods" && req.URL.Query().Get("watch") != "true": | |
+ w.Header().Set("Content-Type", "application/json") | |
+ w.WriteHeader(http.StatusOK) | |
+ _, _ = w.Write([]byte(`{"kind": "PodList","apiVersion": "v1","metadata": {"resourceVersion": "184126176"}, "items": [ | |
+ {"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"default","resourceVersion":"184126176","annotations":{"kcp.io/cluster":"root"}}}, | |
+ {"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"default","resourceVersion":"184126093","annotations":{"kcp.io/cluster":"ws"}}} | |
+ ]}`)) | |
+ case req.URL.Path == "/clusters/*/api/v1/pods" && req.URL.Query().Get("watch") == "true": | |
+ w.Header().Set("Content-Type", "application/json") | |
+ w.Header().Set("Transfer-Encoding", "chunked") | |
+ w.WriteHeader(http.StatusOK) | |
+ _, _ = w.Write([]byte(`{"type":"ADDED","object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"bar","namespace":"default","resourceVersion":"184126177","annotations":{"kcp.io/cluster":"root"}}}}`)) | |
+ _, _ = w.Write([]byte(`{"type":"ADDED","object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"bar","namespace":"default","resourceVersion":"184126178","annotations":{"kcp.io/cluster":"ws"}}}}`)) | |
+ if w, ok := w.(http.Flusher); ok { | |
+ w.Flush() | |
+ } | |
+ time.Sleep(1 * time.Second) | |
+ default: | |
+ w.WriteHeader(http.StatusNotFound) | |
+ _, _ = w.Write([]byte(fmt.Sprintf("Not found %q", req.RequestURI))) | |
+ } | |
+ })) | |
+ go func() { | |
+ <-ctx.Done() | |
+ srv.Close() | |
+ }() | |
+ | |
+ cfg = &rest.Config{ | |
+ Host: srv.URL, | |
+ } | |
+ Expect(rest.SetKubernetesDefaults(cfg)).To(Succeed()) | |
+ | |
+ var err error | |
+ c, err = NewClusterAwareCache(cfg, cache.Options{}) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ go func() { | |
+ if err := c.Start(ctx); err != nil { | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ } | |
+ }() | |
+ c.WaitForCacheSync(ctx) | |
+ }) | |
+ | |
+ BeforeEach(func() { | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ paths = []string{} | |
+ }) | |
+ | |
+ AfterAll(func() { | |
+ cancelCtx() | |
+ }) | |
+ | |
+ It("should always access wildcard clusters and serve other clusters from memory", func(ctx context.Context) { | |
+ pod := &corev1.Pod{} | |
+ err := c.Get(kontext.WithCluster(ctx, "root"), types.NamespacedName{Namespace: "default", Name: "foo"}, pod) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ | |
+ mu.Lock() | |
+ defer mu.Unlock() | |
+ Expect(paths).To(Equal([]string{"/clusters/*/api/v1", "/clusters/*/api/v1/pods", "/clusters/*/api/v1/pods?watch=true"})) | |
+ }) | |
+ | |
+ It("should return only the pods from the requested cluster", func(ctx context.Context) { | |
+ pod := &corev1.Pod{} | |
+ err := c.Get(kontext.WithCluster(ctx, "root"), types.NamespacedName{Namespace: "default", Name: "foo"}, pod) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ Expect(pod.Annotations).To(HaveKeyWithValue("kcp.io/cluster", "root")) | |
+ | |
+ pods := &corev1.PodList{} | |
+ err = c.List(kontext.WithCluster(ctx, "root"), pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ Expect(pods.Items).To(HaveLen(2)) | |
+ Expect(pods.Items[0].Annotations).To(HaveKeyWithValue("kcp.io/cluster", "root")) | |
+ Expect(pods.Items[1].Annotations).To(HaveKeyWithValue("kcp.io/cluster", "root")) | |
+ Expect(sets.New(pods.Items[0].Name, pods.Items[1].Name)).To(Equal(sets.New("foo", "bar"))) | |
+ }) | |
+ | |
+ It("should return all pods from all clusters without cluster in context", func(ctx context.Context) { | |
+ pods := &corev1.PodList{} | |
+ err := c.List(ctx, pods) | |
+ Expect(err).NotTo(HaveOccurred()) | |
+ Expect(pods.Items).To(HaveLen(4)) | |
+ }) | |
+}) | |
diff --git a/pkg/kcp/wrappers.go b/pkg/kcp/wrappers.go | |
new file mode 100644 | |
index 00000000..974cec58 | |
--- /dev/null | |
+++ b/pkg/kcp/wrappers.go | |
@@ -0,0 +1,280 @@ | |
+/* | |
+Copyright 2022 The KCP Authors. | |
+ | |
+Licensed under the Apache License, Version 2.0 (the "License"); | |
+you may not use this file except in compliance with the License. | |
+You may obtain a copy of the License at | |
+ | |
+ http://www.apache.org/licenses/LICENSE-2.0 | |
+ | |
+Unless required by applicable law or agreed to in writing, software | |
+distributed under the License is distributed on an "AS IS" BASIS, | |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+See the License for the specific language governing permissions and | |
+limitations under the License. | |
+*/ | |
+ | |
+package kcp | |
+ | |
+import ( | |
+ "context" | |
+ "fmt" | |
+ "net/http" | |
+ "regexp" | |
+ "strings" | |
+ "time" | |
+ | |
+ "k8s.io/apimachinery/pkg/api/meta" | |
+ "k8s.io/apimachinery/pkg/runtime" | |
+ "k8s.io/client-go/rest" | |
+ k8scache "k8s.io/client-go/tools/cache" | |
+ ctrl "sigs.k8s.io/controller-runtime" | |
+ "sigs.k8s.io/controller-runtime/pkg/cache" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
+ "sigs.k8s.io/controller-runtime/pkg/client/apiutil" | |
+ "sigs.k8s.io/controller-runtime/pkg/kontext" | |
+ "sigs.k8s.io/controller-runtime/pkg/manager" | |
+ | |
+ kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" | |
+ "github.com/kcp-dev/apimachinery/v2/third_party/informers" | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+) | |
+ | |
+// NewClusterAwareManager returns a kcp-aware manager with appropriate defaults for cache and | |
+// client creation. | |
+func NewClusterAwareManager(cfg *rest.Config, options ctrl.Options) (manager.Manager, error) { | |
+ if options.NewCache == nil { | |
+ options.NewCache = NewClusterAwareCache | |
+ } | |
+ | |
+ if options.NewAPIReader == nil { | |
+ options.NewAPIReader = NewClusterAwareAPIReader | |
+ } | |
+ | |
+ if options.NewClient == nil { | |
+ options.NewClient = NewClusterAwareClient | |
+ } | |
+ | |
+ if options.MapperProvider == nil { | |
+ options.MapperProvider = newWildcardClusterMapperProvider | |
+ } | |
+ | |
+ cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper { | |
+ return newClusterAwareRoundTripper(rt) | |
+ }) | |
+ return ctrl.NewManager(cfg, options) | |
+} | |
+ | |
+// NewInformerWithClusterIndexes returns a SharedIndexInformer that is configured | |
+// ClusterIndexName and ClusterAndNamespaceIndexName indexes. | |
+func NewInformerWithClusterIndexes(lw k8scache.ListerWatcher, obj runtime.Object, syncPeriod time.Duration, indexers k8scache.Indexers) k8scache.SharedIndexInformer { | |
+ indexers[kcpcache.ClusterIndexName] = kcpcache.ClusterIndexFunc | |
+ indexers[kcpcache.ClusterAndNamespaceIndexName] = kcpcache.ClusterAndNamespaceIndexFunc | |
+ | |
+ return informers.NewSharedIndexInformer(lw, obj, syncPeriod, indexers) | |
+} | |
+ | |
+// NewClusterAwareCache returns a cache.Cache that handles multi-cluster watches. | |
+func NewClusterAwareCache(config *rest.Config, opts cache.Options) (cache.Cache, error) { | |
+ c := rest.CopyConfig(config) | |
+ c.Host = strings.TrimSuffix(c.Host, "/") + "/clusters/*" | |
+ | |
+ opts.NewInformerFunc = NewInformerWithClusterIndexes | |
+ return cache.New(c, opts) | |
+} | |
+ | |
+// NewClusterAwareAPIReader returns a client.Reader that provides read-only access to the API server, | |
+// and is configured to use the context to scope requests to the proper cluster. To scope requests, | |
+// pass the request context with the cluster set. | |
+// Example: | |
+// | |
+// import ( | |
+// "context" | |
+// kcpclient "github.com/kcp-dev/apimachinery/v2/pkg/client" | |
+// ctrl "sigs.k8s.io/controller-runtime" | |
+// ) | |
+// func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | |
+// ctx = kcpclient.WithCluster(ctx, req.ObjectKey.Cluster) | |
+// // from here on pass this context to all client calls | |
+// ... | |
+// } | |
+func NewClusterAwareAPIReader(config *rest.Config, opts client.Options) (client.Reader, error) { | |
+ if opts.HTTPClient == nil { | |
+ httpClient, err := NewClusterAwareHTTPClient(config) | |
+ if err != nil { | |
+ return nil, err | |
+ } | |
+ opts.HTTPClient = httpClient | |
+ } | |
+ if opts.Mapper == nil && opts.MapperWithContext == nil { | |
+ opts.MapperWithContext = NewClusterAwareMapperProvider(config, opts.HTTPClient) | |
+ } | |
+ return client.NewAPIReader(config, opts) | |
+} | |
+ | |
+// NewClusterAwareClient returns a client.Client that is configured to use the context | |
+// to scope requests to the proper cluster. To scope requests, pass the request context with the cluster set. | |
+// Example: | |
+// | |
+// import ( | |
+// "context" | |
+// kcpclient "github.com/kcp-dev/apimachinery/v2/pkg/client" | |
+// ctrl "sigs.k8s.io/controller-runtime" | |
+// ) | |
+// func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | |
+// ctx = kcpclient.WithCluster(ctx, req.ObjectKey.Cluster) | |
+// // from here on pass this context to all client calls | |
+// ... | |
+// } | |
+func NewClusterAwareClient(config *rest.Config, opts client.Options) (client.Client, error) { | |
+ opts, err := applyClientOptions(config, opts) | |
+ if err != nil { | |
+ return nil, err | |
+ } | |
+ | |
+ return client.New(config, opts) | |
+} | |
+ | |
+func applyClientOptions(config *rest.Config, opts client.Options) (client.Options, error) { | |
+ if opts.HTTPClient == nil { | |
+ httpClient, err := NewClusterAwareHTTPClient(config) | |
+ if err != nil { | |
+ return opts, err | |
+ } | |
+ opts.HTTPClient = httpClient | |
+ } | |
+ if opts.Mapper == nil && opts.MapperWithContext == nil { | |
+ opts.MapperWithContext = NewClusterAwareMapperProvider(config, opts.HTTPClient) | |
+ } | |
+ | |
+ return opts, nil | |
+} | |
+ | |
+// NewClusterAwareHTTPClient returns an http.Client with a cluster aware round tripper. | |
+func NewClusterAwareHTTPClient(config *rest.Config) (*http.Client, error) { | |
+ httpClient, err := rest.HTTPClientFor(config) | |
+ if err != nil { | |
+ return nil, err | |
+ } | |
+ | |
+ httpClient.Transport = newClusterAwareRoundTripper(httpClient.Transport) | |
+ return httpClient, nil | |
+} | |
+ | |
+// NewClusterAwareClientWithWatch returns a new WithWatch with a cluster aware client underneath. | |
+func NewClusterAwareClientWithWatch(config *rest.Config, options client.Options) (client.WithWatch, error) { | |
+ opts, err := applyClientOptions(config, options) | |
+ if err != nil { | |
+ return nil, err | |
+ } | |
+ | |
+ return client.NewWithWatch(config, opts) | |
+} | |
+ | |
+// NewClusterAwareMapperProvider returns a function producing RESTMapper for the | |
+// cluster specified in the context. | |
+func NewClusterAwareMapperProvider(c *rest.Config, httpClient *http.Client) func(ctx context.Context) (meta.RESTMapper, error) { | |
+ return func(ctx context.Context) (meta.RESTMapper, error) { | |
+ cluster, _ := kontext.ClusterFrom(ctx) // intentionally ignoring second "found" return value | |
+ cl := *httpClient | |
+ cl.Transport = clusterRoundTripper{cluster: cluster.Path(), delegate: httpClient.Transport} | |
+ return apiutil.NewDynamicRESTMapper(c, &cl) | |
+ } | |
+} | |
+ | |
+// newWildcardClusterMapperProvider returns a RESTMapper that talks to the /clusters/* endpoint. | |
+func newWildcardClusterMapperProvider(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) { | |
+ mapperCfg := rest.CopyConfig(c) | |
+ if !strings.HasSuffix(mapperCfg.Host, "/clusters/*") { | |
+ mapperCfg.Host += "/clusters/*" | |
+ } | |
+ | |
+ return apiutil.NewDynamicRESTMapper(mapperCfg, httpClient) | |
+} | |
+ | |
+// ClusterAwareBuilderWithOptions returns a cluster aware Cache constructor that will build | |
+// a cache honoring the options argument, this is useful to specify options like | |
+// SelectorsDefaultNamespaces | |
+// WARNING: If SelectorsByObject is specified, filtered out resources are not | |
+// returned. | |
+// WARNING: If UnsafeDisableDeepCopy is enabled, you must DeepCopy any object | |
+// returned from cache get/list before mutating it. | |
+func ClusterAwareBuilderWithOptions(options cache.Options) cache.NewCacheFunc { | |
+ return func(config *rest.Config, opts cache.Options) (cache.Cache, error) { | |
+ if options.Scheme == nil { | |
+ options.Scheme = opts.Scheme | |
+ } | |
+ if options.Mapper == nil { | |
+ options.Mapper = opts.Mapper | |
+ } | |
+ if options.SyncPeriod == nil { | |
+ options.SyncPeriod = opts.SyncPeriod | |
+ } | |
+ if opts.DefaultNamespaces == nil { | |
+ opts.DefaultNamespaces = options.DefaultNamespaces | |
+ } | |
+ | |
+ return NewClusterAwareCache(config, options) | |
+ } | |
+} | |
+ | |
+// clusterAwareRoundTripper is a cluster-aware wrapper around http.RoundTripper | |
+// taking the cluster from the context. | |
+type clusterAwareRoundTripper struct { | |
+ delegate http.RoundTripper | |
+} | |
+ | |
+// newClusterAwareRoundTripper creates a new cluster aware round tripper. | |
+func newClusterAwareRoundTripper(delegate http.RoundTripper) *clusterAwareRoundTripper { | |
+ return &clusterAwareRoundTripper{ | |
+ delegate: delegate, | |
+ } | |
+} | |
+ | |
+func (c *clusterAwareRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | |
+ cluster, ok := kontext.ClusterFrom(req.Context()) | |
+ if ok && !cluster.Empty() { | |
+ return clusterRoundTripper{cluster: cluster.Path(), delegate: c.delegate}.RoundTrip(req) | |
+ } | |
+ return c.delegate.RoundTrip(req) | |
+} | |
+ | |
+// clusterRoundTripper is static cluster-aware wrapper around http.RoundTripper. | |
+type clusterRoundTripper struct { | |
+ cluster logicalcluster.Path | |
+ delegate http.RoundTripper | |
+} | |
+ | |
+func (c clusterRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | |
+ if !c.cluster.Empty() { | |
+ req = req.Clone(req.Context()) | |
+ req.URL.Path = generatePath(req.URL.Path, c.cluster) | |
+ req.URL.RawPath = generatePath(req.URL.RawPath, c.cluster) | |
+ } | |
+ return c.delegate.RoundTrip(req) | |
+} | |
+ | |
+// apiRegex matches any string that has /api/ or /apis/ in it. | |
+var apiRegex = regexp.MustCompile(`(/api/|/apis/)`) | |
+ | |
+// generatePath formats the request path to target the specified cluster. | |
+func generatePath(originalPath string, clusterPath logicalcluster.Path) string { | |
+ // If the originalPath already has cluster.Path() then the path was already modifed and no change needed | |
+ if strings.Contains(originalPath, clusterPath.RequestPath()) { | |
+ return originalPath | |
+ } | |
+ // If the originalPath has /api/ or /apis/ in it, it might be anywhere in the path, so we use a regex to find and | |
+ // replaces /api/ or /apis/ with $cluster/api/ or $cluster/apis/ | |
+ if apiRegex.MatchString(originalPath) { | |
+ return apiRegex.ReplaceAllString(originalPath, fmt.Sprintf("%s$1", clusterPath.RequestPath())) | |
+ } | |
+ // Otherwise, we're just prepending /clusters/$name | |
+ path := clusterPath.RequestPath() | |
+ // if the original path is relative, add a / separator | |
+ if len(originalPath) > 0 && originalPath[0] != '/' { | |
+ path += "/" | |
+ } | |
+ // finally append the original path | |
+ path += originalPath | |
+ return path | |
+} | |
diff --git a/pkg/kontext/kontext.go b/pkg/kontext/kontext.go | |
new file mode 100644 | |
index 00000000..4de151a7 | |
--- /dev/null | |
+++ b/pkg/kontext/kontext.go | |
@@ -0,0 +1,24 @@ | |
+package kontext | |
+ | |
+import ( | |
+ "context" | |
+ | |
+ "github.com/kcp-dev/logicalcluster/v3" | |
+) | |
+ | |
+type key int | |
+ | |
+const ( | |
+ keyCluster key = iota | |
+) | |
+ | |
+// WithCluster injects a cluster name into a context. | |
+func WithCluster(ctx context.Context, cluster logicalcluster.Name) context.Context { | |
+ return context.WithValue(ctx, keyCluster, cluster) | |
+} | |
+ | |
+// ClusterFrom extracts a cluster name from the context. | |
+func ClusterFrom(ctx context.Context) (logicalcluster.Name, bool) { | |
+ s, ok := ctx.Value(keyCluster).(logicalcluster.Name) | |
+ return s, ok | |
+} | |
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go | |
index 3166f481..6abc0b04 100644 | |
--- a/pkg/manager/manager.go | |
+++ b/pkg/manager/manager.go | |
@@ -126,6 +126,11 @@ type Options struct { | |
// Only use a custom NewCache if you know what you are doing. | |
NewCache cache.NewCacheFunc | |
+ // NewAPIReaderFunc is the function that creates the APIReader client to be | |
+ // used by the manager. If not set this will use the default new APIReader | |
+ // function. | |
+ NewAPIReader client.NewAPIReaderFunc | |
+ | |
// Client is the client.Options that will be used to create the default Client. | |
// By default, the client will use the cache for reads and direct calls for writes. | |
Client client.Options | |
@@ -330,6 +335,7 @@ func New(config *rest.Config, options Options) (Manager, error) { | |
clusterOptions.MapperProvider = options.MapperProvider | |
clusterOptions.Logger = options.Logger | |
clusterOptions.NewCache = options.NewCache | |
+ clusterOptions.NewAPIReader = options.NewAPIReader | |
clusterOptions.NewClient = options.NewClient | |
clusterOptions.Cache = options.Cache | |
clusterOptions.Client = options.Client | |
diff --git a/pkg/reconcile/reconcile.go b/pkg/reconcile/reconcile.go | |
index ee63f681..3e24fadb 100644 | |
--- a/pkg/reconcile/reconcile.go | |
+++ b/pkg/reconcile/reconcile.go | |
@@ -50,6 +50,10 @@ func (r *Result) IsZero() bool { | |
type Request struct { | |
// NamespacedName is the name and namespace of the object to reconcile. | |
types.NamespacedName | |
+ | |
+ // ClusterName can be used for reconciling requests across multiple clusters, | |
+ // to prevent objects with the same name and namespace from conflicting | |
+ ClusterName string | |
} | |
/* |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment