diff --git a/.gitignore b/.gitignore index 2ddc5a8b87..4b09af17f4 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 7cb910fb85..96a95b1bf5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,7 +13,6 @@ linters: - errorlint - exhaustive - ginkgolinter - - goconst - gocritic - gocyclo - gofmt diff --git a/.prow.yaml b/.prow.yaml new file mode 100644 index 0000000000..1f9f775c1c --- /dev/null +++ b/.prow.yaml @@ -0,0 +1,34 @@ +presubmits: + - name: pull-controller-runtime-everything + always_run: true + decorate: true + clone_uri: "ssh://git@github.com/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 0000000000..3f5abef6e3 --- /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 0000000000..e69de29bb2 diff --git a/Makefile b/Makefile index 2361df981d..537e29d49b 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 5a6e313f7b..cd2ca0b4d8 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 0000000000..b0d167a9df --- /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\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 0000000000..760f141055 --- /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 0000000000..5e15b53ba7 --- /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 0000000000..038a1cee41 --- /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 0000000000..e4892949c4 --- /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 0000000000..ed68b8bebc --- /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 0000000000..3161d713a9 --- /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 0000000000..e75e3121a8 --- /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 0000000000..02c5feaa38 --- /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 0000000000..93dd23058c --- /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 0000000000..f41a49313b --- /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 0000000000..f68414eac5 --- /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 0000000000..2b7bd64182 --- /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 0000000000..a3d5bfbbaa --- /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 0000000000..50daebb94b --- /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 0000000000..7b3c863c4e --- /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 0000000000..c9290aa36e --- /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 0000000000..b08c3279c0 --- /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 0000000000..0e33f2f889 --- /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 0000000000..1ee6b39da2 --- /dev/null +++ b/examples/kcp/go.mod @@ -0,0 +1,113 @@ +module github.com/kcp-dev/controller-runtime/examples/kcp + +go 1.23.0 + +toolchain go1.23.7 + +// 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.32.1 + k8s.io/apiextensions-apiserver v0.32.1 + k8s.io/apimachinery v0.32.1 + k8s.io/client-go v0.32.1 + k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/controller-runtime v0.12.3 +) + +require ( + cel.dev/expr v0.18.0 // indirect + 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.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // 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.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.22.0 // 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/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.36.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.16 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect + go.etcd.io/etcd/client/v3 v3.5.16 // 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.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiserver v0.32.1 // indirect + k8s.io/component-base v0.32.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/examples/scratch-env/go.sum b/examples/kcp/go.sum similarity index 58% rename from examples/scratch-env/go.sum rename to examples/kcp/go.sum index 59b01aaac3..6383d15ea1 100644 --- a/examples/scratch-env/go.sum +++ b/examples/kcp/go.sum @@ -1,24 +1,48 @@ +cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +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/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/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.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.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.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +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.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -31,12 +55,17 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr 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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= +github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= 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= @@ -49,10 +78,32 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 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/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.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -90,8 +141,18 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg 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.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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= @@ -101,10 +162,48 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/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.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= +go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= +go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= +go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= +go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= +go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= +go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= +go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= +go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= +go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= +go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= +go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= +go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= +go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= +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= @@ -114,6 +213,10 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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= @@ -154,6 +257,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -163,6 +274,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP 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.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= @@ -172,14 +285,22 @@ k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+ k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= 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.32.1 h1:TW6cswRI/fawoQRFGWLmEceO37rZXupdoRdmO019jCc= +k8s.io/kms v0.32.1/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= diff --git a/examples/kcp/hack/go-install.sh b/examples/kcp/hack/go-install.sh new file mode 100755 index 0000000000..dd323c5666 --- /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 0000000000..e2c3eac332 --- /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 0000000000..151b377222 --- /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 0000000000..481a7c6f01 --- /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 0000000000..03524149f0 --- /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 0000000000..6480643410 --- /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 0000000000..9b1b0384de --- /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 0000000000..8a28ddf0f6 --- /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 8fc685c369..0000000000 --- a/examples/scratch-env/go.mod +++ /dev/null @@ -1,68 +0,0 @@ -module sigs.k8s.io/controller-runtime/examples/scratch-env - -go 1.23.0 - -require ( - github.com/spf13/pflag v1.0.6 - go.uber.org/zap v1.27.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.11 // 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.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/btree v1.1.3 // 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/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/net v0.30.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.1 // indirect - k8s.io/apiextensions-apiserver v0.32.1 // indirect - k8s.io/apimachinery v0.32.1 // indirect - k8s.io/client-go v0.32.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) - -replace sigs.k8s.io/controller-runtime => ../.. diff --git a/examples/scratch-env/main.go b/examples/scratch-env/main.go deleted file mode 100644 index b8305ffed3..0000000000 --- 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 126c195aba..1b90565197 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,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 + github.com/kcp-dev/logicalcluster/v3 v3.0.5 +) + require ( cel.dev/expr v0.18.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect diff --git a/go.sum b/go.sum index 0bd9ded5a2..4a6b19f48a 100644 --- a/go.sum +++ b/go.sum @@ -69,12 +69,18 @@ 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/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 8f14bfdbfc..1a21e18805 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -23,6 +23,7 @@ import ( "net/http" "slices" "sort" + "strings" "time" corev1 "k8s.io/api/core/v1" @@ -170,6 +171,10 @@ type Options struct { // instead of `reconcile.Result{}`. SyncPeriod *time.Duration + // 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. // @@ -436,6 +441,7 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc { NewInformer: opts.NewInformer, }), readerFailOnMissingInformer: opts.ReaderFailOnMissingInformer, + clusterIndexes: strings.HasSuffix(restConfig.Host, "/clusters/*"), } } } @@ -546,6 +552,10 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { if opts.SyncPeriod == nil { opts.SyncPeriod = &defaultSyncPeriod } + + if opts.NewInformer == nil { + opts.NewInformer = toolscache.NewSharedIndexInformer + } return opts, nil } diff --git a/pkg/cache/defaulting_test.go b/pkg/cache/defaulting_test.go index d9d0dcceb3..c064ee7708 100644 --- a/pkg/cache/defaulting_test.go +++ b/pkg/cache/defaulting_test.go @@ -426,6 +426,9 @@ func TestDefaultOpts(t *testing.T) { t.Fatal(err) } + // We cannot reference kcp.NewInformerWithClusterIndexes due to import cycle. + defaulted.NewInformer = 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 091667b7fa..71b94da174 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 81ee960b73..aaf0966f5a 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/kcp_test.go b/pkg/cache/kcp_test.go new file mode 100644 index 0000000000..11c48457e9 --- /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 6d87440174..ec6718e861 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 2d07879520..1247879e5e 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 3b282fc2c5..e07ca275bb 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -20,10 +20,9 @@ import ( "context" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" ) diff --git a/pkg/client/metadata_client.go b/pkg/client/metadata_client.go index d0c6b8e13a..8368035607 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 92afd9a9c2..aff18d8468 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 0d96951780..872f316077 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 181b22a673..317c87ffc0 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 0603f4cde5..266b126409 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -110,6 +110,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. @@ -216,7 +221,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, @@ -266,6 +271,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 64cbe8a4d1..f2755c29d0 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" @@ -53,10 +55,7 @@ func (e *TypedEnqueueRequestForObject[T]) Create(ctx context.Context, evt event. return } - item := reconcile.Request{NamespacedName: types.NamespacedName{ - Name: evt.Object.GetName(), - Namespace: evt.Object.GetNamespace(), - }} + item := request(evt.Object) addToQueueCreate(q, evt, item) } @@ -65,17 +64,11 @@ func (e *TypedEnqueueRequestForObject[T]) Create(ctx context.Context, evt event. func (e *TypedEnqueueRequestForObject[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { switch { case !isNil(evt.ObjectNew): - item := reconcile.Request{NamespacedName: types.NamespacedName{ - Name: evt.ObjectNew.GetName(), - Namespace: evt.ObjectNew.GetNamespace(), - }} + item := request(evt.ObjectNew) addToQueueUpdate(q, evt, item) case !isNil(evt.ObjectOld): - item := reconcile.Request{NamespacedName: types.NamespacedName{ - Name: evt.ObjectOld.GetName(), - Namespace: evt.ObjectOld.GetNamespace(), - }} + item := request(evt.ObjectOld) addToQueueUpdate(q, evt, item) default: @@ -89,10 +82,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. @@ -101,10 +91,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 e8fc8eb46e..e7b97037d6 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 0000000000..85d3900127 --- /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 0000000000..b81a20199e --- /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 0000000000..a4bfce85cf --- /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 0000000000..7add3f9cac --- /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.NewInformer = 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 0000000000..4de151a742 --- /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 92906fe6ca..5103b8c91b 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 ee63f681cc..3e24fadb65 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 } /*