From 7816acbaaf546cd2b8a7088960eb0653469ba1be Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Thu, 2 Jan 2025 17:31:50 +0200 Subject: [PATCH 1/8] init kcp backend example Signed-off-by: Mangirdas Judeikis On-behalf-of: SAP mangirdas.judeikis@sap.com --- contrib/example-backend-kcp/.gitignore | 4 + contrib/example-backend-kcp/Makefile | 123 +++++ contrib/example-backend-kcp/README.md | 69 +++ contrib/example-backend-kcp/backend/config.go | 200 ++++++++ contrib/example-backend-kcp/backend/server.go | 222 +++++++++ .../example-backend-kcp/bootstrap/config.go | 87 ++++ .../bootstrap/config/config/bootstrap.go | 52 ++ .../config/clusterworkspace-kube-bind.yaml | 10 + .../bootstrap/config/core/bootstrap.go | 52 ++ .../config/core/resources/1-namespace.yaml | 6 + .../core/resources/2-clusterrolebinding.yaml | 14 + .../core/resources/3-serviceaccount.yaml | 7 + .../config/core/resources/4-legaycsecret.yaml | 9 + .../config/core/resources/bootstrap.go | 47 ++ .../bootstrap/config/kube-bind | 1 + .../bootstrap/options/options.go | 82 ++++ .../example-backend-kcp/bootstrap/server.go | 80 ++++ .../example-backend-kcp/cmd/backend/main.go | 94 ++++ .../example-backend-kcp/cmd/bootstrap/main.go | 78 +++ .../committer/committer.go | 101 ++++ .../crds/kube-bind.io_apiservicebindings.yaml | 156 ++++++ ...kube-bind.io_apiserviceexportrequests.yaml | 175 +++++++ .../crds/kube-bind.io_apiserviceexports.yaml | 431 +++++++++++++++++ .../kube-bind.io_apiservicenamespaces.yaml | 71 +++ .../crds/kube-bind.io_clusterbindings.yaml | 174 +++++++ .../config/examples/apibinding_accepted.yaml | 36 ++ .../config/examples/crds/mangodb.yaml | 58 +++ .../config/examples/mangodbs_instance.yaml | 6 + .../config/kube-bind/bootstrap.go | 196 ++++++++ .../resources/apiexport-kube-bind.io.yaml | 32 ++ ...chema-apiservicebindings.kube-bind.io.yaml | 151 ++++++ ...apiserviceexportrequests.kube-bind.io.yaml | 172 +++++++ ...schema-apiserviceexports.kube-bind.io.yaml | 428 +++++++++++++++++ ...ema-apiservicenamespaces.kube-bind.io.yaml | 67 +++ ...ceschema-clusterbindings.kube-bind.io.yaml | 171 +++++++ .../config/kube-bind/resources/bootstrap.go | 48 ++ .../clusterbinding_controller.go | 332 +++++++++++++ .../clusterbinding_reconcile.go | 280 +++++++++++ .../serviceexport/serviceexport_controller.go | 289 ++++++++++++ .../serviceexport/serviceexport_reconcile.go | 96 ++++ .../serviceexportrequest_controller.go | 338 +++++++++++++ .../serviceexportrequest_reconcile.go | 144 ++++++ .../servicenamespace_controller.go | 418 +++++++++++++++++ .../servicenamespace_reconcile.go | 125 +++++ contrib/example-backend-kcp/cookie/cookie.go | 52 ++ contrib/example-backend-kcp/cookie/session.go | 113 +++++ .../deploy/01-clusterrole.yaml | 45 ++ .../example-backend-kcp/deploy/bootstrap.go | 35 ++ .../example-backend-kcp/docs/dex-config.yaml | 34 ++ contrib/example-backend-kcp/go.mod | 173 +++++++ contrib/example-backend-kcp/go.sum | 372 +++++++++++++++ .../boilerplate/boilerplate.generatego.txt | 16 + .../hack/boilerplate/boilerplate.go.txt | 16 + .../example-backend-kcp/hack/go-install.sh | 62 +++ .../example-backend-kcp/hack/tools/.gitkeep | 0 .../hack/update-codegen-crds.sh | 50 ++ contrib/example-backend-kcp/http/handler.go | 443 ++++++++++++++++++ contrib/example-backend-kcp/http/oidc.go | 123 +++++ contrib/example-backend-kcp/http/server.go | 90 ++++ .../kubernetes/indexers.go | 40 ++ .../example-backend-kcp/kubernetes/manager.go | 168 +++++++ .../kubernetes/resources/cluster_binding.go | 49 ++ .../kubernetes/resources/kubeconfig.go | 129 +++++ .../kubernetes/resources/namespace.go | 62 +++ .../kubernetes/resources/rbac.go | 50 ++ .../kubernetes/resources/resources.go | 28 ++ .../kubernetes/resources/secret.go | 56 +++ contrib/example-backend-kcp/options/cookie.go | 74 +++ contrib/example-backend-kcp/options/oidc.go | 73 +++ .../example-backend-kcp/options/options.go | 196 ++++++++ contrib/example-backend-kcp/options/serve.go | 68 +++ contrib/example-backend-kcp/template/files.go | 24 + .../template/icon/github-icon.svg | 5 + .../template/icon/google-icon.svg | 16 + .../template/icon/microsoft-icon.svg | 9 + .../example-backend-kcp/template/login.html | 49 ++ .../template/resources.gohtml | 33 ++ .../example-backend-kcp/template/styles.css | 252 ++++++++++ contrib/example-backend-kcp/tools.go | 28 ++ 79 files changed, 8765 insertions(+) create mode 100644 contrib/example-backend-kcp/.gitignore create mode 100644 contrib/example-backend-kcp/Makefile create mode 100644 contrib/example-backend-kcp/README.md create mode 100644 contrib/example-backend-kcp/backend/config.go create mode 100644 contrib/example-backend-kcp/backend/server.go create mode 100644 contrib/example-backend-kcp/bootstrap/config.go create mode 100644 contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go create mode 100644 contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind.yaml create mode 100644 contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go create mode 100644 contrib/example-backend-kcp/bootstrap/config/core/resources/1-namespace.yaml create mode 100644 contrib/example-backend-kcp/bootstrap/config/core/resources/2-clusterrolebinding.yaml create mode 100644 contrib/example-backend-kcp/bootstrap/config/core/resources/3-serviceaccount.yaml create mode 100644 contrib/example-backend-kcp/bootstrap/config/core/resources/4-legaycsecret.yaml create mode 100644 contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go create mode 120000 contrib/example-backend-kcp/bootstrap/config/kube-bind create mode 100644 contrib/example-backend-kcp/bootstrap/options/options.go create mode 100644 contrib/example-backend-kcp/bootstrap/server.go create mode 100644 contrib/example-backend-kcp/cmd/backend/main.go create mode 100644 contrib/example-backend-kcp/cmd/bootstrap/main.go create mode 100644 contrib/example-backend-kcp/committer/committer.go create mode 100644 contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicebindings.yaml create mode 100644 contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexportrequests.yaml create mode 100644 contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexports.yaml create mode 100644 contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicenamespaces.yaml create mode 100644 contrib/example-backend-kcp/config/crds/kube-bind.io_clusterbindings.yaml create mode 100644 contrib/example-backend-kcp/config/examples/apibinding_accepted.yaml create mode 100644 contrib/example-backend-kcp/config/examples/crds/mangodb.yaml create mode 100644 contrib/example-backend-kcp/config/examples/mangodbs_instance.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/bootstrap.go create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/apiexport-kube-bind.io.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicenamespaces.kube-bind.io.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml create mode 100644 contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go create mode 100644 contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go create mode 100644 contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go create mode 100644 contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go create mode 100644 contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go create mode 100644 contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go create mode 100644 contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go create mode 100644 contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go create mode 100644 contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go create mode 100644 contrib/example-backend-kcp/cookie/cookie.go create mode 100644 contrib/example-backend-kcp/cookie/session.go create mode 100644 contrib/example-backend-kcp/deploy/01-clusterrole.yaml create mode 100644 contrib/example-backend-kcp/deploy/bootstrap.go create mode 100644 contrib/example-backend-kcp/docs/dex-config.yaml create mode 100644 contrib/example-backend-kcp/go.mod create mode 100644 contrib/example-backend-kcp/go.sum create mode 100644 contrib/example-backend-kcp/hack/boilerplate/boilerplate.generatego.txt create mode 100644 contrib/example-backend-kcp/hack/boilerplate/boilerplate.go.txt create mode 100755 contrib/example-backend-kcp/hack/go-install.sh create mode 100644 contrib/example-backend-kcp/hack/tools/.gitkeep create mode 100755 contrib/example-backend-kcp/hack/update-codegen-crds.sh create mode 100644 contrib/example-backend-kcp/http/handler.go create mode 100644 contrib/example-backend-kcp/http/oidc.go create mode 100644 contrib/example-backend-kcp/http/server.go create mode 100644 contrib/example-backend-kcp/kubernetes/indexers.go create mode 100644 contrib/example-backend-kcp/kubernetes/manager.go create mode 100644 contrib/example-backend-kcp/kubernetes/resources/cluster_binding.go create mode 100644 contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go create mode 100644 contrib/example-backend-kcp/kubernetes/resources/namespace.go create mode 100644 contrib/example-backend-kcp/kubernetes/resources/rbac.go create mode 100644 contrib/example-backend-kcp/kubernetes/resources/resources.go create mode 100644 contrib/example-backend-kcp/kubernetes/resources/secret.go create mode 100644 contrib/example-backend-kcp/options/cookie.go create mode 100644 contrib/example-backend-kcp/options/oidc.go create mode 100644 contrib/example-backend-kcp/options/options.go create mode 100644 contrib/example-backend-kcp/options/serve.go create mode 100644 contrib/example-backend-kcp/template/files.go create mode 100644 contrib/example-backend-kcp/template/icon/github-icon.svg create mode 100644 contrib/example-backend-kcp/template/icon/google-icon.svg create mode 100644 contrib/example-backend-kcp/template/icon/microsoft-icon.svg create mode 100644 contrib/example-backend-kcp/template/login.html create mode 100644 contrib/example-backend-kcp/template/resources.gohtml create mode 100644 contrib/example-backend-kcp/template/styles.css create mode 100644 contrib/example-backend-kcp/tools.go diff --git a/contrib/example-backend-kcp/.gitignore b/contrib/example-backend-kcp/.gitignore new file mode 100644 index 000000000..c5b86dd66 --- /dev/null +++ b/contrib/example-backend-kcp/.gitignore @@ -0,0 +1,4 @@ +/kube-bind +/kcp +/bin +/hack/tools/bin/ \ No newline at end of file diff --git a/contrib/example-backend-kcp/Makefile b/contrib/example-backend-kcp/Makefile new file mode 100644 index 000000000..0ce4e04fc --- /dev/null +++ b/contrib/example-backend-kcp/Makefile @@ -0,0 +1,123 @@ +# Copyright 2025 The Kube Bind 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. + +TOOLS_DIR=hack/tools +TOOLS_BIN_DIR := $(abspath $(TOOLS_DIR))/bin +export TOOLS_BIN_DIR # so hack scripts can use it + +GO_INSTALL = ./hack/go-install.sh + +KCP_VERSION ?= 0.23.0 +CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen +export CONTROLLER_GEN # so hack scripts can use it + +CODE_GENERATOR_VER := v2.1.0 +CODE_GENERATOR_BIN := code-generator +CODE_GENERATOR := $(TOOLS_BIN_DIR)/$(CODE_GENERATOR_BIN)-$(CODE_GENERATOR_VER) +export CODE_GENERATOR # so hack scripts can use itßß + +KCP_APIGEN_VER := v0.26.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 + +OPENSHIFT_GOIMPORTS_VER := c72f1dc2e3aacfa00aece3391d938c9bc734e791 +OPENSHIFT_GOIMPORTS_BIN := openshift-goimports +OPENSHIFT_GOIMPORTS := $(TOOLS_BIN_DIR)/$(OPENSHIFT_GOIMPORTS_BIN)-$(OPENSHIFT_GOIMPORTS_VER) +export OPENSHIFT_GOIMPORTS # so hack scripts can use it + +export KCP_REPO_DIR=${GOPATH}/src/github.com/kcp-dev/kcp/ +export KCP_KUBECONFIG=${KCP_REPO_DIR}.kcp/admin.kubeconfig + +$(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): # Build controller-gen from tools folder. + cd $(TOOLS_BIN_DIR) && go build -tags=tools -o bin/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen + +$(CODE_GENERATOR): + GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/kcp-dev/code-generator/v2 $(CODE_GENERATOR_BIN) $(CODE_GENERATOR_VER) + +$(OPENSHIFT_GOIMPORTS): + GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/openshift-eng/openshift-goimports $(OPENSHIFT_GOIMPORTS_BIN) $(OPENSHIFT_GOIMPORTS_VER) + + +tools: $(CONTROLLER_GEN) $(KCP_APIGEN_GEN) $ $(CODE_GENERATOR $(OPENSHIFT_GOIMPORTS)) ## Install tools +.PHONY: tools + + +KUBE_MAJOR_VERSION := 1 +KUBE_MINOR_VERSION := $(shell go mod edit -json | jq '.Require[] | select(.Path == "k8s.io/client-go") | .Version' --raw-output | sed "s/v[0-9]*\.\([0-9]*\).*/\1/") +GIT_COMMIT := $(shell git rev-parse --short HEAD || echo 'local') +GIT_DIRTY := $(shell git diff --quiet && echo 'clean' || echo 'dirty') +GIT_VERSION := $(shell go mod edit -json | jq '.Require[] | select(.Path == "k8s.io/client-go") | .Version' --raw-output | sed 's/v0/v1/')+kube-bind-$(shell git describe --tags --match='v*' --abbrev=14 "$(GIT_COMMIT)^{commit}" 2>/dev/null || echo v0.0.0-$(GIT_COMMIT)) + +BUILD_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') +LDFLAGS := \ + -X k8s.io/client-go/pkg/version.gitCommit=${GIT_COMMIT} \ + -X k8s.io/client-go/pkg/version.gitTreeState=${GIT_DIRTY} \ + -X k8s.io/client-go/pkg/version.gitVersion=${GIT_VERSION} \ + -X k8s.io/client-go/pkg/version.gitMajor=${KUBE_MAJOR_VERSION} \ + -X k8s.io/client-go/pkg/version.gitMinor=${KUBE_MINOR_VERSION} \ + -X k8s.io/client-go/pkg/version.buildDate=${BUILD_DATE} \ + \ + -X k8s.io/component-base/version.gitCommit=${GIT_COMMIT} \ + -X k8s.io/component-base/version.gitTreeState=${GIT_DIRTY} \ + -X k8s.io/component-base/version.gitVersion=${GIT_VERSION} \ + -X k8s.io/component-base/version.gitMajor=${KUBE_MAJOR_VERSION} \ + -X k8s.io/component-base/version.gitMinor=${KUBE_MINOR_VERSION} \ + -X k8s.io/component-base/version.buildDate=${BUILD_DATE} +build: WHAT ?= ./cmd/... +build: clean + GOOS=$(OS) GOARCH=$(ARCH) go build $(BUILDFLAGS) -ldflags="$(LDFLAGS)" -o bin/ $(WHAT) +.PHONY: build + +clean: + rm -rf bin/* + +run-dev-init: build + bin/bootstrap init --kcp-kubeconfig=${KCP_KUBECONFIG} + +run-dev: build + bin/backend start \ + -v 4 \ + --tls-cert-file=${KCP_REPO_DIR}/127.0.0.1.pem \ + --tls-key-file=${KCP_REPO_DIR}/127.0.0.1.pem \ + --listen-address=127.0.0.1:6443 \ + --oidc-issuer-client-secret=Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg== \ + --oidc-issuer-client-id=kcp-dev \ + --oidc-issuer-url=https://127.0.0.1:5556/dex \ + --oidc-callback-url=https://127.0.0.1:6443/callback \ + --oidc-authorize-url=https://127.0.0.1:6443/authorize \ + --oidc-ca-file=${KCP_REPO_DIR}/127.0.0.1.pem \ + --pretty-name="CorpAAA.com" \ + --namespace-prefix="kube-bind-" \ + --cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= \ + --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= \ + --workspace-path="root:kube-bind" \ + --apiexport-name="kube-bind.io" \ + --kubeconfig=${KCP_KUBECONFIG} \ + --dev-mode=true + +crds: $(CONTROLLER_GEN) ## Generate crds + ./hack/update-codegen-crds.sh +.PHONY: crds + +codegen: crds + $(MAKE) imports +.PHONY: codegen + +.PHONY: imports +imports: $(OPENSHIFT_GOIMPORTS) + $(OPENSHIFT_GOIMPORTS) -m github.com/kube-bind/kube-bind/contrib/example-backend-kcp \ No newline at end of file diff --git a/contrib/example-backend-kcp/README.md b/contrib/example-backend-kcp/README.md new file mode 100644 index 000000000..1c69a61cc --- /dev/null +++ b/contrib/example-backend-kcp/README.md @@ -0,0 +1,69 @@ +# Kube-Bind for KCP + +This is example backend for KCP that uses [kube-bind](https://github.com/kube-bind/kube-bind) to bind api-exports. + +Values here should match the values used to start kcp with so that the oidc tokens are valid. +We use kcp from [kcp-dev/kcp/contrib/kcp-dex](https://github.com/kcp-dev/kcp/tree/main/contrib/kcp-dex) as an example. + + +## Quickstart + +1. Start kcp instance with dex IDP provider: + +Follow [README](https://github.com/kcp-dev/kcp/blob/main/contrib/kcp-dex/README.md) in kcp repository to have kcp with IDP running. +Just use `docs/dex-config.yaml` instead of one in kcp repistory. kube-bind version contains required callback urls for kube-bind to work. + +Once this is done you should have `dex` running with custom configuration in one terminal, and kcp using this dex as IDP in another. + +2. Start kube-bind backend. + +```bash +make build + +# bootstrap kcp instance with required workspaces and exports: +# `make run-dev-init` is make target for command below. +export KCP_REPO_DIR=${GOPATH}/src/github.com/kcp-dev/kcp/ +export KCP_KUBECONFIG=${KCP_REPO_DIR}/.kcp/admin.kubeconfig +bin/bootstrap init --kcp-kubeconfig=$KCP_KUBECONFIG + +# once it boostrap, start kube-bind backend. Make sure `oidc-issuer-client-secret` matches one, used in dex. +# `make run-dev` is make target for command below. + +bin/backend start \ + -v 4 \ + --tls-cert-file=${KCP_REPO_DIR}127.0.0.1.pem \ + --tls-key-file=../../127.0.0.1.pem \ + --listen-address=127.0.0.1:6443 \ + --oidc-issuer-client-secret=Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg== \ + --oidc-issuer-client-id=kcp-dev \ + --oidc-issuer-url=https://127.0.0.1:5556/dex \ + --oidc-callback-url=https://127.0.0.1:6443/callback \ + --oidc-authorize-url=https://127.0.0.1:6443/authorize \ + --oidc-ca-file=../../127.0.0.1.pem \ + --pretty-name="CorpAAA.com" \ + --namespace-prefix="kube-bind-" \ + --cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= \ + --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= \ + --workspace-path="root:kube-bind" \ + --apiexport-name="kube-bind.io" \ + --kubeconfig=${KCP_KUBECONFIG} \ + --dev-mode=true +``` + +3. Try example `mangodb` as consumer. + +TODO(mjudeikis) + + +# Raodmap & limitations + +Current implementation works same way as existing `example-backend`. It allows export `crds` from worksapce to +external Kubernetes clusters. In a way this example now makes `kube-bind` to be usable as kcp enabled service. +This should change with roadmap being implemented. + +1. Add e2e tests for kcp backend to test basic behaviour. +2. Extend `kcp` and `kube-bind` to be able to export `APIBinding` resources. After this is done, you should be able to +export native `kcp` bindings via `kube-bind`. This will allow more native `kcp` to `k8s` integration. +3. Extend `kcp` APIBindings/PermissionsClaims to be more extendable for `kube-bind` use. +4. Implement kcp native `konnector` agent. After this is done, you will be able to do `kcp` to `kcp` bindings. Currently this is +not possible due to need to run connector agents on the consumer end. \ No newline at end of file diff --git a/contrib/example-backend-kcp/backend/config.go b/contrib/example-backend-kcp/backend/config.go new file mode 100644 index 000000000..cf0c64020 --- /dev/null +++ b/contrib/example-backend-kcp/backend/config.go @@ -0,0 +1,200 @@ +/* +Copyright 2022 The Kube Bind 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 backend + +import ( + "context" + "encoding/base64" + "fmt" + "net/url" + "time" + + apiextensionsclient "github.com/kcp-dev/client-go/apiextensions/client" + apiextensionsinformers "github.com/kcp-dev/client-go/apiextensions/informers" + kubeinformers "github.com/kcp-dev/client-go/informers" + kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes" + apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpclusterclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + "github.com/kcp-dev/logicalcluster/v3" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/options" + bindclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned/cluster" + bindinformers "github.com/kube-bind/kube-bind/sdk/kcp/informers/externalversions" +) + +type Config struct { + Options *options.CompletedOptions + + ClientConfig *rest.Config + BindClient *bindclient.ClusterClientset + KubeClient *kcpkubernetesclientset.ClusterClientset + ApiextensionsClient *apiextensionsclient.ClusterClientset + + KubeInformers kubeinformers.SharedInformerFactory + BindInformers bindinformers.SharedInformerFactory + ApiextensionsInformers apiextensionsinformers.SharedInformerFactory +} + +// NewConfig will create clients and informers for the backend +// Important: This will create clients, pointing to virtual workspace of the APIExport, +// not the actual workspace of the APIExport. +func NewConfig(options *options.CompletedOptions) (*Config, error) { + config := &Config{ + Options: options, + } + + // create clients + rules := clientcmd.NewDefaultClientConfigLoadingRules() + rules.ExplicitPath = options.KubeConfig + var err error + config.ClientConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, nil).ClientConfig() + if err != nil { + return nil, err + } + + cluster := logicalcluster.Name(options.KubeBindWorkspacePath) + + bootstrapConfig := rest.CopyConfig(config.ClientConfig) + bootstrapConfig = rest.AddUserAgent(bootstrapConfig, "kube-bind-kcp-bootstrap") + h, err := url.Parse(bootstrapConfig.Host) + if err != nil { + return nil, err + } + h.Path = "" + bootstrapConfig.Host = h.String() + // Get rest configs for all virtual workspaces. + // TODO(mjudeikis): We will use only first one for now, hence NOT supporting sharding. + restConfigs, err := restConfigForAPIExport(context.Background(), bootstrapConfig, options.KubeBindAPIExportName, cluster.Path()) + if err != nil { + return nil, err + } + restConfig := restConfigs[0] + + restConfig = rest.CopyConfig(restConfig) + restConfig = rest.AddUserAgent(restConfig, "kube-bind-kcp-backend") + + h, err = url.Parse(restConfig.Host) + if err != nil { + return nil, err + } + h.Path = "" + restConfig.Host = h.String() + + // In dev-mode, we use provided kubeconfig to access service account secrets and generate in-memory kubeconfig. + // In production this should be done via service account inside workspace, which has access to secrets. + if options.DevMode { + fmt.Println("Running in development mode. Using kubeconfig to get dev secrets and generate in-memory kubeconfig", options.KubeConfig) + fmt.Println("This will override restConfig object credentials to use ServiceAccount token and CA.") + rest := rest.CopyConfig(config.ClientConfig) + h, err = url.Parse(rest.Host) + if err != nil { + return nil, err + } + h.Path = "" + rest.Host = h.String() + + clientset, err := kcpkubernetesclientset.NewForConfig(rest) + if err != nil { + return nil, err + } + // TODO(mjudeikis): This is hardcoded for now, but should be configurable. + secretName := "kube-bind-controller-secret" + saNamespace := "default" + secret, err := clientset.CoreV1().Cluster(cluster.Path()).Secrets(saNamespace).Get(context.TODO(), secretName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + token := secret.Data["token"] + ca := base64.StdEncoding.EncodeToString(secret.Data["ca.crt"]) + + restConfig.BearerToken = string(token) + restConfig.TLSClientConfig.CAData = []byte(ca) + } + + if config.BindClient, err = bindclient.NewForConfig(config.ClientConfig); err != nil { + return nil, err + } + if config.KubeClient, err = kcpkubernetesclientset.NewForConfig(config.ClientConfig); err != nil { + return nil, err + } + if config.ApiextensionsClient, err = apiextensionsclient.NewForConfig(config.ClientConfig); err != nil { + return nil, err + } + + // construct informer factories + config.KubeInformers = kubeinformers.NewSharedInformerFactory(config.KubeClient, time.Minute*30) + config.BindInformers = bindinformers.NewSharedInformerFactory(config.BindClient, time.Minute*30) + config.ApiextensionsInformers = apiextensionsinformers.NewSharedInformerFactory(config.ApiextensionsClient, time.Minute*30) + + return config, nil +} + +// restConfigForAPIExport returns a *rest.Config properly configured to communicate with the endpoint for the +// APIExport's virtual workspace. +func restConfigForAPIExport(ctx context.Context, rootRestConfig *rest.Config, apiExportName string, cluster logicalcluster.Path) ([]*rest.Config, error) { + logger := klog.FromContext(ctx) + logger.V(2).Info("getting apiexport") + + bootstrapClient, err := kcpclusterclientset.NewForConfig(rootRestConfig) + if err != nil { + return nil, err + } + + var apiExport *apisv1alpha1.APIExport + if apiExportName != "" { + if apiExport, err = bootstrapClient.ApisV1alpha1().APIExports().Cluster(cluster).Get(ctx, apiExportName, metav1.GetOptions{}); err != nil { + return nil, fmt.Errorf("error getting APIExport [%q] in cluster [%s] %w", apiExportName, cluster, err) + } + } else { + logger := klog.FromContext(ctx) + logger.V(2).Info("api-export-name is empty - listing") + exports := &apisv1alpha1.APIExportList{} + if exports, err = bootstrapClient.ApisV1alpha1().APIExports().List(ctx, metav1.ListOptions{}); err != nil { + return nil, fmt.Errorf("error listing APIExports: %w", err) + } + if len(exports.Items) == 0 { + return nil, fmt.Errorf("no APIExport found") + } + if len(exports.Items) > 1 { + return nil, fmt.Errorf("more than one APIExport found") + } + apiExport = &exports.Items[0] + } + + if len(apiExport.Status.VirtualWorkspaces) < 1 { + return nil, fmt.Errorf("APIExport %q status.virtualWorkspaces is empty", apiExportName) + } + + var results []*rest.Config + // TODO(mjudeikis): For sharding support we would need to interact with the APIExportEndpointSlice API + // rather than APIExport. We would then have an URL per shard. For now we just get list of all and move on. + // TODO: WE should use something else as base for kubeconfig, not the rootRestConfig. Maybe dedicated service account? + for _, ws := range apiExport.Status.VirtualWorkspaces { + logger.Info("virtual workspace", "url", ws.URL) + cfg := rest.CopyConfig(rootRestConfig) + cfg.Host = ws.URL + results = append(results, cfg) + } + + return results, nil +} diff --git a/contrib/example-backend-kcp/backend/server.go b/contrib/example-backend-kcp/backend/server.go new file mode 100644 index 000000000..4d141d935 --- /dev/null +++ b/contrib/example-backend-kcp/backend/server.go @@ -0,0 +1,222 @@ +/* +Copyright 2022 The Kube Bind 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 backend + +import ( + "context" + "encoding/base64" + "fmt" + "net" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/controllers/clusterbinding" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/controllers/serviceexport" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/controllers/serviceexportrequest" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/controllers/servicenamespace" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/deploy" + examplehttp "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/http" + examplekube "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" +) + +type Server struct { + Config *Config + + OIDC *examplehttp.OIDCServiceProvider + Kubernetes *examplekube.Manager + WebServer *examplehttp.Server + + Controllers +} + +type Controllers struct { + ClusterBinding *clusterbinding.Controller + ServiceNamespace *servicenamespace.Controller + ServiceExport *serviceexport.Controller + ServiceExportRequest *serviceexportrequest.Controller +} + +func NewServer(ctx context.Context, config *Config) (*Server, error) { + s := &Server{ + Config: config, + } + + var err error + s.WebServer, err = examplehttp.NewServer(config.Options.Serve) + if err != nil { + return nil, fmt.Errorf("error setting up HTTP Server: %w", err) + } + + // setup oidc backend + callback := config.Options.OIDC.CallbackURL + if callback == "" { + callback = fmt.Sprintf("http://%s/callback", s.WebServer.Addr().String()) + } + s.OIDC, err = examplehttp.NewOIDCServiceProvider( + ctx, + config.Options.OIDC.IssuerClientID, + config.Options.OIDC.IssuerClientSecret, + callback, + config.Options.OIDC.IssuerURL, + config.Options.OIDC.OIDCCAFile, + ) + if err != nil { + return nil, fmt.Errorf("error setting up OIDC: %w", err) + } + s.Kubernetes, err = examplekube.NewKubernetesManager( + config.Options.NamespacePrefix, + config.Options.PrettyName, + config.ClientConfig, + config.Options.ExternalAddress, + config.Options.ExternalCA, + config.Options.TLSExternalServerName, + config.KubeInformers.Core().V1().Namespaces(), + config.BindInformers, + ) + if err != nil { + return nil, fmt.Errorf("error setting up Kubernetes Manager: %w", err) + } + + signingKey, err := base64.StdEncoding.DecodeString(config.Options.Cookie.SigningKey) + if err != nil { + return nil, fmt.Errorf("error creating signing key: %w", err) + } + + var encryptionKey []byte + if config.Options.Cookie.EncryptionKey != "" { + var err error + encryptionKey, err = base64.StdEncoding.DecodeString(config.Options.Cookie.EncryptionKey) + if err != nil { + return nil, fmt.Errorf("error creating encryption key: %w", err) + } + } + + handler, err := examplehttp.NewHandler( + s.OIDC, + config.Options.OIDC.AuthorizeURL, + callback, + config.Options.PrettyName, + config.Options.TestingAutoSelect, + signingKey, + encryptionKey, + kubebindv1alpha1.Scope(config.Options.ConsumerScope), + s.Kubernetes, + config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions().Lister(), + ) + if err != nil { + return nil, fmt.Errorf("error setting up HTTP Handler: %w", err) + } + handler.AddRoutes(s.WebServer.Router) + + // construct controllers + s.ClusterBinding, err = clusterbinding.NewController( + config.ClientConfig, + kubebindv1alpha1.Scope(config.Options.ConsumerScope), + config.BindInformers.KubeBind().V1alpha1().ClusterBindings(), + config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), + config.KubeInformers.Rbac().V1().ClusterRoles(), + config.KubeInformers.Rbac().V1().ClusterRoleBindings(), + config.KubeInformers.Rbac().V1().RoleBindings(), + config.KubeInformers.Core().V1().Namespaces(), + ) + if err != nil { + return nil, fmt.Errorf("error setting up ClusterBinding Controller: %v", err) + } + s.ServiceNamespace, err = servicenamespace.NewController( + config.ClientConfig, + kubebindv1alpha1.Scope(config.Options.ConsumerScope), + config.BindInformers.KubeBind().V1alpha1().APIServiceNamespaces(), + config.BindInformers.KubeBind().V1alpha1().ClusterBindings(), + config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), + config.KubeInformers.Core().V1().Namespaces(), + config.KubeInformers.Rbac().V1().Roles(), + config.KubeInformers.Rbac().V1().RoleBindings(), + ) + if err != nil { + return nil, fmt.Errorf("error setting up APIServiceNamespace Controller: %w", err) + } + s.ServiceExport, err = serviceexport.NewController( + config.ClientConfig, + config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), + config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions(), + ) + if err != nil { + return nil, fmt.Errorf("error setting up APIServiceExport Controller: %w", err) + } + s.ServiceExportRequest, err = serviceexportrequest.NewController( + config.ClientConfig, + kubebindv1alpha1.Scope(config.Options.ConsumerScope), + config.BindInformers.KubeBind().V1alpha1().APIServiceExportRequests(), + config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), + config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions(), + ) + if err != nil { + return nil, fmt.Errorf("error setting up ServiceExportRequest Controller: %w", err) + } + + return s, nil +} + +func (s *Server) OptionallyStartInformers(ctx context.Context) { + logger := klog.FromContext(ctx) + + // start informer factories + logger.Info("starting informers") + s.Config.KubeInformers.Start(ctx.Done()) + s.Config.BindInformers.Start(ctx.Done()) + s.Config.ApiextensionsInformers.Start(ctx.Done()) + kubeSynced := s.Config.KubeInformers.WaitForCacheSync(ctx.Done()) + kubeBindSynced := s.Config.BindInformers.WaitForCacheSync(ctx.Done()) + apiextensionsSynced := s.Config.ApiextensionsInformers.WaitForCacheSync(ctx.Done()) + + logger.Info("local informers are synced", + "kubeSynced", fmt.Sprintf("%v", kubeSynced), + "kubeBindSynced", fmt.Sprintf("%v", kubeBindSynced), + "apiextensionsSynced", fmt.Sprintf("%v", apiextensionsSynced), + ) +} + +func (s *Server) Addr() net.Addr { + return s.WebServer.Addr() +} + +func (s *Server) Run(ctx context.Context) error { + dynamicClient, err := dynamic.NewForConfig(s.Config.ClientConfig) + if err != nil { + return err + } + + sets.New[string]() + + if err := deploy.Bootstrap(ctx, s.Config.KubeClient.Discovery(), dynamicClient, sets.New[string]()); err != nil { + return err + } + + // start controllers + go s.Controllers.ServiceExport.Start(ctx, 1) + go s.Controllers.ServiceNamespace.Start(ctx, 1) + go s.Controllers.ClusterBinding.Start(ctx, 1) + go s.Controllers.ServiceExportRequest.Start(ctx, 1) + + go func() { + <-ctx.Done() + }() + return s.WebServer.Start(ctx) +} diff --git a/contrib/example-backend-kcp/bootstrap/config.go b/contrib/example-backend-kcp/bootstrap/config.go new file mode 100644 index 000000000..c0e10ab70 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config.go @@ -0,0 +1,87 @@ +/* +Copyright 2022 The Kube Bind 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 bootstrap + +import ( + "net/url" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + kcpdynamic "github.com/kcp-dev/client-go/dynamic" + kcpclusterclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/options" +) + +type Config struct { + Options *options.CompletedOptions + + ClientConfig *rest.Config + KcpClusterClient kcpclusterclientset.ClusterInterface + ApiextensionsClient kcpapiextensionsclientset.ClusterInterface + DynamicClusterClient kcpdynamic.ClusterInterface +} + +func NewConfig(options *options.CompletedOptions) (*Config, error) { + config := &Config{ + Options: options, + } + + kcpClientConfigOverrides := &clientcmd.ConfigOverrides{ + CurrentContext: options.Context, + } + var err error + config.ClientConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: options.KCPKubeConfig}, + kcpClientConfigOverrides).ClientConfig() + if err != nil { + return nil, err + } + config.ClientConfig = rest.CopyConfig(config.ClientConfig) + config.ClientConfig = rest.AddUserAgent(config.ClientConfig, "kube-bind-kcp-init") + + config.ClientConfig, err = newKCPRestConfig(config.ClientConfig) + if err != nil { + return nil, err + } + + if config.KcpClusterClient, err = kcpclusterclientset.NewForConfig(config.ClientConfig); err != nil { + return nil, err + } + if config.ApiextensionsClient, err = kcpapiextensionsclientset.NewForConfig(config.ClientConfig); err != nil { + return nil, err + } + if config.DynamicClusterClient, err = kcpdynamic.NewForConfig(config.ClientConfig); err != nil { + return nil, err + } + + return config, nil +} + +func newKCPRestConfig(restConfig *rest.Config) (*rest.Config, error) { + clusterConfig := rest.CopyConfig(restConfig) + u, err := url.Parse(restConfig.Host) + if err != nil { + return nil, err + } + u.Path = "" + clusterConfig.Host = u.String() + clusterConfig.UserAgent = rest.DefaultKubernetesUserAgent() + return clusterConfig, nil +} diff --git a/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go new file mode 100644 index 000000000..4cf5294f2 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go @@ -0,0 +1,52 @@ +/* +Copyright 2025 The Kube Bind 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 config + +import ( + "context" + "embed" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + kcpdynamic "github.com/kcp-dev/client-go/dynamic" + confighelpers "github.com/kcp-dev/kcp/config/helpers" + "github.com/kcp-dev/kcp/sdk/apis/core" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + "github.com/kcp-dev/logicalcluster/v3" + + "k8s.io/apimachinery/pkg/util/sets" +) + +//go:embed *.yaml +var fs embed.FS + +// RootClusterName is the workspace to host common faros APIs. +var RootClusterName = logicalcluster.NewPath("root") + +// 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, + kcpClientSet kcpclientset.ClusterInterface, + apiExtensionClusterClient kcpapiextensionsclientset.ClusterInterface, + dynamicClusterClient kcpdynamic.ClusterInterface, + batteriesIncluded sets.Set[string], +) error { + rootDiscoveryClient := apiExtensionClusterClient.Cluster(core.RootCluster.Path()).Discovery() + rootDynamicClient := dynamicClusterClient.Cluster(core.RootCluster.Path()) + return confighelpers.Bootstrap(ctx, rootDiscoveryClient, rootDynamicClient, batteriesIncluded, fs) +} diff --git a/contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind.yaml b/contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind.yaml new file mode 100644 index 000000000..0212e1112 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind.yaml @@ -0,0 +1,10 @@ +apiVersion: tenancy.kcp.io/v1alpha1 +kind: Workspace +metadata: + name: kube-bind + annotations: + bootstrap.kcp.io/create-only: "true" +spec: + type: + name: organization + path: root diff --git a/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go new file mode 100644 index 000000000..03d42bcfc --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go @@ -0,0 +1,52 @@ +/* +Copyright 2025 The Kube Bind 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 core + +import ( + "context" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + kcpdynamic "github.com/kcp-dev/client-go/dynamic" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + "github.com/kcp-dev/logicalcluster/v3" + + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/core/resources" +) + +var ( + // RootClusterName is the workspace to host common APIs. + RootClusterName = logicalcluster.NewPath("root:kube-bind") +) + +// 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, + kcpClientSet kcpclientset.ClusterInterface, + apiExtensionClusterClient kcpapiextensionsclientset.ClusterInterface, + dynamicClusterClient kcpdynamic.ClusterInterface, + batteriesIncluded sets.Set[string], +) error { + computeDiscoveryClient := apiExtensionClusterClient.Cluster(RootClusterName).Discovery() + computeDynamicClient := dynamicClusterClient.Cluster(RootClusterName) + + crdClient := apiExtensionClusterClient.ApiextensionsV1().Cluster(RootClusterName).CustomResourceDefinitions() + return resources.Bootstrap(ctx, kcpClientSet, computeDiscoveryClient, computeDynamicClient, crdClient, batteriesIncluded) +} diff --git a/contrib/example-backend-kcp/bootstrap/config/core/resources/1-namespace.yaml b/contrib/example-backend-kcp/bootstrap/config/core/resources/1-namespace.yaml new file mode 100644 index 000000000..c94b326fe --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/core/resources/1-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: controllers + annotations: + bootstrap.kcp.io/create-only: "true" diff --git a/contrib/example-backend-kcp/bootstrap/config/core/resources/2-clusterrolebinding.yaml b/contrib/example-backend-kcp/bootstrap/config/core/resources/2-clusterrolebinding.yaml new file mode 100644 index 000000000..e811fdaa6 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/core/resources/2-clusterrolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-bind-service-account-binding + annotations: + bootstrap.kcp.io/create-only: "true" +subjects: +- kind: ServiceAccount + name: admin-service-account + namespace: default +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/contrib/example-backend-kcp/bootstrap/config/core/resources/3-serviceaccount.yaml b/contrib/example-backend-kcp/bootstrap/config/core/resources/3-serviceaccount.yaml new file mode 100644 index 000000000..d2153a459 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/core/resources/3-serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kube-bind-controller + namespace: default + annotations: + bootstrap.kcp.io/create-only: "true" \ No newline at end of file diff --git a/contrib/example-backend-kcp/bootstrap/config/core/resources/4-legaycsecret.yaml b/contrib/example-backend-kcp/bootstrap/config/core/resources/4-legaycsecret.yaml new file mode 100644 index 000000000..f2534eb8e --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/core/resources/4-legaycsecret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: kube-bind-controller-secret + namespace: default + annotations: + bootstrap.kcp.io/create-only: "true" + kubernetes.io/service-account.name: kube-bind-controller +type: kubernetes.io/service-account-token \ No newline at end of file diff --git a/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go new file mode 100644 index 000000000..5ff4c78c8 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go @@ -0,0 +1,47 @@ +/* +Copyright 2025 The Kube Bind 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" + + confighelpers "github.com/kcp-dev/kcp/config/helpers" + kcpclientcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" +) + +//go:embed *.yaml +var KubeFS 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, + kcpClient kcpclientcluster.ClusterInterface, + discoveryClient discovery.DiscoveryInterface, + dynamicClient dynamic.Interface, + crdClient apiextensionsv1.CustomResourceDefinitionInterface, + batteriesIncluded sets.Set[string]) error { + // create resources in core cluster + return confighelpers.Bootstrap(ctx, discoveryClient, dynamicClient, batteriesIncluded, KubeFS, confighelpers.ReplaceOption()) +} diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind b/contrib/example-backend-kcp/bootstrap/config/kube-bind new file mode 120000 index 000000000..2df07e91b --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind @@ -0,0 +1 @@ +../../config/kube-bind \ No newline at end of file diff --git a/contrib/example-backend-kcp/bootstrap/options/options.go b/contrib/example-backend-kcp/bootstrap/options/options.go new file mode 100644 index 000000000..fa1ba4661 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/options/options.go @@ -0,0 +1,82 @@ +/* +Copyright 2022 The Kube Bind 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 options + +import ( + "fmt" + + "github.com/spf13/pflag" + + "k8s.io/component-base/logs" + logsv1 "k8s.io/component-base/logs/api/v1" +) + +type Options struct { + Logs *logs.Options + + ExtraOptions +} +type ExtraOptions struct { + KCPKubeConfig string + Context string +} + +type completedOptions struct { + Logs *logs.Options + + ExtraOptions +} + +type CompletedOptions struct { + *completedOptions +} + +func NewOptions() *Options { + // Default to -v=2 + logs := logs.NewOptions() + logs.Verbosity = logsv1.VerbosityLevel(2) + + return &Options{ + Logs: logs, + ExtraOptions: ExtraOptions{}, + } +} + +func (options *Options) AddFlags(fs *pflag.FlagSet) { + logsv1.AddFlags(options.Logs, fs) + + fs.StringVar(&options.KCPKubeConfig, "kcp-kubeconfig", options.KCPKubeConfig, "path to a kcp kubeconfig. Required to bootstrap the server.") + fs.StringVar(&options.Context, "context", options.Context, "Name of the context in the kcp kubeconfig file to use") + +} + +func (options *Options) Complete() (*CompletedOptions, error) { + if options.KCPKubeConfig == "" { + return nil, fmt.Errorf("kcp kubeconfig must be specified") + } + + return &CompletedOptions{ + completedOptions: &completedOptions{ + Logs: options.Logs, + ExtraOptions: options.ExtraOptions, + }, + }, nil +} + +func (options *CompletedOptions) Validate() error { + return nil +} diff --git a/contrib/example-backend-kcp/bootstrap/server.go b/contrib/example-backend-kcp/bootstrap/server.go new file mode 100644 index 000000000..b6d9b193e --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/server.go @@ -0,0 +1,80 @@ +/* +Copyright 2025 The Kube Bind 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 bootstrap + +import ( + "context" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" + + bootstrapconfig "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/config" + bootstrapcore "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/core" + bootstrapkubebind "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/kube-bind" +) + +type Server struct { + Config *Config +} + +func NewServer(ctx context.Context, config *Config) (*Server, error) { + s := &Server{ + Config: config, + } + + return s, nil +} + +func (s *Server) Start(ctx context.Context) error { + fakeBatteries := sets.New("") + logger := klog.FromContext(ctx) + + if err := bootstrapconfig.Bootstrap( + ctx, + s.Config.KcpClusterClient, + s.Config.ApiextensionsClient, + s.Config.DynamicClusterClient, + fakeBatteries, + ); err != nil { + logger.Error(err, "failed to bootstrap initial config workspace") + return nil // don't klog.Fatal. This only happens when context is cancelled. + } + + if err := bootstrapcore.Bootstrap( + ctx, + s.Config.KcpClusterClient, + s.Config.ApiextensionsClient, + s.Config.DynamicClusterClient, + fakeBatteries, + ); err != nil { + logger.Error(err, "failed to bootstrap core workspace") + return nil // don't klog.Fatal. This only happens when context is cancelled. + } + + if err := bootstrapkubebind.Bootstrap( + ctx, + s.Config.KcpClusterClient, + s.Config.ApiextensionsClient, + s.Config.DynamicClusterClient, + fakeBatteries, + ); err != nil { + logger.Error(err, "failed to bootstrap core workspace") + return nil // don't klog.Fatal. This only happens when context is cancelled. + } + + return nil +} diff --git a/contrib/example-backend-kcp/cmd/backend/main.go b/contrib/example-backend-kcp/cmd/backend/main.go new file mode 100644 index 000000000..8e13e8445 --- /dev/null +++ b/contrib/example-backend-kcp/cmd/backend/main.go @@ -0,0 +1,94 @@ +/* +Copyright 2022 The Kube Bind 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" + "fmt" + "os" + "strings" + + "github.com/spf13/pflag" + + genericapiserver "k8s.io/apiserver/pkg/server" + logsv1 "k8s.io/component-base/logs/api/v1" + "k8s.io/component-base/version" + "k8s.io/klog/v2" + + backend "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/backend" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/options" +) + +func main() { + ctx := genericapiserver.SetupSignalContext() + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } +} + +func run(ctx context.Context) error { + defer klog.Flush() + + options := options.NewOptions() + options.AddFlags(pflag.CommandLine) + pflag.Parse() + + // setup logging first + if err := logsv1.ValidateAndApply(options.Logs, nil); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } + ver := version.Get().GitVersion + if i := strings.Index(ver, "bind-"); i != -1 { + ver = ver[i+5:] // example: v1.25.2+kubectl-bind-v0.0.7-52-g8fee0baeaff3aa + } + logger := klog.FromContext(ctx) + logger.Info("Starting example-backend-kcp", "version", ver) + + // create server + completed, err := options.Complete() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } + if err := completed.Validate(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } + + // start server + config, err := backend.NewConfig(completed) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } + server, err := backend.NewServer(ctx, config) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } + server.OptionallyStartInformers(ctx) + if err := server.Run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } + fmt.Printf("Listening on port %s\n", server.Addr()) + + <-ctx.Done() + return nil +} diff --git a/contrib/example-backend-kcp/cmd/bootstrap/main.go b/contrib/example-backend-kcp/cmd/bootstrap/main.go new file mode 100644 index 000000000..b68debb4a --- /dev/null +++ b/contrib/example-backend-kcp/cmd/bootstrap/main.go @@ -0,0 +1,78 @@ +/* +Copyright 2025 The Kube Bind 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" + "fmt" + "os" + + "github.com/spf13/pflag" + + genericapiserver "k8s.io/apiserver/pkg/server" + logsv1 "k8s.io/component-base/logs/api/v1" + "k8s.io/klog/v2" + + bootstrap "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/options" +) + +func main() { + ctx := genericapiserver.SetupSignalContext() + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) // nolint: errcheck + os.Exit(1) + } +} + +func run(ctx context.Context) error { + defer klog.Flush() + + options := options.NewOptions() + options.AddFlags(pflag.CommandLine) + pflag.Parse() + + logger := klog.FromContext(ctx) + logger.Info("bootstrapping api") + + // setup logging first + if err := logsv1.ValidateAndApply(options.Logs, nil); err != nil { + return err + } + + // create init server + completed, err := options.Complete() + if err != nil { + return err + } + if err := completed.Validate(); err != nil { + return err + } + + // start server + config, err := bootstrap.NewConfig(completed) + if err != nil { + return err + } + + server, err := bootstrap.NewServer(ctx, config) + if err != nil { + return err + } + + return server.Start(ctx) +} diff --git a/contrib/example-backend-kcp/committer/committer.go b/contrib/example-backend-kcp/committer/committer.go new file mode 100644 index 000000000..9c1ea03b1 --- /dev/null +++ b/contrib/example-backend-kcp/committer/committer.go @@ -0,0 +1,101 @@ +/* +Copyright 2025 The Kube Bind 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 committer + +import ( + "encoding/json" + "fmt" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/google/go-cmp/cmp" + "github.com/kcp-dev/logicalcluster/v3" + + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Resource is a generic wrapper around resources so we can generate patches. +type Resource[Sp any, St any] struct { + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Sp `json:"spec"` + Status St `json:"status,omitempty"` +} + +func GeneratePatchAndSubResources[Sp any, St any](old, obj *Resource[Sp, St]) ([]byte, []string, error) { + objectMetaChanged := !equality.Semantic.DeepEqual(old.ObjectMeta, obj.ObjectMeta) + specChanged := !equality.Semantic.DeepEqual(old.Spec, obj.Spec) + statusChanged := !equality.Semantic.DeepEqual(old.Status, obj.Status) + + specOrObjectMetaChanged := specChanged || objectMetaChanged + + // Simultaneous updates of spec and status are never allowed. + if specOrObjectMetaChanged && statusChanged { + panic(fmt.Sprintf("programmer error: spec and status changed in same reconcile iteration. diff=%s", cmp.Diff(old, obj))) + } + + if !specOrObjectMetaChanged && !statusChanged { + return nil, nil, nil + } + + // forPatch ensures that only the spec/objectMeta fields will be changed + // or the status field but never both at the same time. + forPatch := func(r *Resource[Sp, St]) *Resource[Sp, St] { + var ret Resource[Sp, St] + if specOrObjectMetaChanged { + ret.ObjectMeta = r.ObjectMeta + ret.Spec = r.Spec + } else { + ret.Status = r.Status + } + return &ret + } + + clusterName := logicalcluster.From(old) + name := old.Name + + oldForPatch := forPatch(old) + // to ensure they appear in the patch as preconditions + oldForPatch.UID = "" + oldForPatch.ResourceVersion = "" + + oldData, err := json.Marshal(oldForPatch) + if err != nil { + return nil, nil, fmt.Errorf("failed to Marshal old data for %s|%s: %w", clusterName, name, err) + } + + newForPatch := forPatch(obj) + // to ensure they appear in the patch as preconditions + newForPatch.UID = old.UID + newForPatch.ResourceVersion = old.ResourceVersion + + newData, err := json.Marshal(newForPatch) + if err != nil { + return nil, nil, fmt.Errorf("failed to Marshal new data for %s|%s: %w", clusterName, name, err) + } + + patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) + if err != nil { + return nil, nil, fmt.Errorf("failed to create patch for %s|%s: %w", clusterName, name, err) + } + + var subresources []string + if statusChanged { + subresources = []string{"status"} + } + + return patchBytes, subresources, nil +} diff --git a/contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicebindings.yaml b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicebindings.yaml new file mode 100644 index 000000000..a3e5a6591 --- /dev/null +++ b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicebindings.yaml @@ -0,0 +1,156 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: apiservicebindings.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceBinding + listKind: APIServiceBindingList + plural: apiservicebindings + shortNames: + - sb + singular: apiservicebinding + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.providerPrettyName + name: Provider + type: string + - jsonPath: .metadata.annotations.kube-bind\.io/resources + name: Resources + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + APIServiceBinding binds an API service represented by a APIServiceExport + in a service provider cluster into a consumer cluster. This object lives in + the consumer cluster. + 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: |- + spec specifies how an API service from a service provider should be bound in the + local consumer cluster. + properties: + kubeconfigSecretRef: + description: kubeconfigSecretName is the secret ref that contains + the kubeconfig of the service cluster. + properties: + key: + description: The key of the secret to select from. Must be "kubeconfig". + enum: + - kubeconfig + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + namespace: + description: Namespace of the referent. + minLength: 1 + type: string + required: + - key + - name + - namespace + type: object + x-kubernetes-validations: + - message: kubeconfigSecretRef is immutable + rule: self == oldSelf + required: + - kubeconfigSecretRef + type: object + status: + description: status contains reconciliation information for a service + binding. + properties: + conditions: + description: conditions is a list of conditions that apply to the + APIServiceBinding. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + providerPrettyName: + description: |- + providerPrettyName is the pretty name of the service provider cluster. This + can be shared among different APIServiceBindings. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexportrequests.yaml b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexportrequests.yaml new file mode 100644 index 000000000..a877158ad --- /dev/null +++ b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexportrequests.yaml @@ -0,0 +1,175 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: apiserviceexportrequests.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceExportRequest + listKind: APIServiceExportRequestList + plural: apiserviceexportrequests + singular: apiserviceexportrequest + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + APIServiceExportRequest is represents a request session of kubectl-bind-apiservice. + + The service provider can prune these objects after some time. + 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: |- + spec specifies how an API service from a service provider should be bound in the + local consumer cluster. + properties: + parameters: + description: |- + parameters holds service provider specific parameters for this binding + request. + type: object + x-kubernetes-preserve-unknown-fields: true + x-kubernetes-validations: + - message: parameters are immutable + rule: self == oldSelf + resources: + description: resources is a list of resources that should be exported. + items: + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + versions: + description: |- + versions is a list of versions that should be exported. If this is empty + a sensible default is chosen by the service provider. + items: + type: string + type: array + required: + - resource + type: object + minItems: 1 + type: array + x-kubernetes-validations: + - message: resources are immutable + rule: self == oldSelf + required: + - resources + type: object + status: + default: {} + description: status contains reconciliation information for a service + binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + phase: + default: Pending + description: |- + phase is the current phase of the binding request. It starts in Pending + and transitions to Succeeded or Failed. See the condition for detailed + information. + enum: + - Pending + - Failed + - Succeeded + type: string + terminalMessage: + description: |- + terminalMessage is a human readable message that describes the reason + for the current phase. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexports.yaml b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexports.yaml new file mode 100644 index 000000000..f90c1a0c3 --- /dev/null +++ b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiserviceexports.yaml @@ -0,0 +1,431 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: apiserviceexports.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceExport + listKind: APIServiceExportList + plural: apiserviceexports + singular: apiserviceexport + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Established")].status + name: Established + priority: 5 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + APIServiceExport specifies the resource to be exported. It is mostly a CRD: + - the spec is a CRD spec, but without webhooks + - the status reflects that on the consumer cluster + 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: spec specifies the resource. + properties: + clusterScopedIsolation: + description: |- + ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. + It can be "Prefixed", "Namespaced", or "None". + enum: + - Prefixed + - Namespaced + - None + type: string + group: + description: "group is the API group of the defined custom resource. + Empty string means the\ncore API group. \tThe resources are served + under `/apis//...` or `/api` for the core group." + type: string + informerScope: + description: |- + informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. + + Cluster: The konnector has permission to watch all namespaces at once and cluster-scoped resources. + This is more efficient than watching each namespace individually. + Namespaced: The konnector has permission to watch only single namespaces. + This is more resource intensive. And it means cluster-scoped resources cannot be exported. + enum: + - Cluster + - Namespaced + type: string + x-kubernetes-validations: + - message: informerScope is immutable + rule: self == oldSelf + names: + description: names specify the resource and kind names for the custom + resource. + properties: + categories: + description: |- + categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). + This is published in API discovery documents, and used by clients to support invocations like + `kubectl get all`. + items: + type: string + type: array + x-kubernetes-list-type: atomic + kind: + description: |- + kind is the serialized kind of the resource. It is normally CamelCase and singular. + Custom resource instances will use this value as the `kind` attribute in API calls. + type: string + listKind: + description: listKind is the serialized kind of the list for this + resource. Defaults to "`kind`List". + type: string + plural: + description: |- + plural is the plural name of the resource to serve. + The custom resources are served under `/apis///.../`. + Must match the name of the CustomResourceDefinition (in the form `.`). + Must be all lowercase. + type: string + shortNames: + description: |- + shortNames are short names for the resource, exposed in API discovery documents, + and used by clients to support invocations like `kubectl get `. + It must be all lowercase. + items: + type: string + type: array + x-kubernetes-list-type: atomic + singular: + description: singular is the singular name of the resource. It + must be all lowercase. Defaults to lowercased `kind`. + type: string + required: + - kind + - plural + type: object + scope: + description: |- + scope indicates whether the defined custom resource is cluster- or namespace-scoped. + Allowed values are `Cluster` and `Namespaced`. + enum: + - Cluster + - Namespaced + type: string + versions: + description: |- + versions is the API version of the defined custom resource. + + Note: the OpenAPI v3 schemas must be equal for all versions until CEL + version migration is supported. + items: + description: APIServiceExportVersion describes one API version of + a resource. + properties: + additionalPrinterColumns: + description: |- + additionalPrinterColumns specifies additional columns returned in Table output. + See https://kubernetes.io/docs/reference/using-api/api-concepts/#receiving-resources-as-tables for details. + If no columns are specified, a single column displaying the age of the custom resource is used. + items: + description: CustomResourceColumnDefinition specifies a column + for server side printing. + properties: + description: + description: description is a human readable description + of this column. + type: string + format: + description: |- + format is an optional OpenAPI type definition for this column. The 'name' format is applied + to the primary identifier column to assist in clients identifying column is the resource name. + See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. + type: string + jsonPath: + description: |- + jsonPath is a simple JSON path (i.e. with array notation) which is evaluated against + each custom resource to produce the value for this column. + type: string + name: + description: name is a human readable name for the column. + type: string + priority: + description: |- + priority is an integer defining the relative importance of this column compared to others. Lower + numbers are considered higher priority. Columns that may be omitted in limited space scenarios + should be given a priority greater than 0. + format: int32 + type: integer + type: + description: |- + type is an OpenAPI type definition for this column. + See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. + type: string + required: + - jsonPath + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + deprecated: + description: |- + deprecated indicates this version of the custom resource API is deprecated. + When set to true, API requests to this version receive a warning header in the server response. + Defaults to false. + type: boolean + deprecationWarning: + description: |- + deprecationWarning overrides the default warning returned to API clients. + May only be set when `deprecated` is true. + The default warning indicates this version is deprecated and recommends use + of the newest served version of equal or greater stability, if one exists. + type: string + name: + description: |- + name is the version name, e.g. β€œv1”, β€œv2beta1”, etc. + The custom resources are served under this version at `/apis///...` if `served` is true. + minLength: 1 + pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ + type: string + schema: + description: |- + schema describes the structural schema used for validation, pruning, and defaulting + of this version of the custom resource. + properties: + openAPIV3Schema: + description: openAPIV3Schema is the OpenAPI v3 schema to + use for validation and pruning. + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - openAPIV3Schema + type: object + served: + default: true + description: served is a flag enabling/disabling this version + from being served via REST APIs + type: boolean + storage: + description: |- + storage indicates this version should be used when persisting custom resources to storage. + There must be exactly one version with storage=true. + type: boolean + subresources: + description: subresources specify what subresources this version + of the defined custom resource have. + properties: + scale: + description: scale indicates the custom resource should + serve a `/scale` subresource that returns an `autoscaling/v1` + Scale object. + properties: + labelSelectorPath: + description: |- + labelSelectorPath defines the JSON path inside of a custom resource that corresponds to Scale `status.selector`. + Only JSON paths without the array notation are allowed. + Must be a JSON Path under `.status` or `.spec`. + Must be set to work with HorizontalPodAutoscaler. + The field pointed by this JSON path must be a string field (not a complex selector struct) + which contains a serialized label selector in string form. + More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource + If there is no value under the given path in the custom resource, the `status.selector` value in the `/scale` + subresource will default to the empty string. + type: string + specReplicasPath: + description: |- + specReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `spec.replicas`. + Only JSON paths without the array notation are allowed. + Must be a JSON Path under `.spec`. + If there is no value under the given path in the custom resource, the `/scale` subresource will return an error on GET. + type: string + statusReplicasPath: + description: |- + statusReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `status.replicas`. + Only JSON paths without the array notation are allowed. + Must be a JSON Path under `.status`. + If there is no value under the given path in the custom resource, the `status.replicas` value in the `/scale` subresource + will default to 0. + type: string + required: + - specReplicasPath + - statusReplicasPath + type: object + status: + description: |- + status indicates the custom resource should serve a `/status` subresource. + When enabled: + 1. requests to the custom resource primary endpoint ignore changes to the `status` stanza of the object. + 2. requests to the custom resource `/status` subresource ignore changes to anything other than the `status` stanza of the object. + type: object + type: object + required: + - name + - schema + - served + - storage + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - group + - informerScope + - names + - scope + - versions + type: object + x-kubernetes-validations: + - message: informerScope must be Cluster for cluster-scoped resources + rule: self.scope == "Namespaced" || self.informerScope == "Cluster" + - message: clusterScopedIsolation must be defined for cluster-scoped resources + rule: self.scope == "Namespaced" || has(self.clusterScopedIsolation) + - message: clusterScopedIsolation is not relevant for namespaced resources + rule: self.scope == "Cluster" || !has(self.clusterScopedIsolation) + status: + description: status contains reconciliation information for the resource. + properties: + acceptedNames: + description: |- + acceptedNames are the names that are actually being used to serve discovery. + They may be different than the names in spec. + properties: + categories: + description: |- + categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). + This is published in API discovery documents, and used by clients to support invocations like + `kubectl get all`. + items: + type: string + type: array + x-kubernetes-list-type: atomic + kind: + description: |- + kind is the serialized kind of the resource. It is normally CamelCase and singular. + Custom resource instances will use this value as the `kind` attribute in API calls. + type: string + listKind: + description: listKind is the serialized kind of the list for this + resource. Defaults to "`kind`List". + type: string + plural: + description: |- + plural is the plural name of the resource to serve. + The custom resources are served under `/apis///.../`. + Must match the name of the CustomResourceDefinition (in the form `.`). + Must be all lowercase. + type: string + shortNames: + description: |- + shortNames are short names for the resource, exposed in API discovery documents, + and used by clients to support invocations like `kubectl get `. + It must be all lowercase. + items: + type: string + type: array + x-kubernetes-list-type: atomic + singular: + description: singular is the singular name of the resource. It + must be all lowercase. Defaults to lowercased `kind`. + type: string + required: + - kind + - plural + type: object + conditions: + description: |- + conditions is a list of conditions that apply to the APIServiceExport. It is + updated by the konnector on the consumer cluster. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + storedVersions: + description: |- + storedVersions lists all versions of CustomResources that were ever persisted. Tracking these + versions allows a migration path for stored versions in etcd. The field is mutable + so a migration controller can finish a migration to another version (ensuring + no old objects are left in storage), and then remove the rest of the + versions from this list. + Versions may not be removed from `spec.versions` while they exist in this list. + items: + type: string + type: array + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: informerScope is immutable + rule: self.metadata.name == self.spec.names.plural+"."+self.spec.group + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicenamespaces.yaml b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicenamespaces.yaml new file mode 100644 index 000000000..69d9e9180 --- /dev/null +++ b/contrib/example-backend-kcp/config/crds/kube-bind.io_apiservicenamespaces.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: apiservicenamespaces.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceNamespace + listKind: APIServiceNamespaceList + plural: apiservicenamespaces + singular: apiservicenamespace + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.namespace + name: Namespace + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + APIServiceNamespace defines how consumer namespaces map to service namespaces. + These objects are created by the konnector, and a service namespace is then + created by the service provider. + + The name of the APIServiceNamespace equals the namespace name in the consumer + cluster. + 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: spec specifies a service namespace. + type: object + status: + description: status contains reconciliation information for a service + namespace + properties: + namespace: + description: |- + namespace is the service provider namespace name that will be bound to the + consumer namespace named like this object. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/crds/kube-bind.io_clusterbindings.yaml b/contrib/example-backend-kcp/config/crds/kube-bind.io_clusterbindings.yaml new file mode 100644 index 000000000..b6a6fbb0c --- /dev/null +++ b/contrib/example-backend-kcp/config/crds/kube-bind.io_clusterbindings.yaml @@ -0,0 +1,174 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: clusterbindings.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: ClusterBinding + listKind: ClusterBindingList + plural: clusterbindings + singular: clusterbinding + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.konnectorVersion + name: Konnector Version + type: string + - jsonPath: .status.lastHeartbeatTime + name: Last Heartbeat + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ClusterBinding represents a bound consumer class. It lives in a service provider cluster + and is a singleton named "cluster". + 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: spec represents the data in the newly created ClusterBinding. + properties: + kubeconfigSecretRef: + description: kubeconfigSecretName is the secret ref that contains + the kubeconfig of the service cluster. + properties: + key: + description: The key of the secret to select from. Must be "kubeconfig". + enum: + - kubeconfig + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-validations: + - message: kubeconfigSecretRef is immutable + rule: self == oldSelf + providerPrettyName: + description: |- + providerPrettyName is the pretty name of the service provider cluster. This + can be shared among different ServiceBindings. + minLength: 1 + type: string + serviceProviderSpec: + description: |- + serviceProviderSpec contains all the data and information about the service which has been bound to the service + binding request. The service providers decide what they need and what to configure based on what then include in + this field, such as service region, type, tiers, etc... + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - kubeconfigSecretRef + - providerPrettyName + type: object + status: + description: status contains reconciliation information for the service + binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + heartbeatInterval: + description: |- + heartbeatInterval is the maximal interval between heartbeats that the + konnector promises to send. The service provider can assume that the + konnector is not unhealthy if it does not receive a heartbeat within + this time. + type: string + konnectorVersion: + description: |- + konnectorVersion is the version of the konnector that is running on the + consumer cluster. + type: string + lastHeartbeatTime: + description: lastHeartbeatTime is the last time the konnector updated + the status. + format: date-time + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: cluster binding name should be cluster + rule: self.metadata.name == "cluster" + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/examples/apibinding_accepted.yaml b/contrib/example-backend-kcp/config/examples/apibinding_accepted.yaml new file mode 100644 index 000000000..2b67ada6e --- /dev/null +++ b/contrib/example-backend-kcp/config/examples/apibinding_accepted.yaml @@ -0,0 +1,36 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIBinding +metadata: + generation: 4 + name: kube-bind.io +spec: + permissionClaims: + - all: true + group: rbac.authorization.k8s.io + resource: clusterrolebindings + state: Accepted + - all: true + group: rbac.authorization.k8s.io + resource: clusterroles + state: Accepted + - all: true + resource: serviceaccounts + state: Accepted + - all: true + resource: configmaps + state: Accepted + - all: true + resource: secrets + state: Accepted + - all: true + group: apis.kcp.io + resource: apiexports + state: Accepted + - all: true + group: apiextensions.k8s.io + resource: customresourcedefinitions + state: Accepted + reference: + export: + name: kube-bind.io + path: root:kube-bind diff --git a/contrib/example-backend-kcp/config/examples/crds/mangodb.yaml b/contrib/example-backend-kcp/config/examples/crds/mangodb.yaml new file mode 100644 index 000000000..0ec795060 --- /dev/null +++ b/contrib/example-backend-kcp/config/examples/crds/mangodb.yaml @@ -0,0 +1,58 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: mangodbs.mangodb.com + labels: + kube-bind.io/exported: "true" +spec: + group: mangodb.com + names: + kind: MangoDB + listKind: MangoDBList + plural: mangodbs + singular: mangodb + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + tier: + type: string + enum: + - Dedicated + - Shared + default: Shared + region: + type: string + default: us-east-1 + minLength: 1 + backup: + type: boolean + default: false + tokenSecret: + type: string + minLength: 1 + required: + - tokenSecret + status: + type: object + properties: + phase: + type: string + enum: + - Pending + - Running + - Succeeded + - Failed + - Unknown + required: + - spec diff --git a/contrib/example-backend-kcp/config/examples/mangodbs_instance.yaml b/contrib/example-backend-kcp/config/examples/mangodbs_instance.yaml new file mode 100644 index 000000000..23c9b3ca7 --- /dev/null +++ b/contrib/example-backend-kcp/config/examples/mangodbs_instance.yaml @@ -0,0 +1,6 @@ +apiVersion: mangodb.com/v1alpha1 +kind: MangoDB +metadata: + name: my-db +spec: + tokenSecret: not-sure-if-this-is-used diff --git a/contrib/example-backend-kcp/config/kube-bind/bootstrap.go b/contrib/example-backend-kcp/config/kube-bind/bootstrap.go new file mode 100644 index 000000000..5ad4da84e --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/bootstrap.go @@ -0,0 +1,196 @@ +/* +Copyright 2025 The Kube Bind 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 kubebind + +import ( + "context" + "time" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + kcpdynamic "github.com/kcp-dev/client-go/dynamic" + apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpclient "github.com/kcp-dev/kcp/sdk/client/clientset/versioned" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + "github.com/kcp-dev/logicalcluster/v3" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/kube-bind/resources" +) + +var ( + // RootClusterName is the workspace to host common APIs. + RootClusterName = logicalcluster.NewPath("root:kube-bind") +) + +// 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, + kcpClientSet kcpclientset.ClusterInterface, + apiExtensionClusterClient kcpapiextensionsclientset.ClusterInterface, + dynamicClusterClient kcpdynamic.ClusterInterface, + batteriesIncluded sets.Set[string], +) error { + computeDiscoveryClient := apiExtensionClusterClient.Cluster(RootClusterName).Discovery() + computeDynamicClient := dynamicClusterClient.Cluster(RootClusterName) + + crdClient := apiExtensionClusterClient.ApiextensionsV1().Cluster(RootClusterName).CustomResourceDefinitions() + kcpClient := kcpClientSet.Cluster(RootClusterName) + + err := resources.Bootstrap(ctx, kcpClientSet, computeDiscoveryClient, computeDynamicClient, crdClient, batteriesIncluded) + if err != nil { + return err + } + + // create recursive apibinding so we can start controllers. + // this is a temporary solution until we have a better way to bootstrap controllers. + return bindAPIExport(ctx, kcpClient, "kube-bind.io", RootClusterName) +} + +func bindAPIExport(ctx context.Context, kcpClient kcpclient.Interface, exportName string, clusterPath logicalcluster.Path) error { + logger := klog.FromContext(ctx) + + binding := &apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: exportName, + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.BindingReference{ + Export: &apisv1alpha1.ExportBindingReference{ + Path: clusterPath.String(), + Name: exportName, + }, + }, + }, + } + + binding.Spec.PermissionClaims = []apisv1alpha1.AcceptablePermissionClaim{ + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "rbac.authorization.k8s.io", + Resource: "clusterrolebindings", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "rbac.authorization.k8s.io", + Resource: "clusterroles", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "", + Resource: "serviceaccounts", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "", + Resource: "configmaps", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "", + Resource: "secrets", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: apisv1alpha1.SchemeGroupVersion.Group, + Resource: "apiexports", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + } + + _, err := kcpClient.ApisV1alpha1().APIBindings().Create(ctx, binding, metav1.CreateOptions{}) + if err == nil { + return nil + } + if !apierrors.IsAlreadyExists(err) { + return err + } + + if err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) { + existing, err := kcpClient.ApisV1alpha1().APIBindings().Get(ctx, exportName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "error getting APIBinding", "name", exportName) + // Always keep trying. Don't ever return an error out of this function. + return false, nil + } + + logger.V(2).Info("Updating API binding") + existing.Spec = binding.Spec + + _, err = kcpClient.ApisV1alpha1().APIBindings().Update(ctx, existing, metav1.UpdateOptions{}) + if err == nil { + return true, nil + } + if apierrors.IsConflict(err) { + logger.V(2).Info("API binding update conflict, retrying") + return false, nil + } + + logger.Error(err, "error updating APIBinding") + // Always keep trying. Don't ever return an error out of this function. + return false, nil + }); err != nil { + return err + } + + return nil +} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/apiexport-kube-bind.io.yaml b/contrib/example-backend-kcp/config/kube-bind/resources/apiexport-kube-bind.io.yaml new file mode 100644 index 000000000..fce6aba82 --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/apiexport-kube-bind.io.yaml @@ -0,0 +1,32 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIExport +metadata: + creationTimestamp: null + name: kube-bind.io +spec: + latestResourceSchemas: + - v241216-832011705.apiservicebindings.kube-bind.io + - v241216-832011705.apiserviceexportrequests.kube-bind.io + - v241216-832011705.apiserviceexports.kube-bind.io + - v241216-832011705.apiservicenamespaces.kube-bind.io + - v241216-832011705.clusterbindings.kube-bind.io + permissionClaims: + - all: true + group: rbac.authorization.k8s.io + resource: clusterrolebindings + - all: true + resource: serviceaccounts + - all: true + resource: configmaps + - all: true + resource: secrets + - all: true + group: rbac.authorization.k8s.io + resource: clusterroles + - all: true + group: apiextensions.k8s.io + resource: customresourcedefinitions + - all: true + group: apis.kcp.io + resource: apiexports +status: {} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml new file mode 100644 index 000000000..5fb176faa --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml @@ -0,0 +1,151 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v241216-832011705.apiservicebindings.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceBinding + listKind: APIServiceBindingList + plural: apiservicebindings + shortNames: + - sb + singular: apiservicebinding + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.providerPrettyName + name: Provider + type: string + - jsonPath: .metadata.annotations.kube-bind\.io/resources + name: Resources + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + description: |- + APIServiceBinding binds an API service represented by a APIServiceExport + in a service provider cluster into a consumer cluster. This object lives in + the consumer cluster. + 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: |- + spec specifies how an API service from a service provider should be bound in the + local consumer cluster. + properties: + kubeconfigSecretRef: + description: kubeconfigSecretName is the secret ref that contains the + kubeconfig of the service cluster. + properties: + key: + description: The key of the secret to select from. Must be "kubeconfig". + enum: + - kubeconfig + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + namespace: + description: Namespace of the referent. + minLength: 1 + type: string + required: + - key + - name + - namespace + type: object + x-kubernetes-validations: + - message: kubeconfigSecretRef is immutable + rule: self == oldSelf + required: + - kubeconfigSecretRef + type: object + status: + description: status contains reconciliation information for a service binding. + properties: + conditions: + description: conditions is a list of conditions that apply to the APIServiceBinding. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + providerPrettyName: + description: |- + providerPrettyName is the pretty name of the service provider cluster. This + can be shared among different APIServiceBindings. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml new file mode 100644 index 000000000..ff1b4142e --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml @@ -0,0 +1,172 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v241216-832011705.apiserviceexportrequests.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceExportRequest + listKind: APIServiceExportRequestList + plural: apiserviceexportrequests + singular: apiserviceexportrequest + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + description: |- + APIServiceExportRequest is represents a request session of kubectl-bind-apiservice. + + The service provider can prune these objects after some time. + 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: |- + spec specifies how an API service from a service provider should be bound in the + local consumer cluster. + properties: + parameters: + description: |- + parameters holds service provider specific parameters for this binding + request. + type: object + x-kubernetes-preserve-unknown-fields: true + x-kubernetes-validations: + - message: parameters are immutable + rule: self == oldSelf + resources: + description: resources is a list of resources that should be exported. + items: + properties: + group: + default: "" + description: |- + group is the name of an API group. + For core groups this is the empty string '""'. + pattern: ^(|[a-z0-9]([-a-z0-9]*[a-z0-9](\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?)$ + type: string + resource: + description: |- + resource is the name of the resource. + Note: it is worth noting that you can not ask for permissions for resource provided by a CRD + not provided by an service binding export. + pattern: ^[a-z][-a-z0-9]*[a-z0-9]$ + type: string + versions: + description: |- + versions is a list of versions that should be exported. If this is empty + a sensible default is chosen by the service provider. + items: + type: string + type: array + required: + - resource + type: object + minItems: 1 + type: array + x-kubernetes-validations: + - message: resources are immutable + rule: self == oldSelf + required: + - resources + type: object + status: + default: {} + description: status contains reconciliation information for a service binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + phase: + default: Pending + description: |- + phase is the current phase of the binding request. It starts in Pending + and transitions to Succeeded or Failed. See the condition for detailed + information. + enum: + - Pending + - Failed + - Succeeded + type: string + terminalMessage: + description: |- + terminalMessage is a human readable message that describes the reason + for the current phase. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml new file mode 100644 index 000000000..bf3f64274 --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml @@ -0,0 +1,428 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v241216-832011705.apiserviceexports.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceExport + listKind: APIServiceExportList + plural: apiserviceexports + singular: apiserviceexport + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Established")].status + name: Established + priority: 5 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + description: |- + APIServiceExport specifies the resource to be exported. It is mostly a CRD: + - the spec is a CRD spec, but without webhooks + - the status reflects that on the consumer cluster + 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: spec specifies the resource. + properties: + clusterScopedIsolation: + description: |- + ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. + It can be "Prefixed", "Namespaced", or "None". + enum: + - Prefixed + - Namespaced + - None + type: string + group: + description: "group is the API group of the defined custom resource. + Empty string means the\ncore API group. \tThe resources are served + under `/apis//...` or `/api` for the core group." + type: string + informerScope: + description: |- + informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. + + Cluster: The konnector has permission to watch all namespaces at once and cluster-scoped resources. + This is more efficient than watching each namespace individually. + Namespaced: The konnector has permission to watch only single namespaces. + This is more resource intensive. And it means cluster-scoped resources cannot be exported. + enum: + - Cluster + - Namespaced + type: string + x-kubernetes-validations: + - message: informerScope is immutable + rule: self == oldSelf + names: + description: names specify the resource and kind names for the custom + resource. + properties: + categories: + description: |- + categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). + This is published in API discovery documents, and used by clients to support invocations like + `kubectl get all`. + items: + type: string + type: array + x-kubernetes-list-type: atomic + kind: + description: |- + kind is the serialized kind of the resource. It is normally CamelCase and singular. + Custom resource instances will use this value as the `kind` attribute in API calls. + type: string + listKind: + description: listKind is the serialized kind of the list for this + resource. Defaults to "`kind`List". + type: string + plural: + description: |- + plural is the plural name of the resource to serve. + The custom resources are served under `/apis///.../`. + Must match the name of the CustomResourceDefinition (in the form `.`). + Must be all lowercase. + type: string + shortNames: + description: |- + shortNames are short names for the resource, exposed in API discovery documents, + and used by clients to support invocations like `kubectl get `. + It must be all lowercase. + items: + type: string + type: array + x-kubernetes-list-type: atomic + singular: + description: singular is the singular name of the resource. It must + be all lowercase. Defaults to lowercased `kind`. + type: string + required: + - kind + - plural + type: object + scope: + description: |- + scope indicates whether the defined custom resource is cluster- or namespace-scoped. + Allowed values are `Cluster` and `Namespaced`. + enum: + - Cluster + - Namespaced + type: string + versions: + description: |- + versions is the API version of the defined custom resource. + + Note: the OpenAPI v3 schemas must be equal for all versions until CEL + version migration is supported. + items: + description: APIServiceExportVersion describes one API version of + a resource. + properties: + additionalPrinterColumns: + description: |- + additionalPrinterColumns specifies additional columns returned in Table output. + See https://kubernetes.io/docs/reference/using-api/api-concepts/#receiving-resources-as-tables for details. + If no columns are specified, a single column displaying the age of the custom resource is used. + items: + description: CustomResourceColumnDefinition specifies a column + for server side printing. + properties: + description: + description: description is a human readable description + of this column. + type: string + format: + description: |- + format is an optional OpenAPI type definition for this column. The 'name' format is applied + to the primary identifier column to assist in clients identifying column is the resource name. + See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. + type: string + jsonPath: + description: |- + jsonPath is a simple JSON path (i.e. with array notation) which is evaluated against + each custom resource to produce the value for this column. + type: string + name: + description: name is a human readable name for the column. + type: string + priority: + description: |- + priority is an integer defining the relative importance of this column compared to others. Lower + numbers are considered higher priority. Columns that may be omitted in limited space scenarios + should be given a priority greater than 0. + format: int32 + type: integer + type: + description: |- + type is an OpenAPI type definition for this column. + See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details. + type: string + required: + - jsonPath + - name + - type + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + deprecated: + description: |- + deprecated indicates this version of the custom resource API is deprecated. + When set to true, API requests to this version receive a warning header in the server response. + Defaults to false. + type: boolean + deprecationWarning: + description: |- + deprecationWarning overrides the default warning returned to API clients. + May only be set when `deprecated` is true. + The default warning indicates this version is deprecated and recommends use + of the newest served version of equal or greater stability, if one exists. + type: string + name: + description: |- + name is the version name, e.g. β€œv1”, β€œv2beta1”, etc. + The custom resources are served under this version at `/apis///...` if `served` is true. + minLength: 1 + pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ + type: string + schema: + description: |- + schema describes the structural schema used for validation, pruning, and defaulting + of this version of the custom resource. + properties: + openAPIV3Schema: + description: openAPIV3Schema is the OpenAPI v3 schema to use + for validation and pruning. + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - openAPIV3Schema + type: object + served: + default: true + description: served is a flag enabling/disabling this version + from being served via REST APIs + type: boolean + storage: + description: |- + storage indicates this version should be used when persisting custom resources to storage. + There must be exactly one version with storage=true. + type: boolean + subresources: + description: subresources specify what subresources this version + of the defined custom resource have. + properties: + scale: + description: scale indicates the custom resource should serve + a `/scale` subresource that returns an `autoscaling/v1` + Scale object. + properties: + labelSelectorPath: + description: |- + labelSelectorPath defines the JSON path inside of a custom resource that corresponds to Scale `status.selector`. + Only JSON paths without the array notation are allowed. + Must be a JSON Path under `.status` or `.spec`. + Must be set to work with HorizontalPodAutoscaler. + The field pointed by this JSON path must be a string field (not a complex selector struct) + which contains a serialized label selector in string form. + More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource + If there is no value under the given path in the custom resource, the `status.selector` value in the `/scale` + subresource will default to the empty string. + type: string + specReplicasPath: + description: |- + specReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `spec.replicas`. + Only JSON paths without the array notation are allowed. + Must be a JSON Path under `.spec`. + If there is no value under the given path in the custom resource, the `/scale` subresource will return an error on GET. + type: string + statusReplicasPath: + description: |- + statusReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `status.replicas`. + Only JSON paths without the array notation are allowed. + Must be a JSON Path under `.status`. + If there is no value under the given path in the custom resource, the `status.replicas` value in the `/scale` subresource + will default to 0. + type: string + required: + - specReplicasPath + - statusReplicasPath + type: object + status: + description: |- + status indicates the custom resource should serve a `/status` subresource. + When enabled: + 1. requests to the custom resource primary endpoint ignore changes to the `status` stanza of the object. + 2. requests to the custom resource `/status` subresource ignore changes to anything other than the `status` stanza of the object. + type: object + type: object + required: + - name + - schema + - served + - storage + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - group + - informerScope + - names + - scope + - versions + type: object + x-kubernetes-validations: + - message: informerScope must be Cluster for cluster-scoped resources + rule: self.scope == "Namespaced" || self.informerScope == "Cluster" + - message: clusterScopedIsolation must be defined for cluster-scoped resources + rule: self.scope == "Namespaced" || has(self.clusterScopedIsolation) + - message: clusterScopedIsolation is not relevant for namespaced resources + rule: self.scope == "Cluster" || !has(self.clusterScopedIsolation) + status: + description: status contains reconciliation information for the resource. + properties: + acceptedNames: + description: |- + acceptedNames are the names that are actually being used to serve discovery. + They may be different than the names in spec. + properties: + categories: + description: |- + categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). + This is published in API discovery documents, and used by clients to support invocations like + `kubectl get all`. + items: + type: string + type: array + x-kubernetes-list-type: atomic + kind: + description: |- + kind is the serialized kind of the resource. It is normally CamelCase and singular. + Custom resource instances will use this value as the `kind` attribute in API calls. + type: string + listKind: + description: listKind is the serialized kind of the list for this + resource. Defaults to "`kind`List". + type: string + plural: + description: |- + plural is the plural name of the resource to serve. + The custom resources are served under `/apis///.../`. + Must match the name of the CustomResourceDefinition (in the form `.`). + Must be all lowercase. + type: string + shortNames: + description: |- + shortNames are short names for the resource, exposed in API discovery documents, + and used by clients to support invocations like `kubectl get `. + It must be all lowercase. + items: + type: string + type: array + x-kubernetes-list-type: atomic + singular: + description: singular is the singular name of the resource. It must + be all lowercase. Defaults to lowercased `kind`. + type: string + required: + - kind + - plural + type: object + conditions: + description: |- + conditions is a list of conditions that apply to the APIServiceExport. It is + updated by the konnector on the consumer cluster. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + storedVersions: + description: |- + storedVersions lists all versions of CustomResources that were ever persisted. Tracking these + versions allows a migration path for stored versions in etcd. The field is mutable + so a migration controller can finish a migration to another version (ensuring + no old objects are left in storage), and then remove the rest of the + versions from this list. + Versions may not be removed from `spec.versions` while they exist in this list. + items: + type: string + type: array + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: informerScope is immutable + rule: self.metadata.name == self.spec.names.plural+"."+self.spec.group + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicenamespaces.kube-bind.io.yaml b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicenamespaces.kube-bind.io.yaml new file mode 100644 index 000000000..2683f2ef3 --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-apiservicenamespaces.kube-bind.io.yaml @@ -0,0 +1,67 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v241216-832011705.apiservicenamespaces.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceNamespace + listKind: APIServiceNamespaceList + plural: apiservicenamespaces + singular: apiservicenamespace + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.namespace + name: Namespace + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + description: |- + APIServiceNamespace defines how consumer namespaces map to service namespaces. + These objects are created by the konnector, and a service namespace is then + created by the service provider. + + The name of the APIServiceNamespace equals the namespace name in the consumer + cluster. + 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: spec specifies a service namespace. + type: object + status: + description: status contains reconciliation information for a service namespace + properties: + namespace: + description: |- + namespace is the service provider namespace name that will be bound to the + consumer namespace named like this object. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml new file mode 100644 index 000000000..9174982d1 --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml @@ -0,0 +1,171 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v241216-832011705.clusterbindings.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: ClusterBinding + listKind: ClusterBindingList + plural: clusterbindings + singular: clusterbinding + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.konnectorVersion + name: Konnector Version + type: string + - jsonPath: .status.lastHeartbeatTime + name: Last Heartbeat + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + description: |- + ClusterBinding represents a bound consumer class. It lives in a service provider cluster + and is a singleton named "cluster". + 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: spec represents the data in the newly created ClusterBinding. + properties: + kubeconfigSecretRef: + description: kubeconfigSecretName is the secret ref that contains the + kubeconfig of the service cluster. + properties: + key: + description: The key of the secret to select from. Must be "kubeconfig". + enum: + - kubeconfig + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-validations: + - message: kubeconfigSecretRef is immutable + rule: self == oldSelf + providerPrettyName: + description: |- + providerPrettyName is the pretty name of the service provider cluster. This + can be shared among different ServiceBindings. + minLength: 1 + type: string + serviceProviderSpec: + description: |- + serviceProviderSpec contains all the data and information about the service which has been bound to the service + binding request. The service providers decide what they need and what to configure based on what then include in + this field, such as service region, type, tiers, etc... + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - kubeconfigSecretRef + - providerPrettyName + type: object + status: + description: status contains reconciliation information for the service + binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + heartbeatInterval: + description: |- + heartbeatInterval is the maximal interval between heartbeats that the + konnector promises to send. The service provider can assume that the + konnector is not unhealthy if it does not receive a heartbeat within + this time. + type: string + konnectorVersion: + description: |- + konnectorVersion is the version of the konnector that is running on the + consumer cluster. + type: string + lastHeartbeatTime: + description: lastHeartbeatTime is the last time the konnector updated + the status. + format: date-time + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: cluster binding name should be cluster + rule: self.metadata.name == "cluster" + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go b/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go new file mode 100644 index 000000000..a518f3393 --- /dev/null +++ b/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 The Kube Bind 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" + + confighelpers "github.com/kcp-dev/kcp/config/helpers" + kcpclientcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" +) + +//go:embed *.yaml +var KubeFS 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, + kcpClient kcpclientcluster.ClusterInterface, + discoveryClient discovery.DiscoveryInterface, + dynamicClient dynamic.Interface, + crdClient apiextensionsv1.CustomResourceDefinitionInterface, + batteriesIncluded sets.Set[string], +) error { + // create resources in core cluster + return confighelpers.Bootstrap(ctx, discoveryClient, dynamicClient, batteriesIncluded, KubeFS, confighelpers.ReplaceOption()) +} diff --git a/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go new file mode 100644 index 000000000..a9d5243d1 --- /dev/null +++ b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go @@ -0,0 +1,332 @@ +/* +Copyright 2022 The Kube Bind 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 clusterbinding + +import ( + "context" + "fmt" + "time" + + "github.com/google/go-cmp/cmp" + kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" + kubeinformers "github.com/kcp-dev/client-go/informers/core/v1" + rbacinformers "github.com/kcp-dev/client-go/informers/rbac/v1" + kubeclient "github.com/kcp-dev/client-go/kubernetes" + corelisters "github.com/kcp-dev/client-go/listers/core/v1" + rbaclisters "github.com/kcp-dev/client-go/listers/rbac/v1" + "github.com/kcp-dev/logicalcluster/v3" + + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/committer" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + kubebindv1alpha1clusterclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned/cluster/typed/kubebind/v1alpha1" + bindinformers "github.com/kube-bind/kube-bind/sdk/kcp/informers/externalversions/kubebind/v1alpha1" + bindlisters "github.com/kube-bind/kube-bind/sdk/kcp/listers/kubebind/v1alpha1" +) + +const ( + controllerName = "kube-bind-example-backend-clusterbinding" +) + +// NewController returns a new controller to reconcile ClusterBindings. +func NewController( + config *rest.Config, + scope kubebindv1alpha1.Scope, + clusterBindingInformer bindinformers.ClusterBindingClusterInformer, + serviceExportInformer bindinformers.APIServiceExportClusterInformer, + clusterRoleInformer rbacinformers.ClusterRoleClusterInformer, + clusterRoleBindingInformer rbacinformers.ClusterRoleBindingClusterInformer, + roleBindingInformer rbacinformers.RoleBindingClusterInformer, + namespaceInformer kubeinformers.NamespaceClusterInformer, +) (*Controller, error) { + queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) + + logger := klog.Background().WithValues("controller", controllerName) + + config = rest.CopyConfig(config) + config = rest.AddUserAgent(config, controllerName) + + bindClientCluster, err := kubebindv1alpha1clusterclient.NewForConfig(config) + if err != nil { + return nil, err + } + + kubeClient, err := kubeclient.NewForConfig(config) + if err != nil { + return nil, err + } + + c := &Controller{ + queue: queue, + + clusterBindingLister: clusterBindingInformer.Lister(), + clusterBindingIndexer: clusterBindingInformer.Informer().GetIndexer(), + + serviceExportLister: serviceExportInformer.Lister(), + serviceExportIndexer: serviceExportInformer.Informer().GetIndexer(), + + clusterRoleLister: clusterRoleInformer.Lister(), + clusterRoleIndexer: clusterRoleInformer.Informer().GetIndexer(), + + clusterRoleBindingLister: clusterRoleBindingInformer.Lister(), + clusterRoleBindingIndexer: clusterRoleBindingInformer.Informer().GetIndexer(), + + namespaceLister: namespaceInformer.Lister(), + namespaceIndexer: namespaceInformer.Informer().GetIndexer(), + + bindClient: bindClientCluster, + + reconciler: reconciler{ + scope: scope, + listServiceExports: func(cluster logicalcluster.Name, ns string) ([]*kubebindv1alpha1.APIServiceExport, error) { + return serviceExportInformer.Lister().Cluster(cluster).APIServiceExports(ns).List(labels.Everything()) + }, + getClusterRole: func(cluster logicalcluster.Name, name string) (*rbacv1.ClusterRole, error) { + return clusterRoleInformer.Lister().Cluster(cluster).Get(name) + }, + createClusterRole: func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRole) (*rbacv1.ClusterRole, error) { + return kubeClient.RbacV1().Cluster(cluster).ClusterRoles().Create(ctx, binding, metav1.CreateOptions{}) + }, + updateClusterRole: func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRole) (*rbacv1.ClusterRole, error) { + return kubeClient.RbacV1().Cluster(cluster).ClusterRoles().Update(ctx, binding, metav1.UpdateOptions{}) + }, + getClusterRoleBinding: func(cluster logicalcluster.Name, name string) (*rbacv1.ClusterRoleBinding, error) { + return clusterRoleBindingInformer.Lister().Cluster(cluster).Get(name) + }, + createClusterRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRoleBinding) (*rbacv1.ClusterRoleBinding, error) { + return kubeClient.RbacV1().Cluster(cluster).ClusterRoleBindings().Create(ctx, binding, metav1.CreateOptions{}) + }, + updateClusterRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRoleBinding) (*rbacv1.ClusterRoleBinding, error) { + return kubeClient.RbacV1().Cluster(cluster).ClusterRoleBindings().Update(ctx, binding, metav1.UpdateOptions{}) + }, + deleteClusterRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, name string) error { + return kubeClient.RbacV1().Cluster(cluster).ClusterRoleBindings().Delete(ctx, name, metav1.DeleteOptions{}) + }, + getNamespace: func(cluster logicalcluster.Name, name string) (*v1.Namespace, error) { + return namespaceInformer.Lister().Cluster(cluster).Get(name) + }, + createRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, ns string, binding *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) { + return kubeClient.RbacV1().Cluster(cluster).RoleBindings(ns).Create(ctx, binding, metav1.CreateOptions{}) + }, + updateRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, ns string, binding *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) { + return kubeClient.RbacV1().Cluster(cluster).RoleBindings(ns).Update(ctx, binding, metav1.UpdateOptions{}) + }, + getRoleBinding: func(cluster logicalcluster.Name, ns, name string) (*rbacv1.RoleBinding, error) { + return roleBindingInformer.Lister().Cluster(cluster).RoleBindings(ns).Get(name) + }, + }, + } + + clusterBindingInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueClusterBinding(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueClusterBinding(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueClusterBinding(logger, obj) + }, + }) + + serviceExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueServiceExport(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + }) + + return c, nil +} + +type Resource = committer.Resource[*kubebindv1alpha1.ClusterBindingSpec, *kubebindv1alpha1.ClusterBindingStatus] + +// Controller reconciles ClusterBinding conditions. +type Controller struct { + queue workqueue.RateLimitingInterface + + clusterBindingLister bindlisters.ClusterBindingClusterLister + clusterBindingIndexer cache.Indexer + + serviceExportLister bindlisters.APIServiceExportClusterLister + serviceExportIndexer cache.Indexer + + clusterRoleLister rbaclisters.ClusterRoleClusterLister + clusterRoleIndexer cache.Indexer + + clusterRoleBindingLister rbaclisters.ClusterRoleBindingClusterLister + clusterRoleBindingIndexer cache.Indexer + + namespaceLister corelisters.NamespaceClusterLister + namespaceIndexer cache.Indexer + + bindClient *kubebindv1alpha1clusterclient.KubeBindV1alpha1ClusterClient + + reconciler +} + +func (c *Controller) enqueueClusterBinding(logger klog.Logger, obj interface{}) { + key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + logger.V(2).Info("queueing ClusterBinding", "key", key) + c.queue.Add(key) +} + +func (c *Controller) enqueueServiceExport(logger klog.Logger, obj interface{}) { + seKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + // TODO: this might be wrong. In most cases is. + _, ns, _, err := kcpcache.SplitMetaClusterNamespaceKey(seKey) + if err != nil { + runtime.HandleError(err) + return + } + + key := ns + "/cluster" + logger.V(2).Info("queueing ClusterBinding", "key", key, "reason", "APIServiceExport", "ServiceExportKey", seKey) + c.queue.Add(key) +} + +// Start starts the controller, which stops when ctx.Done() is closed. +func (c *Controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + defer c.queue.ShutDown() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *Controller) startWorker(ctx context.Context) { + defer runtime.HandleCrash() + + for c.processNextWorkItem(ctx) { + } +} + +func (c *Controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k.(string) + + logger := klog.FromContext(ctx).WithValues("key", key) + ctx = klog.NewContext(ctx, logger) + logger.V(2).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *Controller) process(ctx context.Context, key string) error { + logger := klog.FromContext(ctx) + + clusterName, ns, name, err := kcpcache.SplitMetaClusterNamespaceKey(key) + if err != nil { + logger.Error(err, "invalid key") + return nil + } + + logger.V(4).Info("processing ClusterBinding", "cluster", clusterName, "namespace", ns, "name", name) + obj, err := c.clusterBindingLister.Cluster(clusterName).ClusterBindings(ns).Get(name) + if err != nil && !errors.IsNotFound(err) { + return err + } else if errors.IsNotFound(err) { + logger.V(2).Info("ClusterBinding not found, ignoring") + return nil // nothing we can do + } + + old := obj + obj = obj.DeepCopy() + + var errs []error + if err := c.reconcile(ctx, clusterName, obj); err != nil { + errs = append(errs, err) + } + + // Regardless of whether reconcile returned an error or not, always try to patch status if needed. Return the + // reconciliation error at the end. + + // If the object being reconciled changed as a result, update it. + objectMetaChanged := !equality.Semantic.DeepEqual(old.ObjectMeta, obj.ObjectMeta) + specChanged := !equality.Semantic.DeepEqual(old.Spec, obj.Spec) + statusChanged := !equality.Semantic.DeepEqual(old.Status, obj.Status) + + specOrObjectMetaChanged := specChanged || objectMetaChanged + + // Simultaneous updates of spec and status are never allowed. + if specOrObjectMetaChanged && statusChanged { + panic(fmt.Sprintf("programmer error: spec and status changed in same reconcile iteration. diff=%s", cmp.Diff(old, obj))) + } + + oldResource := &Resource{ObjectMeta: old.ObjectMeta, Spec: &old.Spec, Status: &old.Status} + newResource := &Resource{ObjectMeta: obj.ObjectMeta, Spec: &obj.Spec, Status: &obj.Status} + patchBytes, subresources, err := committer.GeneratePatchAndSubResources(oldResource, newResource) + if err != nil { + errs = append(errs, err) + } + + if len(patchBytes) == 0 { + return nil + } + + _, err = c.bindClient.Cluster(clusterName.Path()).ClusterBindings(ns).Patch(ctx, obj.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, subresources...) + return err +} diff --git a/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go new file mode 100644 index 000000000..cc38b7997 --- /dev/null +++ b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go @@ -0,0 +1,280 @@ +/* +Copyright 2022 The Kube Bind 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 clusterbinding + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/pointer" + + kuberesources "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes/resources" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" + "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions" +) + +type reconciler struct { + scope kubebindv1alpha1.Scope + + listServiceExports func(cluster logicalcluster.Name, ns string) ([]*kubebindv1alpha1.APIServiceExport, error) + + getClusterRole func(cluster logicalcluster.Name, name string) (*rbacv1.ClusterRole, error) + createClusterRole func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRole) (*rbacv1.ClusterRole, error) + updateClusterRole func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRole) (*rbacv1.ClusterRole, error) + + getClusterRoleBinding func(cluster logicalcluster.Name, name string) (*rbacv1.ClusterRoleBinding, error) + createClusterRoleBinding func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRoleBinding) (*rbacv1.ClusterRoleBinding, error) + updateClusterRoleBinding func(ctx context.Context, cluster logicalcluster.Path, binding *rbacv1.ClusterRoleBinding) (*rbacv1.ClusterRoleBinding, error) + deleteClusterRoleBinding func(ctx context.Context, cluster logicalcluster.Path, name string) error + + getRoleBinding func(cluster logicalcluster.Name, ns, name string) (*rbacv1.RoleBinding, error) + createRoleBinding func(ctx context.Context, cluster logicalcluster.Path, ns string, binding *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) + updateRoleBinding func(ctx context.Context, cluster logicalcluster.Path, ns string, binding *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) + + getNamespace func(cluster logicalcluster.Name, name string) (*corev1.Namespace, error) +} + +func (r *reconciler) reconcile(ctx context.Context, clusterName logicalcluster.Name, clusterBinding *kubebindv1alpha1.ClusterBinding) error { + var errs []error + + if err := r.ensureClusterBindingConditions(ctx, clusterName, clusterBinding); err != nil { + errs = append(errs, err) + } + if err := r.ensureRBACRoleBinding(ctx, clusterName, clusterBinding); err != nil { + errs = append(errs, err) + } + if err := r.ensureRBACClusterRole(ctx, clusterName, clusterBinding); err != nil { + errs = append(errs, err) + } + if err := r.ensureRBACClusterRoleBinding(ctx, clusterName, clusterBinding); err != nil { + errs = append(errs, err) + } + + conditions.SetSummary(clusterBinding) + + return utilerrors.NewAggregate(errs) +} + +func (r *reconciler) ensureClusterBindingConditions(ctx context.Context, _ logicalcluster.Name, clusterBinding *kubebindv1alpha1.ClusterBinding) error { + if clusterBinding.Status.LastHeartbeatTime.IsZero() { + conditions.MarkFalse(clusterBinding, + kubebindv1alpha1.ClusterBindingConditionHealthy, + "FirstHeartbeatPending", + conditionsapi.ConditionSeverityInfo, + "Waiting for first heartbeat", + ) + } else if clusterBinding.Status.HeartbeatInterval.Duration == 0 { + conditions.MarkFalse(clusterBinding, + kubebindv1alpha1.ClusterBindingConditionHealthy, + "HeartbeatIntervalMissing", + conditionsapi.ConditionSeverityInfo, + "Waiting for consumer cluster reporting its heartbeat interval", + ) + } else if ago := time.Since(clusterBinding.Status.LastHeartbeatTime.Time); ago > clusterBinding.Status.HeartbeatInterval.Duration*2 { + conditions.MarkFalse(clusterBinding, + kubebindv1alpha1.ClusterBindingConditionHealthy, + "HeartbeatTimeout", + conditionsapi.ConditionSeverityError, + "Heartbeat timeout: expected heartbeat within %s, but last one has been at %s", + clusterBinding.Status.HeartbeatInterval.Duration, + clusterBinding.Status.LastHeartbeatTime.Time, // do not put "ago" here. It will hotloop. + ) + } else if ago < time.Second*10 { + conditions.MarkFalse(clusterBinding, + kubebindv1alpha1.ClusterBindingConditionHealthy, + "HeartbeatTimeDrift", + conditionsapi.ConditionSeverityWarning, + "Clocks of consumer cluster and service account cluster seem to be off by more than 10s", + ) + } else { + conditions.MarkTrue(clusterBinding, + kubebindv1alpha1.ClusterBindingConditionHealthy, + ) + } + + return nil +} + +func (r *reconciler) ensureRBACClusterRole(ctx context.Context, clusterName logicalcluster.Name, clusterBinding *kubebindv1alpha1.ClusterBinding) error { + name := "kube-binder-" + clusterBinding.Namespace + cluster := clusterName.Path() + + role, err := r.getClusterRole(clusterName, name) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get ClusterRole %s: %w", name, err) + } + + ns, err := r.getNamespace(clusterName, clusterBinding.Namespace) + if err != nil { + return fmt.Errorf("failed to get Namespace %s: %w", clusterBinding.Namespace, err) + } + + exports, err := r.listServiceExports(clusterName, clusterBinding.Namespace) + if err != nil { + return fmt.Errorf("failed to list APIServiceExports: %w", err) + } + expected := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Namespace", + Name: clusterBinding.Namespace, + Controller: pointer.Bool(true), + UID: ns.UID, + }, + }, + }, + } + for _, export := range exports { + expected.Rules = append(expected.Rules, rbacv1.PolicyRule{ + APIGroups: []string{export.Spec.Group}, + Resources: []string{export.Spec.Names.Plural}, + Verbs: []string{"get", "list", "watch", "update", "patch", "delete", "create"}, + }) + } + + if role == nil { + if _, err := r.createClusterRole(ctx, cluster, expected); err != nil { + return fmt.Errorf("failed to create ClusterRole %s: %w", expected.Name, err) + } + } else if !reflect.DeepEqual(role.Rules, expected.Rules) { + role = role.DeepCopy() + role.Rules = expected.Rules + if _, err := r.updateClusterRole(ctx, cluster, role); err != nil { + return fmt.Errorf("failed to create ClusterRole %s: %w", role.Name, err) + } + } + + return nil +} + +func (r *reconciler) ensureRBACClusterRoleBinding(ctx context.Context, clusterName logicalcluster.Name, clusterBinding *kubebindv1alpha1.ClusterBinding) error { + name := "kube-binder-" + clusterBinding.Namespace + cluster := clusterName.Path() + + binding, err := r.getClusterRoleBinding(clusterName, name) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get ClusterRoleBinding %s: %w", name, err) + } + + if r.scope != kubebindv1alpha1.ClusterScope { + if err := r.deleteClusterRoleBinding(ctx, cluster, name); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete ClusterRoleBinding %s: %w", name, err) + } + } + + ns, err := r.getNamespace(clusterName, clusterBinding.Namespace) + if err != nil { + return fmt.Errorf("failed to get Namespace %s: %w", clusterBinding.Namespace, err) + } + + expected := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Namespace", + Name: clusterBinding.Namespace, + Controller: pointer.Bool(true), + UID: ns.UID, + }, + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: clusterBinding.Namespace, + Name: kuberesources.ServiceAccountName, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: name, + APIGroup: "rbac.authorization.k8s.io", + }, + } + + if binding == nil { + if _, err := r.createClusterRoleBinding(ctx, cluster, expected); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding %s: %w", expected.Name, err) + } + } else if !reflect.DeepEqual(binding.Subjects, expected.Subjects) { + binding = binding.DeepCopy() + binding.Subjects = expected.Subjects + // roleRef is immutable + if _, err := r.updateClusterRoleBinding(ctx, cluster, binding); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding %s: %w", expected.Namespace, err) + } + } + + return nil +} + +func (r *reconciler) ensureRBACRoleBinding(ctx context.Context, clusterName logicalcluster.Name, clusterBinding *kubebindv1alpha1.ClusterBinding) error { + cluster := clusterName.Path() + + binding, err := r.getRoleBinding(clusterName, clusterBinding.Namespace, "kube-binder") + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get RoleBinding \"kube-binder\": %w", err) + } + + expected := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: kuberesources.ServiceAccountName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: kuberesources.ServiceAccountName, + Namespace: clusterBinding.Namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "kube-binder", + }, + } + + if binding == nil { + if _, err := r.createRoleBinding(ctx, cluster, clusterBinding.Namespace, expected); err != nil { + return fmt.Errorf("failed to create RoleBinding %s: %w", expected.Name, err) + } + } else if !reflect.DeepEqual(binding.Subjects, expected.Subjects) { + binding = binding.DeepCopy() + binding.Subjects = expected.Subjects + // roleRef is immutable + if _, err := r.updateRoleBinding(ctx, cluster, clusterBinding.Namespace, binding); err != nil { + return fmt.Errorf("failed to create RoleBinding %s: %w", expected.Namespace, err) + } + } + + return nil +} diff --git a/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go new file mode 100644 index 000000000..3c2ba9dea --- /dev/null +++ b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go @@ -0,0 +1,289 @@ +/* +Copyright 2022 The Kube Bind 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 serviceexport + +import ( + "context" + "fmt" + "time" + + "github.com/google/go-cmp/cmp" + kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" + apiextensionsinformers "github.com/kcp-dev/client-go/apiextensions/informers/apiextensions/v1" + apiextensionslisters "github.com/kcp-dev/client-go/apiextensions/listers/apiextensions/v1" + "github.com/kcp-dev/logicalcluster/v3" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/committer" + "github.com/kube-bind/kube-bind/pkg/indexers" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + kubebindv1alpha1clusterclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned/cluster/typed/kubebind/v1alpha1" + bindinformers "github.com/kube-bind/kube-bind/sdk/kcp/informers/externalversions/kubebind/v1alpha1" + bindlisters "github.com/kube-bind/kube-bind/sdk/kcp/listers/kubebind/v1alpha1" +) + +const ( + controllerName = "kube-bind-example-backend-serviceexport" +) + +// NewController returns a new controller to reconcile ServiceExports. +func NewController( + config *rest.Config, + serviceExportInformer bindinformers.APIServiceExportClusterInformer, + crdInformer apiextensionsinformers.CustomResourceDefinitionClusterInformer, +) (*Controller, error) { + queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) + + logger := klog.Background().WithValues("controller", controllerName) + + config = rest.CopyConfig(config) + config = rest.AddUserAgent(config, controllerName) + + bindClient, err := kubebindv1alpha1clusterclient.NewForConfig(config) + if err != nil { + return nil, err + } + + c := &Controller{ + queue: queue, + + bindClient: bindClient, + + serviceExportLister: serviceExportInformer.Lister(), + serviceExportIndexer: serviceExportInformer.Informer().GetIndexer(), + + crdLister: crdInformer.Lister(), + crdIndexer: crdInformer.Informer().GetIndexer(), + + reconciler: reconciler{ + getCRD: func(clusterName logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) { + return crdInformer.Lister().Cluster(clusterName).Get(name) + }, + deleteServiceExport: func(ctx context.Context, clusterName logicalcluster.Name, ns, name string) error { + return bindClient.Cluster(clusterName.Path()).APIServiceExports(ns).Delete(ctx, name, metav1.DeleteOptions{}) + }, + requeue: func(export *kubebindv1alpha1.APIServiceExport) { + key, err := kcpcache.MetaClusterNamespaceKeyFunc(export) + if err != nil { + runtime.HandleError(err) + return + } + queue.Add(key) + }, + }, + } + + indexers.AddIfNotPresentOrDie(serviceExportInformer.Informer().GetIndexer(), cache.Indexers{ + indexers.ServiceExportByCustomResourceDefinition: indexers.IndexServiceExportByCustomResourceDefinition, + }) + + serviceExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueServiceExport(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + }) + + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueCRD(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueCRD(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueCRD(logger, obj) + }, + }) + + return c, nil +} + +type Resource = committer.Resource[*kubebindv1alpha1.APIServiceExportSpec, *kubebindv1alpha1.APIServiceExportStatus] + +// Controller reconciles ServiceNamespaces by creating a Namespace for each, and deleting it if +// the APIServiceNamespace is deleted. +type Controller struct { + queue workqueue.RateLimitingInterface + + serviceExportLister bindlisters.APIServiceExportClusterLister + serviceExportIndexer cache.Indexer + + crdLister apiextensionslisters.CustomResourceDefinitionClusterLister + crdIndexer cache.Indexer + + reconciler + + bindClient *kubebindv1alpha1clusterclient.KubeBindV1alpha1ClusterClient +} + +func (c *Controller) enqueueServiceExport(logger klog.Logger, obj interface{}) { + key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + logger.V(2).Info("queueing APIServiceExport", "key", key) + c.queue.Add(key) +} + +func (c *Controller) enqueueCRD(logger klog.Logger, obj interface{}) { + crdKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + exports, err := c.serviceExportIndexer.ByIndex(indexers.ServiceExportByCustomResourceDefinition, crdKey) + if err != nil { + runtime.HandleError(err) + return + } + + for _, obj := range exports { + export, ok := obj.(*kubebindv1alpha1.APIServiceExport) + if !ok { + runtime.HandleError(fmt.Errorf("unexpected type %T", obj)) + return + } + key, err := cache.MetaNamespaceKeyFunc(export) + if err != nil { + runtime.HandleError(err) + continue + } + logger.V(2).Info("queueing APIServiceExport", "key", key, "reason", "CustomResourceDefinition", "CustomResourceDefinitionKey", crdKey) + c.queue.Add(key) + } +} + +// Start starts the controller, which stops when ctx.Done() is closed. +func (c *Controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + defer c.queue.ShutDown() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *Controller) startWorker(ctx context.Context) { + defer runtime.HandleCrash() + + for c.processNextWorkItem(ctx) { + } +} + +func (c *Controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k.(string) + + logger := klog.FromContext(ctx).WithValues("key", key) + ctx = klog.NewContext(ctx, logger) + logger.V(2).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *Controller) process(ctx context.Context, key string) error { + clusterName, snsNamespace, snsName, err := kcpcache.SplitMetaClusterNamespaceKey(key) + if err != nil { + return nil + } + + obj, err := c.serviceExportLister.Cluster(clusterName).APIServiceExports(snsNamespace).Get(snsName) + if err != nil && !errors.IsNotFound(err) { + return err + } else if errors.IsNotFound(err) { + return nil // nothing we can do + } + + old := obj + obj = obj.DeepCopy() + + var errs []error + if err := c.reconcile(ctx, clusterName, obj); err != nil { + errs = append(errs, err) + } + + // Regardless of whether reconcile returned an error or not, always try to patch status if needed. Return the + // reconciliation error at the end. + + // If the object being reconciled changed as a result, update it. + objectMetaChanged := !equality.Semantic.DeepEqual(old.ObjectMeta, obj.ObjectMeta) + specChanged := !equality.Semantic.DeepEqual(old.Spec, obj.Spec) + statusChanged := !equality.Semantic.DeepEqual(old.Status, obj.Status) + + specOrObjectMetaChanged := specChanged || objectMetaChanged + + // Simultaneous updates of spec and status are never allowed. + if specOrObjectMetaChanged && statusChanged { + panic(fmt.Sprintf("programmer error: spec and status changed in same reconcile iteration. diff=%s", cmp.Diff(old, obj))) + } + + oldResource := &Resource{ObjectMeta: old.ObjectMeta, Spec: &old.Spec, Status: &old.Status} + newResource := &Resource{ObjectMeta: obj.ObjectMeta, Spec: &obj.Spec, Status: &obj.Status} + + patchBytes, subresources, err := committer.GeneratePatchAndSubResources(oldResource, newResource) + if err != nil { + errs = append(errs, err) + } + + if len(patchBytes) == 0 { + return nil + } + + _, err = c.bindClient.Cluster(clusterName.Path()).APIServiceExports(snsNamespace).Patch(ctx, obj.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, subresources...) + return err +} diff --git a/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go new file mode 100644 index 000000000..a2af00887 --- /dev/null +++ b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go @@ -0,0 +1,96 @@ +/* +Copyright 2022 The Kube Bind 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 serviceexport + +import ( + "context" + + "github.com/kcp-dev/logicalcluster/v3" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/klog/v2" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + kubebindhelpers "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1/helpers" + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" + "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions" +) + +type reconciler struct { + getCRD func(clusterName logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) + deleteServiceExport func(ctx context.Context, clusterName logicalcluster.Name, namespace, name string) error + + requeue func(export *kubebindv1alpha1.APIServiceExport) +} + +func (r *reconciler) reconcile(ctx context.Context, clusterName logicalcluster.Name, export *kubebindv1alpha1.APIServiceExport) error { + var errs []error + + if specChanged, err := r.ensureSchema(ctx, clusterName, export); err != nil { + errs = append(errs, err) + } else if specChanged { + r.requeue(export) + return nil + } + + return utilerrors.NewAggregate(errs) +} + +func (r *reconciler) ensureSchema(ctx context.Context, clusterName logicalcluster.Name, export *kubebindv1alpha1.APIServiceExport) (specChanged bool, err error) { + logger := klog.FromContext(ctx) + + crd, err := r.getCRD(clusterName, export.Name) + if err != nil && !errors.IsNotFound(err) { + return false, err + } + + if crd == nil { + // CRD missing => delete SER too + logger.V(1).Info("Deleting APIServiceExport because CRD is missing") + return false, r.deleteServiceExport(ctx, clusterName, export.Namespace, export.Name) + } + + expected, err := kubebindhelpers.CRDToServiceExport(crd) + if err != nil { + conditions.MarkFalse( + export, + kubebindv1alpha1.APIServiceExportConditionProviderInSync, + "CustomResourceDefinitionUpdateFailed", + conditionsapi.ConditionSeverityError, + "CustomResourceDefinition %s cannot be converted into a APIServiceExport: %s", + export.Name, err, + ) + return false, nil //nothing we can do + } + + if hash := kubebindhelpers.APIServiceExportCRDSpecHash(expected); export.Annotations[kubebindv1alpha1.SourceSpecHashAnnotationKey] != hash { + // both exist, update APIServiceExport + logger.V(1).Info("Updating APIServiceExport") + export.Spec.APIServiceExportCRDSpec = *expected + if export.Annotations == nil { + export.Annotations = map[string]string{} + } + export.Annotations[kubebindv1alpha1.SourceSpecHashAnnotationKey] = hash + return true, nil + } + + conditions.MarkTrue(export, kubebindv1alpha1.APIServiceExportConditionProviderInSync) + + return false, nil +} diff --git a/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go new file mode 100644 index 000000000..d42c0f45c --- /dev/null +++ b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go @@ -0,0 +1,338 @@ +/* +Copyright 2022 The Kube Bind 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 serviceexportrequest + +import ( + "context" + "fmt" + "time" + + "github.com/google/go-cmp/cmp" + kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" + apiextensionsinformers "github.com/kcp-dev/client-go/apiextensions/informers/apiextensions/v1" + apiextensionslisters "github.com/kcp-dev/client-go/apiextensions/listers/apiextensions/v1" + "github.com/kcp-dev/logicalcluster/v3" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + kubernetesclient "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/committer" + "github.com/kube-bind/kube-bind/pkg/indexers" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + bindclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned/cluster" + bindinformers "github.com/kube-bind/kube-bind/sdk/kcp/informers/externalversions/kubebind/v1alpha1" + bindlisters "github.com/kube-bind/kube-bind/sdk/kcp/listers/kubebind/v1alpha1" +) + +const ( + controllerName = "kube-bind-example-backend-serviceexportrequest" +) + +// NewController returns a new controller to reconcile APIServiceExportRequests by +// creating corresponding APIServiceExports. +func NewController( + config *rest.Config, + scope kubebindv1alpha1.Scope, + serviceExportRequestInformer bindinformers.APIServiceExportRequestClusterInformer, + serviceExportInformer bindinformers.APIServiceExportClusterInformer, + crdInformer apiextensionsinformers.CustomResourceDefinitionClusterInformer, +) (*Controller, error) { + queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) + + logger := klog.Background().WithValues("controller", controllerName) + + config = rest.CopyConfig(config) + config = rest.AddUserAgent(config, controllerName) + + bindClient, err := bindclient.NewForConfig(config) + if err != nil { + return nil, err + } + kubeClient, err := kubernetesclient.NewForConfig(config) + if err != nil { + return nil, err + } + + c := &Controller{ + queue: queue, + + bindClient: bindClient, + kubeClient: kubeClient, + + serviceExportLister: serviceExportInformer.Lister(), + serviceExportIndexer: serviceExportInformer.Informer().GetIndexer(), + + serviceExportRequestLister: serviceExportRequestInformer.Lister(), + serviceExportRequestIndexer: serviceExportRequestInformer.Informer().GetIndexer(), + + crdLister: crdInformer.Lister(), + crdIndexer: crdInformer.Informer().GetIndexer(), + + reconciler: reconciler{ + informerScope: scope, + getCRD: func(cluster logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) { + return crdInformer.Lister().Cluster(cluster).Get(name) + }, + getServiceExport: func(cluster logicalcluster.Name, ns, name string) (*kubebindv1alpha1.APIServiceExport, error) { + return serviceExportInformer.Lister().Cluster(cluster).APIServiceExports(ns).Get(name) + }, + createServiceExport: func(ctx context.Context, clusterName logicalcluster.Name, resource *kubebindv1alpha1.APIServiceExport) (*kubebindv1alpha1.APIServiceExport, error) { + return bindClient.KubeBindV1alpha1().Cluster(clusterName.Path()).APIServiceExports(resource.Namespace).Create(ctx, resource, metav1.CreateOptions{}) + }, + deleteServiceExportRequest: func(ctx context.Context, cluster logicalcluster.Name, ns, name string) error { + return bindClient.KubeBindV1alpha1().Cluster(cluster.Path()).APIServiceExportRequests(ns).Delete(ctx, name, metav1.DeleteOptions{}) + }, + }, + } + + indexers.AddIfNotPresentOrDie(serviceExportRequestInformer.Informer().GetIndexer(), cache.Indexers{ + indexers.ServiceExportRequestByServiceExport: indexers.IndexServiceExportRequestByServiceExport, + }) + indexers.AddIfNotPresentOrDie(serviceExportRequestInformer.Informer().GetIndexer(), cache.Indexers{ + indexers.ServiceExportRequestByGroupResource: indexers.IndexServiceExportRequestByGroupResource, + }) + + serviceExportRequestInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceExportRequest(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueServiceExportRequest(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceExportRequest(logger, obj) + }, + }) + + serviceExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueServiceExport(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + }) + + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueCRD(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + c.enqueueCRD(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueCRD(logger, obj) + }, + }) + + return c, nil +} + +type Resource = committer.Resource[*kubebindv1alpha1.APIServiceExportRequestSpec, *kubebindv1alpha1.APIServiceExportRequestStatus] + +// Controller to reconcile APIServiceExportRequests by creating corresponding APIServiceExports. +type Controller struct { + queue workqueue.RateLimitingInterface + + bindClient bindclient.ClusterInterface + kubeClient kubernetesclient.Interface + + serviceExportRequestLister bindlisters.APIServiceExportRequestClusterLister + serviceExportRequestIndexer cache.Indexer + + serviceExportLister bindlisters.APIServiceExportClusterLister + serviceExportIndexer cache.Indexer + + crdLister apiextensionslisters.CustomResourceDefinitionClusterLister + crdIndexer cache.Indexer + + reconciler +} + +func (c *Controller) enqueueServiceExportRequest(logger klog.Logger, obj interface{}) { + key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + logger.V(2).Info("queueing APIServiceExportRequest", "key", key) + c.queue.Add(key) +} + +func (c *Controller) enqueueServiceExport(logger klog.Logger, obj interface{}) { + seKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + requests, err := c.serviceExportRequestIndexer.ByIndex(indexers.ServiceExportRequestByServiceExport, seKey) + if err != nil { + runtime.HandleError(err) + return + } + for _, obj := range requests { + key, err := kcpcache.MetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + continue + } + logger.V(2).Info("queueing APIServiceExportRequest", "key", key, "reason", "APIServiceExport", "APIServiceExportKey", seKey) + c.queue.Add(key) + } +} + +func (c *Controller) enqueueCRD(logger klog.Logger, obj interface{}) { + crdKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + requests, err := c.serviceExportRequestIndexer.ByIndex(indexers.ServiceExportRequestByGroupResource, crdKey) + if err != nil { + runtime.HandleError(err) + return + } + for _, obj := range requests { + key, err := kcpcache.MetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + continue + } + logger.V(2).Info("queueing APIServiceExportRequest", "key", key, "reason", "CustomResourceDefinition", "CustomResourceDefinitionKey", crdKey) + c.queue.Add(key) + } +} + +// Start starts the controller, which stops when ctx.Done() is closed. +func (c *Controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + defer c.queue.ShutDown() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *Controller) startWorker(ctx context.Context) { + defer runtime.HandleCrash() + + for c.processNextWorkItem(ctx) { + } +} + +func (c *Controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k.(string) + + logger := klog.FromContext(ctx).WithValues("key", key) + ctx = klog.NewContext(ctx, logger) + logger.V(2).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *Controller) process(ctx context.Context, key string) error { + logger := klog.FromContext(ctx) + + clusterName, snsNamespace, snsName, err := kcpcache.SplitMetaClusterNamespaceKey(key) + if err != nil { + return nil + } + + obj, err := c.serviceExportRequestLister.Cluster(clusterName).APIServiceExportRequests(snsNamespace).Get(snsName) + if err != nil && !errors.IsNotFound(err) { + return err + } else if errors.IsNotFound(err) { + logger.V(2).Info("APIServiceExport not found, ignoring") + return nil // nothing we can do + } + + old := obj + obj = obj.DeepCopy() + + var errs []error + if err := c.reconcile(ctx, clusterName, obj); err != nil { + errs = append(errs, err) + } + + // Regardless of whether reconcile returned an error or not, always try to patch status if needed. Return the + // reconciliation error at the end. + + // If the object being reconciled changed as a result, update it. + objectMetaChanged := !equality.Semantic.DeepEqual(old.ObjectMeta, obj.ObjectMeta) + specChanged := !equality.Semantic.DeepEqual(old.Spec, obj.Spec) + statusChanged := !equality.Semantic.DeepEqual(old.Status, obj.Status) + + specOrObjectMetaChanged := specChanged || objectMetaChanged + + // Simultaneous updates of spec and status are never allowed. + if specOrObjectMetaChanged && statusChanged { + panic(fmt.Sprintf("programmer error: spec and status changed in same reconcile iteration. diff=%s", cmp.Diff(old, obj))) + } + + oldResource := &Resource{ObjectMeta: old.ObjectMeta, Spec: &old.Spec, Status: &old.Status} + newResource := &Resource{ObjectMeta: obj.ObjectMeta, Spec: &obj.Spec, Status: &obj.Status} + + patchBytes, subresources, err := committer.GeneratePatchAndSubResources(oldResource, newResource) + if err != nil { + errs = append(errs, err) + } + + if len(patchBytes) == 0 { + return nil + } + + _, err = c.bindClient.Cluster(clusterName.Path()).KubeBindV1alpha1().APIServiceExportRequests(snsNamespace).Patch(ctx, obj.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, subresources...) + return err +} diff --git a/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go new file mode 100644 index 000000000..9f209b770 --- /dev/null +++ b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go @@ -0,0 +1,144 @@ +/* +Copyright 2022 The Kube Bind 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 serviceexportrequest + +import ( + "context" + "time" + + "github.com/kcp-dev/logicalcluster/v3" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/klog/v2" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1/helpers" + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" + "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions" +) + +type reconciler struct { + informerScope kubebindv1alpha1.Scope + + getCRD func(cluster logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) + getServiceExport func(cluster logicalcluster.Name, ns, name string) (*kubebindv1alpha1.APIServiceExport, error) + createServiceExport func(ctx context.Context, cluster logicalcluster.Name, resource *kubebindv1alpha1.APIServiceExport) (*kubebindv1alpha1.APIServiceExport, error) + + deleteServiceExportRequest func(ctx context.Context, cluster logicalcluster.Name, namespace, name string) error +} + +func (r *reconciler) reconcile(ctx context.Context, clusterName logicalcluster.Name, req *kubebindv1alpha1.APIServiceExportRequest) error { + var errs []error + + if err := r.ensureExports(ctx, clusterName, req); err != nil { + errs = append(errs, err) + } + + conditions.SetSummary(req) + + return utilerrors.NewAggregate(errs) +} + +func (r *reconciler) ensureExports(ctx context.Context, clusterName logicalcluster.Name, req *kubebindv1alpha1.APIServiceExportRequest) error { + logger := klog.FromContext(ctx) + + if req.Status.Phase == kubebindv1alpha1.APIServiceExportRequestPhasePending { + failure := false + for _, res := range req.Spec.Resources { + name := res.Resource + "." + res.Group + crd, err := r.getCRD(clusterName, name) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + if apierrors.IsNotFound(err) { + conditions.MarkFalse( + req, + kubebindv1alpha1.APIServiceExportRequestConditionExportsReady, + "CRDNotFound", + conditionsapi.ConditionSeverityError, + "CustomResourceDefinition %s in the service provider cluster not found", + name, + ) + failure = true + break + } + + if _, err := r.getServiceExport(clusterName, req.Namespace, name); err != nil && !apierrors.IsNotFound(err) { + return err + } else if err == nil { + continue + } + + exportSpec, err := helpers.CRDToServiceExport(crd) + if err != nil { + conditions.MarkFalse( + req, + kubebindv1alpha1.APIServiceExportRequestConditionExportsReady, + "CRDInvalid", + conditionsapi.ConditionSeverityError, + "CustomResourceDefinition %s cannot be converted to a APIServiceExport: %v", + name, + err, + ) + failure = true + break + } + hash := helpers.APIServiceExportCRDSpecHash(exportSpec) + export := &kubebindv1alpha1.APIServiceExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: crd.Name, + Namespace: req.Namespace, + Annotations: map[string]string{ + kubebindv1alpha1.SourceSpecHashAnnotationKey: hash, + }, + }, + Spec: kubebindv1alpha1.APIServiceExportSpec{ + APIServiceExportCRDSpec: *exportSpec, + InformerScope: r.informerScope, + }, + } + + logger.V(1).Info("Creating APIServiceExport", "name", export.Name, "namespace", export.Namespace) + if _, err = r.createServiceExport(ctx, clusterName, export); err != nil { + return err + } + } + + if !failure { + conditions.MarkTrue(req, kubebindv1alpha1.APIServiceExportRequestConditionExportsReady) + req.Status.Phase = kubebindv1alpha1.APIServiceExportRequestPhaseSucceeded + return nil + } + + if time.Since(req.CreationTimestamp.Time) > time.Minute { + req.Status.Phase = kubebindv1alpha1.APIServiceExportRequestPhaseFailed + req.Status.TerminalMessage = conditions.GetMessage(req, kubebindv1alpha1.APIServiceExportRequestConditionExportsReady) + } + + return nil + } + + if time.Since(req.CreationTimestamp.Time) > 10*time.Minute { + logger.Info("Deleting service binding request %s/%s", req.Namespace, req.Name, "reason", "timeout", "age", time.Since(req.CreationTimestamp.Time)) + return r.deleteServiceExportRequest(ctx, clusterName, req.Namespace, req.Name) + } + + return nil +} diff --git a/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go new file mode 100644 index 000000000..70a2b1e06 --- /dev/null +++ b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go @@ -0,0 +1,418 @@ +/* +Copyright 2022 The Kube Bind 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 servicenamespace + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/google/go-cmp/cmp" + kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" + coreinformers "github.com/kcp-dev/client-go/informers/core/v1" + rbacinformers "github.com/kcp-dev/client-go/informers/rbac/v1" + kubernetesclient "github.com/kcp-dev/client-go/kubernetes" + corelisters "github.com/kcp-dev/client-go/listers/core/v1" + rbaclisters "github.com/kcp-dev/client-go/listers/rbac/v1" + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/committer" + "github.com/kube-bind/kube-bind/pkg/indexers" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + bindclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned/cluster" + bindinformers "github.com/kube-bind/kube-bind/sdk/kcp/informers/externalversions/kubebind/v1alpha1" + bindlisters "github.com/kube-bind/kube-bind/sdk/kcp/listers/kubebind/v1alpha1" +) + +const ( + controllerName = "kube-bind-example-backend-servicenamespace" +) + +// NewController returns a new controller for ServiceNamespaces. +func NewController( + config *rest.Config, + scope kubebindv1alpha1.Scope, + serviceNamespaceInformer bindinformers.APIServiceNamespaceClusterInformer, + clusterBindingInformer bindinformers.ClusterBindingClusterInformer, + serviceExportInformer bindinformers.APIServiceExportClusterInformer, + namespaceInformer coreinformers.NamespaceClusterInformer, + roleInformer rbacinformers.RoleClusterInformer, + roleBindingInformer rbacinformers.RoleBindingClusterInformer, +) (*Controller, error) { + queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) + + logger := klog.Background().WithValues("Controller", controllerName) + + config = rest.CopyConfig(config) + config = rest.AddUserAgent(config, controllerName) + + bindClient, err := bindclient.NewForConfig(config) + if err != nil { + return nil, err + } + kubeClient, err := kubernetesclient.NewForConfig(config) + if err != nil { + return nil, err + } + + c := &Controller{ + queue: queue, + + bindClient: bindClient, + kubeClient: kubeClient, + + serviceNamespaceLister: serviceNamespaceInformer.Lister(), + serviceNamespaceIndexer: serviceNamespaceInformer.Informer().GetIndexer(), + + clusterBindingLister: clusterBindingInformer.Lister(), + clusterBindingIndexer: clusterBindingInformer.Informer().GetIndexer(), + + serviceExportLister: serviceExportInformer.Lister(), + serviceExportIndexer: serviceExportInformer.Informer().GetIndexer(), + + namespaceLister: namespaceInformer.Lister(), + namespaceIndexer: namespaceInformer.Informer().GetIndexer(), + + roleLister: roleInformer.Lister(), + roleIndexer: roleInformer.Informer().GetIndexer(), + + roleBindingLister: roleBindingInformer.Lister(), + roleBindingIndexer: roleBindingInformer.Informer().GetIndexer(), + + reconciler: reconciler{ + scope: scope, + + getNamespace: func(cluster logicalcluster.Name, name string) (*corev1.Namespace, error) { + return namespaceInformer.Lister().Cluster(cluster).Get(name) + }, + createNamespace: func(ctx context.Context, cluster logicalcluster.Path, ns *corev1.Namespace) (*corev1.Namespace, error) { + return kubeClient.CoreV1().Cluster(cluster).Namespaces().Create(ctx, ns, metav1.CreateOptions{}) + }, + deleteNamespace: func(ctx context.Context, cluster logicalcluster.Path, name string) error { + return kubeClient.CoreV1().Cluster(cluster).Namespaces().Delete(ctx, name, metav1.DeleteOptions{}) + }, + + getRoleBinding: func(cluster logicalcluster.Name, ns, name string) (*rbacv1.RoleBinding, error) { + return roleBindingInformer.Lister().Cluster(cluster).RoleBindings(ns).Get(name) + }, + createRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, crb *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) { + return kubeClient.RbacV1().Cluster(cluster).RoleBindings(crb.Namespace).Create(ctx, crb, metav1.CreateOptions{}) + }, + updateRoleBinding: func(ctx context.Context, cluster logicalcluster.Path, crb *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) { + return kubeClient.RbacV1().Cluster(cluster).RoleBindings(crb.Namespace).Update(ctx, crb, metav1.UpdateOptions{}) + }, + }, + } + + indexers.AddIfNotPresentOrDie(serviceNamespaceInformer.Informer().GetIndexer(), cache.Indexers{ + indexers.ServiceNamespaceByNamespace: indexers.IndexServiceNamespaceByNamespace, + }) + + namespaceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueNamespace(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueNamespace(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueNamespace(logger, obj) + }, + }) + + serviceNamespaceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceNamespace(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueServiceNamespace(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceNamespace(logger, obj) + }, + }) + + clusterBindingInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueClusterBinding(logger, obj) + }, + }) + + serviceExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + UpdateFunc: func(old, newObj interface{}) { + oldExport, ok := old.(*kubebindv1alpha1.APIServiceExport) + if !ok { + return + } + newExport, ok := old.(*kubebindv1alpha1.APIServiceExport) + if !ok { + return + } + if reflect.DeepEqual(oldExport.Spec, newExport.Spec) { + return + } + c.enqueueServiceExport(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceExport(logger, obj) + }, + }) + + return c, nil +} + +type Resource = committer.Resource[*kubebindv1alpha1.APIServiceNamespaceSpec, *kubebindv1alpha1.APIServiceNamespaceStatus] + +// Controller reconciles ServiceNamespaces by creating a Namespace for each, and deleting it if +// the APIServiceNamespace is deleted. +type Controller struct { + queue workqueue.RateLimitingInterface + + bindClient bindclient.ClusterInterface + kubeClient kubernetesclient.ClusterInterface + + namespaceLister corelisters.NamespaceClusterLister + namespaceIndexer cache.Indexer + + serviceNamespaceLister bindlisters.APIServiceNamespaceClusterLister + serviceNamespaceIndexer cache.Indexer + + clusterBindingLister bindlisters.ClusterBindingClusterLister + clusterBindingIndexer cache.Indexer + + serviceExportLister bindlisters.APIServiceExportClusterLister + serviceExportIndexer cache.Indexer + + roleLister rbaclisters.RoleClusterLister + roleIndexer cache.Indexer + + roleBindingLister rbaclisters.RoleBindingClusterLister + roleBindingIndexer cache.Indexer + + reconciler +} + +func (c *Controller) enqueueServiceNamespace(logger klog.Logger, obj interface{}) { + key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + logger.V(2).Info("queueing APIServiceNamespace", "key", key) + c.queue.Add(key) +} + +func (c *Controller) enqueueClusterBinding(logger klog.Logger, obj interface{}) { + cbKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + _, ns, _, err := kcpcache.SplitMetaClusterNamespaceKey(cbKey) + if err != nil { + runtime.HandleError(err) + return + } + + snss, err := c.serviceNamespaceIndexer.ByIndex(cache.NamespaceIndex, ns) + if err != nil { + runtime.HandleError(err) + return + } + logger.V(2).Info("queueing ServiceNamespaces", "namespace", ns, "number", len(snss), "reason", "ClusterBinding", "ClusterBindingKey", cbKey) + for _, sns := range snss { + key, err := cache.MetaNamespaceKeyFunc(sns) + if err != nil { + runtime.HandleError(err) + continue + } + c.queue.Add(key) + } +} + +func (c *Controller) enqueueServiceExport(logger klog.Logger, obj interface{}) { + seKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + _, ns, _, err := kcpcache.SplitMetaClusterNamespaceKey(seKey) + if err != nil { + runtime.HandleError(err) + return + } + + snss, err := c.serviceNamespaceIndexer.ByIndex(cache.NamespaceIndex, ns) + if err != nil { + runtime.HandleError(err) + return + } + logger.V(2).Info("queueing ServiceNamespaces", "namespace", ns, "number", len(snss), "reason", "APIServiceExport", "ServiceExportKey", seKey) + for _, sns := range snss { + key, err := kcpcache.MetaClusterNamespaceKeyFunc(sns) + if err != nil { + runtime.HandleError(err) + continue + } + c.queue.Add(key) + } +} + +func (c *Controller) enqueueNamespace(logger klog.Logger, obj interface{}) { + nsKey, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + sns, err := c.serviceNamespaceIndexer.ByIndex(indexers.ServiceNamespaceByNamespace, nsKey) + if err != nil { + runtime.HandleError(err) + return + } + for _, obj := range sns { + key, err := kcpcache.MetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + continue + } + logger.V(2).Info("queueing APIServiceNamespace", "key", key, "reason", "Namespace", "NamespaceKey", nsKey) + c.queue.Add(key) + } +} + +// Start starts the controller, which stops when ctx.Done() is closed. +func (c *Controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + defer c.queue.ShutDown() + + logger := klog.FromContext(ctx).WithValues("Controller", controllerName) + + logger.Info("Starting Controller") + defer logger.Info("Shutting down Controller") + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *Controller) startWorker(ctx context.Context) { + defer runtime.HandleCrash() + + for c.processNextWorkItem(ctx) { + } +} + +func (c *Controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k.(string) + + logger := klog.FromContext(ctx).WithValues("key", key) + ctx = klog.NewContext(ctx, logger) + logger.V(2).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q Controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *Controller) process(ctx context.Context, key string) error { + clusterName, snsNamespace, snsName, err := kcpcache.SplitMetaClusterNamespaceKey(key) + if err != nil { + return nil + } + nsName := clusterName.String() + "-" + snsNamespace + "-" + snsName + + obj, err := c.serviceNamespaceLister.Cluster(clusterName).APIServiceNamespaces(snsNamespace).Get(snsName) + if err != nil && !errors.IsNotFound(err) { + return err + } else if errors.IsNotFound(err) { + if err := c.deleteNamespace(ctx, clusterName.Path(), nsName); err != nil && !errors.IsNotFound(err) { + return err + } + return nil + } + + old := obj + obj = obj.DeepCopy() + + var errs []error + if err := c.reconcile(ctx, clusterName, obj); err != nil { + errs = append(errs, err) + } + + // Regardless of whether reconcile returned an error or not, always try to patch status if needed. Return the + // reconciliation error at the end. + + // If the object being reconciled changed as a result, update it. + objectMetaChanged := !equality.Semantic.DeepEqual(old.ObjectMeta, obj.ObjectMeta) + specChanged := !equality.Semantic.DeepEqual(old.Spec, obj.Spec) + statusChanged := !equality.Semantic.DeepEqual(old.Status, obj.Status) + + specOrObjectMetaChanged := specChanged || objectMetaChanged + + // Simultaneous updates of spec and status are never allowed. + if specOrObjectMetaChanged && statusChanged { + panic(fmt.Sprintf("programmer error: spec and status changed in same reconcile iteration. diff=%s", cmp.Diff(old, obj))) + } + + oldResource := &Resource{ObjectMeta: old.ObjectMeta, Spec: &old.Spec, Status: &old.Status} + newResource := &Resource{ObjectMeta: obj.ObjectMeta, Spec: &obj.Spec, Status: &obj.Status} + + patchBytes, subresources, err := committer.GeneratePatchAndSubResources(oldResource, newResource) + if err != nil { + errs = append(errs, err) + } + + if len(patchBytes) == 0 { + return nil + } + + _, err = c.bindClient.Cluster(clusterName.Path()).KubeBindV1alpha1().APIServiceNamespaces(snsNamespace).Patch(ctx, obj.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, subresources...) + return err +} diff --git a/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go new file mode 100644 index 000000000..3a7c2be78 --- /dev/null +++ b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go @@ -0,0 +1,125 @@ +/* +Copyright 2022 The Kube Bind 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 servicenamespace + +import ( + "context" + "fmt" + "reflect" + + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kuberesources "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes/resources" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" +) + +type reconciler struct { + scope kubebindv1alpha1.Scope + + getNamespace func(cluster logicalcluster.Name, name string) (*corev1.Namespace, error) + createNamespace func(ctx context.Context, cluster logicalcluster.Path, ns *corev1.Namespace) (*corev1.Namespace, error) + deleteNamespace func(ctx context.Context, cluster logicalcluster.Path, name string) error + + getRoleBinding func(cluster logicalcluster.Name, ns, name string) (*rbacv1.RoleBinding, error) + createRoleBinding func(ctx context.Context, cluster logicalcluster.Path, crb *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) + updateRoleBinding func(ctx context.Context, cluster logicalcluster.Path, cr *rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) +} + +func (c *reconciler) reconcile(ctx context.Context, clusterName logicalcluster.Name, sns *kubebindv1alpha1.APIServiceNamespace) error { + cluster := clusterName.Path() + + var ns *corev1.Namespace + nsName := sns.Namespace + "-" + sns.Name + if sns.Status.Namespace != "" { + nsName = sns.Status.Namespace + ns, _ = c.getNamespace(clusterName, nsName) // golint:errcheck + } + if ns == nil { + ns = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName, + Annotations: map[string]string{ + kubebindv1alpha1.APIServiceNamespaceAnnotationKey: sns.Namespace + "/" + sns.Name, + }, + }, + } + if _, err := c.createNamespace(ctx, cluster, ns); err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create namespace %q: %w", nsName, err) + } + } + + if c.scope == kubebindv1alpha1.NamespacedScope { + if err := c.ensureRBACRoleBinding(ctx, nsName, sns); err != nil { + return fmt.Errorf("failed to ensure RBAC: %w", err) + } + } + + if sns.Status.Namespace != nsName { + sns.Status.Namespace = nsName + } + + return nil +} + +func (c *reconciler) ensureRBACRoleBinding(ctx context.Context, ns string, sns *kubebindv1alpha1.APIServiceNamespace) error { + clusterName := logicalcluster.From(sns) + cluster := clusterName.Path() + objName := "kube-binder" + binding, err := c.getRoleBinding(clusterName, ns, objName) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get role binding %s/%s: %w", ns, objName, err) + } + + expected := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: objName, + Namespace: ns, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: sns.Namespace, + Name: kuberesources.ServiceAccountName, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "kube-binder-" + sns.Namespace, + APIGroup: "rbac.authorization.k8s.io", + }, + } + + if binding == nil { + if _, err := c.createRoleBinding(ctx, cluster, expected); err != nil { + return fmt.Errorf("failed to create role binding %s/%s: %w", ns, objName, err) + } + } else if !reflect.DeepEqual(binding.Subjects, expected.Subjects) || !reflect.DeepEqual(binding.RoleRef, expected.RoleRef) { + binding = binding.DeepCopy() + binding.Subjects = expected.Subjects + binding.RoleRef = expected.RoleRef + if _, err := c.updateRoleBinding(ctx, cluster, binding); err != nil { + return fmt.Errorf("failed to create role binding %s/%s: %w", ns, objName, err) + } + } + + return nil +} diff --git a/contrib/example-backend-kcp/cookie/cookie.go b/contrib/example-backend-kcp/cookie/cookie.go new file mode 100644 index 000000000..ebcee017e --- /dev/null +++ b/contrib/example-backend-kcp/cookie/cookie.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 The Kube Bind 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 cookie + +import ( + "fmt" + "net/http" + "time" +) + +func MakeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie { + return &http.Cookie{ + Name: name, + Value: value, + Path: "/", // TODO: make configurable + Domain: "", // TODO: add domain support + Expires: time.Now().Add(expiration), + HttpOnly: true, // TODO: make configurable + // setting to false so it works over http://localhost + Secure: false, // TODO: make configurable + SameSite: ParseSameSite(""), // TODO: make configurable + } +} + +func ParseSameSite(v string) http.SameSite { + switch v { + case "lax": + return http.SameSiteLaxMode + case "strict": + return http.SameSiteStrictMode + case "none": + return http.SameSiteNoneMode + case "": + return 0 + default: + panic(fmt.Sprintf("Invalid value for SameSite: %s", v)) + } +} diff --git a/contrib/example-backend-kcp/cookie/session.go b/contrib/example-backend-kcp/cookie/session.go new file mode 100644 index 000000000..6ac1a549f --- /dev/null +++ b/contrib/example-backend-kcp/cookie/session.go @@ -0,0 +1,113 @@ +/* +Copyright 2022 The Kube Bind 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 cookie + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "time" + + "github.com/pierrec/lz4" + "github.com/vmihailenco/msgpack/v4" +) + +type SessionState struct { + CreatedAt time.Time `msgpack:"ca,omitempty"` + ExpiresOn time.Time `msgpack:"eo,omitempty"` + + AccessToken string `msgpack:"at,omitempty"` + IDToken string `msgpack:"it,omitempty"` + RefreshToken string `msgpack:"rt,omitempty"` + + RedirectURL string `msgpack:"ru,omitempty"` + SessionID string `msgpack:"si,omitempty"` + ClusterID string `msgpack:"ci,omitempty"` +} + +func (s *SessionState) Encode() ([]byte, error) { + return msgpack.Marshal(s) +} + +func Decode(data string) (*SessionState, error) { + decoded, err := base64.RawURLEncoding.DecodeString(data) + if err != nil { + return nil, err + } + + var ss SessionState + err = msgpack.Unmarshal(decoded, &ss) + if err != nil { + return nil, fmt.Errorf("error unmarshalling data to session state: %w", err) + } + + return &ss, nil +} + +// lz4Compress compresses with LZ4 +// +// The Compress:Decompress ratio is 1:Many. LZ4 gives fastest decompress speeds +// at the expense of greater compression compared to other compression +// algorithms. +// nolint: unused +func lz4Compress(payload []byte) ([]byte, error) { + buf := new(bytes.Buffer) + zw := lz4.NewWriter(nil) + zw.Header = lz4.Header{ + BlockMaxSize: 65536, + CompressionLevel: 0, + } + zw.Reset(buf) + + reader := bytes.NewReader(payload) + _, err := io.Copy(zw, reader) + if err != nil { + return nil, fmt.Errorf("error copying lz4 stream to buffer: %w", err) + } + err = zw.Close() + if err != nil { + return nil, fmt.Errorf("error closing lz4 writer: %w", err) + } + + compressed, err := io.ReadAll(buf) + if err != nil { + return nil, fmt.Errorf("error reading lz4 buffer: %w", err) + } + + return compressed, nil +} + +// lz4Decompress decompresses with LZ4 +// nolint: unused +func lz4Decompress(compressed []byte) ([]byte, error) { + reader := bytes.NewReader(compressed) + buf := new(bytes.Buffer) + zr := lz4.NewReader(nil) + zr.Reset(reader) + _, err := io.Copy(buf, zr) + if err != nil { + return nil, fmt.Errorf("error copying lz4 stream to buffer: %w", err) + } + + payload, err := io.ReadAll(buf) + if err != nil { + return nil, fmt.Errorf("error reading lz4 buffer: %w", err) + } + + return payload, nil +} diff --git a/contrib/example-backend-kcp/deploy/01-clusterrole.yaml b/contrib/example-backend-kcp/deploy/01-clusterrole.yaml new file mode 100644 index 000000000..9bae036e1 --- /dev/null +++ b/contrib/example-backend-kcp/deploy/01-clusterrole.yaml @@ -0,0 +1,45 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-binder +rules: +- apiGroups: + - "kube-bind.io" + resources: + - "apiserviceexportrequests" + verbs: ["create","delete","patch","update","get","list","watch"] +- apiGroups: + - "" + resources: + - "namespaces" + verbs: ["get"] +- apiGroups: + - "" + resources: + - "secrets" + verbs: ["get", "watch", "list"] +- apiGroups: + - "kube-bind.io" + resources: + - "clusterbindings" + verbs: ["get", "watch", "list"] +- apiGroups: + - "kube-bind.io" + resources: + - "clusterbindings/status" + verbs: ["get","patch","update"] +- apiGroups: + - "kube-bind.io" + resources: + - "apiserviceexports" + verbs: ["get", "watch", "list"] +- apiGroups: + - "kube-bind.io" + resources: + - "apiserviceexports/status" + verbs: ["get","patch","update"] +- apiGroups: + - "kube-bind.io" + resources: + - "apiservicenamespaces" + verbs: ["create","delete","patch","update","get","list","watch"] diff --git a/contrib/example-backend-kcp/deploy/bootstrap.go b/contrib/example-backend-kcp/deploy/bootstrap.go new file mode 100644 index 000000000..7593433b8 --- /dev/null +++ b/contrib/example-backend-kcp/deploy/bootstrap.go @@ -0,0 +1,35 @@ +/* +Copyright 2022 The Kube Bind 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 deploy + +import ( + "context" + "embed" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + + "github.com/kube-bind/kube-bind/pkg/bootstrap" +) + +//go:embed *.yaml +var raw embed.FS + +func Bootstrap(ctx context.Context, discoveryClient discovery.DiscoveryInterface, dynamicClient dynamic.Interface, batteriesIncluded sets.Set[string]) error { + return bootstrap.Bootstrap(ctx, discoveryClient, dynamicClient, batteriesIncluded, raw) +} diff --git a/contrib/example-backend-kcp/docs/dex-config.yaml b/contrib/example-backend-kcp/docs/dex-config.yaml new file mode 100644 index 000000000..69fa8fb29 --- /dev/null +++ b/contrib/example-backend-kcp/docs/dex-config.yaml @@ -0,0 +1,34 @@ + +issuer: https://127.0.0.1:5556/dex +web: + https: 127.0.0.1:5556 + tlsCert: ../127.0.0.1.pem + tlsKey: ../127.0.0.1.pem +storage: + type: sqlite3 + config: + file: examples/dex.db +staticClients: + - id: kcp-dev + public: true + redirectURIs: + - http://localhost:8000 # oidc-login callback url + - https://127.0.0.1:8080/callback # kube-bind callback url + - https://127.0.0.1:6443/callback # kube-bind callback url + name: 'KCP App' + secret: Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg== + +# Let dex keep a list of passwords which can be used to login to dex. +enablePasswordDB: true + +# A static list of passwords to login the end user. By identifying here, dex +# won't look in its underlying storage for passwords. +# +# If this option isn't chosen users may be added through the gRPC API. +staticPasswords: +- email: "admin" + # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + groups: ["system:kcp:admin", "system:admin"] diff --git a/contrib/example-backend-kcp/go.mod b/contrib/example-backend-kcp/go.mod new file mode 100644 index 000000000..c9f967025 --- /dev/null +++ b/contrib/example-backend-kcp/go.mod @@ -0,0 +1,173 @@ +module github.com/kube-bind/kube-bind/contrib/example-backend-kcp + +go 1.23.4 + +// These are just for local development and example. You can remove them and point to the actual releases. +replace ( + github.com/kube-bind/kube-bind => ../../ + github.com/kube-bind/kube-bind/sdk/apis => ../../sdk/apis + github.com/kube-bind/kube-bind/sdk/kcp => ../../sdk/kcp +) + +// These replace follows kcp version. Currently it is 0.26.1. +replace ( + k8s.io/api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/apimachinery => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/cli-runtime => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/client-go => github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/cloud-provider => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/cluster-bootstrap => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/code-generator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/component-base => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/component-helpers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/cri-api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/cri-client => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/csi-translation-lib => github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/dynamic-resource-allocation => github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/endpointslice => github.com/kcp-dev/kubernetes/staging/src/k8s.io/endpointslice v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kms => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kube-aggregator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kube-controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kube-proxy => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kube-scheduler => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kubectl => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kubelet => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/kubernetes => github.com/kcp-dev/kubernetes v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/legacy-cloud-providers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/metrics => github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/mount-utils => github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/pod-security-admission => github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/sample-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/sample-cli-plugin => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-cli-plugin v0.0.0-20240918143026-ab5c3a6448cb + k8s.io/sample-controller => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-controller v0.0.0-20240918143026-ab5c3a6448cb +) + +require ( + github.com/coreos/go-oidc v2.2.1+incompatible + github.com/evanphx/json-patch v5.6.0+incompatible + github.com/google/go-cmp v0.6.0 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/securecookie v1.1.2 + github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb + github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a + github.com/kcp-dev/kcp v0.26.0 + github.com/kcp-dev/kcp/sdk v0.26.0 + github.com/kcp-dev/logicalcluster/v3 v3.0.5 + github.com/kube-bind/kube-bind v0.4.6 + github.com/kube-bind/kube-bind/sdk/apis v0.4.6 + github.com/kube-bind/kube-bind/sdk/kcp v0.4.6 + github.com/pierrec/lz4 v2.6.1+incompatible + github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace + github.com/vmihailenco/msgpack/v4 v4.3.13 + golang.org/x/oauth2 v0.24.0 + k8s.io/api v0.32.0 + k8s.io/apiextensions-apiserver v0.32.0 + k8s.io/apimachinery v0.32.0 + k8s.io/apiserver v0.32.0 + k8s.io/client-go v0.32.0 + k8s.io/code-generator v0.32.0 + k8s.io/component-base v0.32.0 + k8s.io/klog/v2 v2.130.1 + k8s.io/utils v0.0.0-20241210054802-24370beab758 + sigs.k8s.io/controller-tools v0.16.1 +) + +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/bombsimon/logrusr/v3 v3.1.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/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-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/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/golang/protobuf v1.5.4 // 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/imdario/mergo v0.3.12 // 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/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/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/pquerna/cachecontrol v0.1.0 // 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/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/vmihailenco/tagparser v0.1.1 // 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.31.0 // indirect + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/appengine v1.6.7 // 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/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // 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.5.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/contrib/example-backend-kcp/go.sum b/contrib/example-backend-kcp/go.sum new file mode 100644 index 000000000..e05d12ff1 --- /dev/null +++ b/contrib/example-backend-kcp/go.sum @@ -0,0 +1,372 @@ +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/bombsimon/logrusr/v3 v3.1.0 h1:zORbLM943D+hDMGgyjMhSAz/iDz86ZV72qaak/CA0zQ= +github.com/bombsimon/logrusr/v3 v3.1.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= +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-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +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 v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +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= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/cel-go v0.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= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/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/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +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/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb h1:W11F/dp6NdUnHeB0SrpyWLiifRosu1qaMJvdFGGLXc0= +github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb/go.mod h1:mEDD1K5BVUXJ4CP6wcJ0vZUf+7tbFMjkCFzBKsUNj18= +github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a h1:O9SNM3MqMlwoEAPSWxk/yw4JU211KpVsAFjTXWQcMEk= +github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a/go.mod h1:h5jC8rEbkyGUgV86+sgtMMcl950ooGzk+iLrQnbCR6o= +github.com/kcp-dev/kcp v0.26.0 h1:A0auaa7M7QxcF7/HL/ydkx4qa+puhr6IU6DFkL5rwtQ= +github.com/kcp-dev/kcp v0.26.0/go.mod h1:zjSxe+dtgsSgaDZGLCizSO0UxfASGEL3IezcLjw0iUQ= +github.com/kcp-dev/kcp/sdk v0.26.0 h1:QB0BiidlW4ERXGb6A/W93IBCo0g1zlE6T/bcOMyONX4= +github.com/kcp-dev/kcp/sdk v0.26.0/go.mod h1:XjabYVlKkpuRr1qATymS0gMTEjC6McuuwdoVGSar2fE= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20240918143026-ab5c3a6448cb h1:6vSaQJE2W9etXQFdHL9xWDUuzv8f8oTZ3tsXBL9UxMU= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:6X07YVZkpyT/6XVz4cwyYM2oYH3A3k2QR54H7JXMD90= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20240918143026-ab5c3a6448cb h1:ZoC/9f3PdmoNg5nezL/QrSP8S/lE+nvYsRHf6/bP450= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:8EZw4zqlExmz7lUTE/P7V0vdAyfiYL84i4ZUHY6qyrk= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20240918143026-ab5c3a6448cb h1:0obIpoEinm9CtUjjPKAZ1vL/c6OrlrlNzUJzjsw3zNE= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:5F0wbie5xX1jDEg5sk5dr+KF8rwFkYtZFHDhSF/UsG4= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20240918143026-ab5c3a6448cb h1:1J6FC8pvCrMeWdnM2bbMZuhHGeeLG+JvQ1uUPnwwlC8= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:EC5je+P5ix2QCV4zbwxlY8Zk5MJe4e1eKXxjCMd/7Eo= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20240918143026-ab5c3a6448cb h1:yuzQJrRaTpdylN62AQH7IsQth4gf4pSHX/MBcEEcuYw= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:l7HaB8VBHdNA72/wtAohDsemuLiVNdW6hx9lNB5J088= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20240918143026-ab5c3a6448cb h1:tXzwNHi7U49G65ldKyikILcq1Ymni9F8Dho325mrnWw= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:FeckrMB5SHLGBJWSRr79xheTG7il5LcGhzdx/v88Jus= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20240918143026-ab5c3a6448cb h1:5N/enNWDb3CJ5693LevtUb2sdn6l3FpwYl4H9QtehWQ= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:pgdjhgz6QeKjIVxzIYq4JFZ7VBJRutg/n5W9OKX33qA= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20240918143026-ab5c3a6448cb h1:R9utUkok9aychTtScqCgx3++UgkrFROQLwSviwYlVxU= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:gClzb5q8LLAagWlaL9S/rt8IcU3iY6gRARKN09DY4o8= +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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/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/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= +github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.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/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI= +github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= +go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= +go.etcd.io/etcd/api/v3 v3.5.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.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8= +go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg= +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.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M= +go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0= +go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA= +go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw= +go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok= +go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-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-20200301022130-244492dfa37a/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-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= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4= +k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-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-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/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/controller-tools v0.16.1 h1:gvIsZm+2aimFDIBiDKumR7EBkc+oLxljoUVfRbDI6RI= +sigs.k8s.io/controller-tools v0.16.1/go.mod h1:0I0xqjR65YTfoO12iR+mZR6s6UAVcUARgXRlsu0ljB0= +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.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +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/contrib/example-backend-kcp/hack/boilerplate/boilerplate.generatego.txt b/contrib/example-backend-kcp/hack/boilerplate/boilerplate.generatego.txt new file mode 100644 index 000000000..2d3aa5143 --- /dev/null +++ b/contrib/example-backend-kcp/hack/boilerplate/boilerplate.generatego.txt @@ -0,0 +1,16 @@ +/* +Copyright 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. +*/ + diff --git a/contrib/example-backend-kcp/hack/boilerplate/boilerplate.go.txt b/contrib/example-backend-kcp/hack/boilerplate/boilerplate.go.txt new file mode 100644 index 000000000..f6d7ed844 --- /dev/null +++ b/contrib/example-backend-kcp/hack/boilerplate/boilerplate.go.txt @@ -0,0 +1,16 @@ +/* +Copyright YEAR 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. +*/ + diff --git a/contrib/example-backend-kcp/hack/go-install.sh b/contrib/example-backend-kcp/hack/go-install.sh new file mode 100755 index 000000000..e265369d5 --- /dev/null +++ b/contrib/example-backend-kcp/hack/go-install.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# Copyright 2025 The Kube Bind 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/contrib/example-backend-kcp/hack/tools/.gitkeep b/contrib/example-backend-kcp/hack/tools/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/example-backend-kcp/hack/update-codegen-crds.sh b/contrib/example-backend-kcp/hack/update-codegen-crds.sh new file mode 100755 index 000000000..4502feda5 --- /dev/null +++ b/contrib/example-backend-kcp/hack/update-codegen-crds.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Copyright 2025 The Kube Bind 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) +if [ ! -d "$REPO_ROOT/kube-bind/.git" ]; then + git clone "$KUBE_BIND_REPO" --depth 1 --branch "$KUBE_BIND_VERSION" "$REPO_ROOT/kube-bind" +fi +#trap 'rm -rf $REPO_ROOT/kube-bind' EXIT + +KUBE_BIND_REPO_ROOT=$REPO_ROOT/kube-bind + +# Update generated CRD YAML +( + cp -r "${KUBE_BIND_REPO_ROOT}"/deploy/crd/*.yaml "${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/kube-bind/resources +) \ No newline at end of file diff --git a/contrib/example-backend-kcp/http/handler.go b/contrib/example-backend-kcp/http/handler.go new file mode 100644 index 000000000..39f999509 --- /dev/null +++ b/contrib/example-backend-kcp/http/handler.go @@ -0,0 +1,443 @@ +/* +Copyright 2022 The Kube Bind 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 http + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + htmltemplate "html/template" + "net/http" + "net/url" + "sort" + "strings" + "time" + + oidc "github.com/coreos/go-oidc" + "github.com/gorilla/mux" + "github.com/gorilla/securecookie" + apiextensionslisters "github.com/kcp-dev/client-go/apiextensions/listers/apiextensions/v1" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + componentbaseversion "k8s.io/component-base/version" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/cookie" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes/resources" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/template" + bindversion "github.com/kube-bind/kube-bind/pkg/version" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" +) + +var ( + resourcesTemplate = htmltemplate.Must(htmltemplate.New("resource").Parse(mustRead(template.Files.ReadFile, "resources.gohtml"))) +) + +// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en +var noCacheHeaders = map[string]string{ + "Expires": time.Unix(0, 0).Format(time.RFC1123), + "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0", + "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ +} + +type handler struct { + oidc *OIDCServiceProvider + + scope kubebindv1alpha1.Scope + oidcAuthorizeURL string + backendCallbackURL string + providerPrettyName string + testingAutoSelect string + + cookieEncryptionKey []byte + cookieSigningKey []byte + + client *http.Client + apiextensionsLister apiextensionslisters.CustomResourceDefinitionClusterLister + kubeManager *kubernetes.Manager +} + +func NewHandler( + provider *OIDCServiceProvider, + oidcAuthorizeURL, backendCallbackURL, providerPrettyName, testingAutoSelect string, + cookieSigningKey, cookieEncryptionKey []byte, + scope kubebindv1alpha1.Scope, + mgr *kubernetes.Manager, + apiextensionsLister apiextensionslisters.CustomResourceDefinitionClusterLister, +) (*handler, error) { + return &handler{ + oidc: provider, + oidcAuthorizeURL: oidcAuthorizeURL, + backendCallbackURL: backendCallbackURL, + providerPrettyName: providerPrettyName, + testingAutoSelect: testingAutoSelect, + scope: scope, + client: http.DefaultClient, + kubeManager: mgr, + apiextensionsLister: apiextensionsLister, + cookieSigningKey: cookieSigningKey, + cookieEncryptionKey: cookieEncryptionKey, + }, nil +} + +func (h *handler) AddRoutes(mux *mux.Router) { + mux.HandleFunc("/export", h.handleServiceExport).Methods("GET") + mux.HandleFunc("/resources", h.handleResources).Methods("GET") + mux.HandleFunc("/bind", h.handleBind).Methods("GET") + mux.HandleFunc("/authorize", h.handleAuthorize).Methods("GET") + mux.HandleFunc("/callback", h.handleCallback).Methods("GET") +} + +func (h *handler) handleServiceExport(w http.ResponseWriter, r *http.Request) { + logger := klog.FromContext(r.Context()).WithValues("method", r.Method, "url", r.URL.String()) + + oidcAuthorizeURL := h.oidcAuthorizeURL + if oidcAuthorizeURL == "" { + oidcAuthorizeURL = fmt.Sprintf("http://%s/authorize", r.Host) + } + + ver, err := bindversion.BinaryVersion(componentbaseversion.Get().GitVersion) + if err != nil { + logger.Error(err, "failed to parse version %q", componentbaseversion.Get().GitVersion) + ver = "v0.0.0" + } + + provider := &kubebindv1alpha1.BindingProvider{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha1.GroupVersion, + Kind: "BindingProvider", + }, + Version: ver, + ProviderPrettyName: "kcp-example-backend", + AuthenticationMethods: []kubebindv1alpha1.AuthenticationMethod{ + { + Method: "OAuth2CodeGrant", + OAuth2CodeGrant: &kubebindv1alpha1.OAuth2CodeGrant{ + AuthenticatedURL: oidcAuthorizeURL, + }, + }, + }, + } + + bs, err := json.Marshal(provider) + if err != nil { + logger.Error(err, "failed to marshal provider") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(bs) // nolint:errcheck +} + +// prepareNoCache prepares headers for preventing browser caching. +func prepareNoCache(w http.ResponseWriter) { + // Set NoCache headers + for k, v := range noCacheHeaders { + w.Header().Set(k, v) + } +} + +func (h *handler) handleAuthorize(w http.ResponseWriter, r *http.Request) { + logger := klog.FromContext(r.Context()).WithValues("method", r.Method, "url", r.URL.String()) + + scopes := []string{"openid", "profile", "email", "offline_access"} + code := &AuthCode{ + RedirectURL: r.URL.Query().Get("u"), + SessionID: r.URL.Query().Get("s"), + ClusterID: r.URL.Query().Get("c"), + } + if p := r.URL.Query().Get("p"); p != "" && code.RedirectURL == "" { + code.RedirectURL = fmt.Sprintf("http://localhost:%s/callback", p) + } + if code.RedirectURL == "" || code.SessionID == "" || code.ClusterID == "" { + logger.Error(errors.New("missing redirect url or session id or cluster id"), "failed to authorize") + http.Error(w, "missing redirect_url or session_id", http.StatusBadRequest) + return + } + + dataCode, err := json.Marshal(code) + if err != nil { + logger.Info("failed to marshal auth code", "error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + encoded := base64.URLEncoding.EncodeToString(dataCode) + authURL := h.oidc.OIDCProviderConfig(scopes).AuthCodeURL(encoded) + http.Redirect(w, r, authURL, http.StatusFound) +} + +func parseJWT(p string) ([]byte, error) { + parts := strings.Split(p, ".") + if len(parts) < 2 { + return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err) + } + return payload, nil +} + +// handleCallback handle the authorization redirect callback from OAuth2 auth flow. +func (h *handler) handleCallback(w http.ResponseWriter, r *http.Request) { + logger := klog.FromContext(r.Context()).WithValues("method", r.Method, "url", r.URL.String()) + ctx := r.Context() + + if errMsg := r.Form.Get("error"); errMsg != "" { + logger.Info("failed to authorize", "error", errMsg) + http.Error(w, errMsg+": "+r.Form.Get("error_description"), http.StatusBadRequest) + return + } + code := r.Form.Get("code") + if code == "" { + code = r.URL.Query().Get("code") + } + if code == "" { + logger.Info("no code in request", "error", "missing code") + http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest) + return + } + + state := r.Form.Get("state") + if state == "" { + state = r.URL.Query().Get("state") + } + decoded, err := base64.StdEncoding.DecodeString(state) + if err != nil { + logger.Info("failed to decode state", "error", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + authCode := &AuthCode{} + if err := json.Unmarshal(decoded, authCode); err != nil { + logger.Info("faile to unmarshal authCode", "error", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // TODO: sign state and verify that it is not faked by the oauth provider + ctx = oidc.ClientContext(ctx, h.oidc.GetHTTPClient()) + token, err := h.oidc.OIDCProviderConfig(nil).Exchange(ctx, code) + if err != nil { + logger.Info("failed to exchange token", "error", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + jwtStr, ok := token.Extra("id_token").(string) + if !ok { + logger.Info("failed to get id_token from token", "error", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + jwt, err := parseJWT(jwtStr) + if err != nil { + logger.Info("failed to parse jwt", "error", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + sessionCookie := cookie.SessionState{ + CreatedAt: time.Now(), + ExpiresOn: token.Expiry, + AccessToken: token.AccessToken, + IDToken: string(jwt), + RefreshToken: token.RefreshToken, + RedirectURL: authCode.RedirectURL, + SessionID: authCode.SessionID, + ClusterID: authCode.ClusterID, + } + + cookieName := "kube-bind-" + authCode.SessionID + s := securecookie.New(h.cookieSigningKey, h.cookieEncryptionKey) + encoded, err := s.Encode(cookieName, sessionCookie) + if err != nil { + logger.Info("failed to encode secure session cookie", "error", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + http.SetCookie(w, cookie.MakeCookie(r, cookieName, encoded, time.Duration(1)*time.Hour)) + http.Redirect(w, r, "/resources?s="+authCode.SessionID, http.StatusFound) +} + +func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { + logger := klog.FromContext(r.Context()).WithValues("method", r.Method, "url", r.URL.String()) + + prepareNoCache(w) + + if h.testingAutoSelect != "" { + parts := strings.SplitN(h.testingAutoSelect, ".", 2) + http.Redirect(w, r, "/resources/"+parts[0]+"/"+parts[1], http.StatusFound) + return + } + + labelSelector := labels.Set{ + resources.ExportedCRDsLabel: "true", + } + crds, err := h.apiextensionsLister.List(labelSelector.AsSelector()) + if err != nil { + logger.Error(err, "failed to list crds") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + sort.SliceStable(crds, func(i, j int) bool { + return crds[i].Name < crds[j].Name + }) + rightScopedCRDs := []*apiextensionsv1.CustomResourceDefinition{} + for _, crd := range crds { + if h.scope == kubebindv1alpha1.ClusterScope || crd.Spec.Scope == apiextensionsv1.NamespaceScoped { + rightScopedCRDs = append(rightScopedCRDs, crd) + } + } + + bs := bytes.Buffer{} + if err := resourcesTemplate.Execute(&bs, struct { + SessionID string + CRDs []*apiextensionsv1.CustomResourceDefinition + }{ + SessionID: r.URL.Query().Get("s"), + CRDs: rightScopedCRDs, + }); err != nil { + logger.Error(err, "failed to execute template") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + w.Write(bs.Bytes()) // nolint:errcheck +} + +func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { + logger := klog.FromContext(r.Context()).WithValues("method", r.Method, "url", r.URL.String()) + + prepareNoCache(w) + + cookieName := "kube-bind-" + r.URL.Query().Get("s") + ck, err := r.Cookie(cookieName) + if err != nil { + logger.Error(err, "failed to get session cookie") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + state := cookie.SessionState{} + s := securecookie.New(h.cookieSigningKey, h.cookieEncryptionKey) + if err := s.Decode(cookieName, ck.Value, &state); err != nil { + logger.Error(err, "failed to decode session cookie") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + var idToken struct { + Subject string `json:"sub"` + Issuer string `json:"iss"` + } + if err := json.Unmarshal([]byte(state.IDToken), &idToken); err != nil { + logger.Error(err, "failed to unmarshal id token") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + group := r.URL.Query().Get("group") + resource := r.URL.Query().Get("resource") + kfg, err := h.kubeManager.HandleResources(r.Context(), idToken.Subject+"#"+state.ClusterID, resource, group) + if err != nil { + logger.Error(err, "failed to handle resources") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + request := kubebindv1alpha1.APIServiceExportRequestResponse{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha1.SchemeGroupVersion.String(), + Kind: "APIServiceExportRequest", + }, + ObjectMeta: kubebindv1alpha1.NameObjectMeta{ + // this is good for one resource. If there are more (in the future), + // we need a better name heuristic. Note: it does not have to be unique. + // But pretty is better. + Name: resource + "." + group, + }, + Spec: kubebindv1alpha1.APIServiceExportRequestSpec{ + Resources: []kubebindv1alpha1.APIServiceExportRequestResource{ + {GroupResource: kubebindv1alpha1.GroupResource{Group: group, Resource: resource}}, + }, + }, + } + + // callback response + requestBytes, err := json.Marshal(&request) + if err != nil { + logger.Error(err, "failed to marshal request") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + response := kubebindv1alpha1.BindingResponse{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha1.SchemeGroupVersion.String(), + Kind: "BindingResponse", + }, + Authentication: kubebindv1alpha1.BindingResponseAuthentication{ + OAuth2CodeGrant: &kubebindv1alpha1.BindingResponseAuthenticationOAuth2CodeGrant{ + SessionID: state.SessionID, + ID: idToken.Issuer + "/" + idToken.Subject, + }, + }, + Kubeconfig: kfg, + Requests: []runtime.RawExtension{{Raw: requestBytes}}, + } + payload, err := json.Marshal(&response) + if err != nil { + logger.Error(err, "failed to marshal auth response") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + encoded := base64.URLEncoding.EncodeToString(payload) + + parsedAuthURL, err := url.Parse(state.RedirectURL) + if err != nil { + logger.Error(err, "failed to parse redirect url") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + values := parsedAuthURL.Query() + values.Add("response", encoded) + + parsedAuthURL.RawQuery = values.Encode() + + logger.V(1).Info("redirecting to auth callback", "url", state.RedirectURL+"?response=") + http.Redirect(w, r, parsedAuthURL.String(), http.StatusFound) +} + +func mustRead(f func(name string) ([]byte, error), name string) string { + bs, err := f(name) + if err != nil { + panic(err) + } + return string(bs) +} diff --git a/contrib/example-backend-kcp/http/oidc.go b/contrib/example-backend-kcp/http/oidc.go new file mode 100644 index 000000000..9df9584c1 --- /dev/null +++ b/contrib/example-backend-kcp/http/oidc.go @@ -0,0 +1,123 @@ +/* +Copyright 2022 The Kube Bind 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 http + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "os" + + oidc "github.com/coreos/go-oidc" + "golang.org/x/oauth2" +) + +// AuthCode is sent and received by to/from the OIDC provider. It's the state +// we can use to map the OIDC provider's response to the request from the client. +type AuthCode struct { + RedirectURL string `json:"redirectURL"` + SessionID string `json:"sid"` + ClusterID string `json:"cid"` +} + +type OIDCServiceProvider struct { + clientID string + clientSecret string + redirectURI string + issuerURL string + oidcCAFile string + + verifier *oidc.IDTokenVerifier + provider *oidc.Provider + + oidcClient *http.Client +} + +func NewOIDCServiceProvider(ctx context.Context, clientID, clientSecret, redirectURI, issuerURL, oidcCAFile string) (*OIDCServiceProvider, error) { + var oidcClient *http.Client + if oidcCAFile != "" { + var err error + oidcClient, err = httpClientForRootCAs(oidcCAFile) + if err != nil { + return nil, err + } + } + + ctx = oidc.ClientContext(ctx, oidcClient) + provider, err := oidc.NewProvider(ctx, issuerURL) + if err != nil { + return nil, err + } + + return &OIDCServiceProvider{ + clientID: clientID, + clientSecret: clientSecret, + redirectURI: redirectURI, + issuerURL: issuerURL, + oidcCAFile: oidcCAFile, + oidcClient: oidcClient, + provider: provider, + verifier: provider.Verifier(&oidc.Config{ClientID: clientID}), + }, nil +} + +func (o *OIDCServiceProvider) OIDCProviderConfig(scopes []string) *oauth2.Config { + return &oauth2.Config{ + ClientID: o.clientID, + ClientSecret: o.clientSecret, + Endpoint: o.provider.Endpoint(), + RedirectURL: o.redirectURI, + Scopes: scopes, + } +} + +func (o *OIDCServiceProvider) GetHTTPClient() *http.Client { + return o.oidcClient +} + +// httpClientForRootCAs return an HTTP client which trusts the provided root CAs. +func httpClientForRootCAs(oidcCAFile string) (*http.Client, error) { + caCert, err := os.ReadFile(oidcCAFile) + if err != nil { + return nil, err + } + + // Create a CA certificate pool and add the CA certificate to it + caCertPool := x509.NewCertPool() + if ok := caCertPool.AppendCertsFromPEM(caCert); !ok { + fmt.Println("Failed to append CA certificate") + return nil, err + } + + // Create a TLS configuration with the CA certificate pool + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + } + + // Create an HTTP transport with the TLS configuration + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + // Create an HTTP client with the custom transport + client := &http.Client{ + Transport: transport, + } + return client, nil +} diff --git a/contrib/example-backend-kcp/http/server.go b/contrib/example-backend-kcp/http/server.go new file mode 100644 index 000000000..258fa89d8 --- /dev/null +++ b/contrib/example-backend-kcp/http/server.go @@ -0,0 +1,90 @@ +/* +Copyright 2022 The Kube Bind 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 http + +import ( + "context" + "fmt" + "net" + "net/http" + "strconv" + + "github.com/gorilla/mux" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/options" +) + +type Server struct { + options *options.Serve + listener net.Listener + Router *mux.Router +} + +func NewServer(options *options.Serve) (*Server, error) { + server := &Server{ + options: options, + Router: mux.NewRouter(), + } + + if options.Listener == nil { + var err error + addr := options.ListenAddress + if options.ListenIP != "" { + addr = net.JoinHostPort(options.ListenIP, strconv.Itoa(options.ListenPort)) + } + server.listener, err = net.Listen("tcp", addr) + if err != nil { + return nil, err + } + } else { + server.listener = options.Listener + } + + return server, nil +} + +func (s *Server) Addr() net.Addr { + return s.listener.Addr() +} + +func (s *Server) Start(ctx context.Context) error { + server := &http.Server{ + Handler: s.Router, + } + go func() { + <-ctx.Done() + server.Close() // nolint:errcheck + }() + + go func() { + if s.options.KeyFile == "" { + fmt.Printf("Listening on port http://%s\n", s.Addr()) + err := server.Serve(s.listener) + if err != nil { + fmt.Printf("Error: %v\n", err) + } + } else { + fmt.Printf("Listening on port https://%s\n", s.Addr()) + err := server.ServeTLS(s.listener, s.options.CertFile, s.options.KeyFile) + if err != nil { + fmt.Printf("Error: %v\n", err) + } + } + }() + + return nil +} diff --git a/contrib/example-backend-kcp/kubernetes/indexers.go b/contrib/example-backend-kcp/kubernetes/indexers.go new file mode 100644 index 000000000..e4797aeae --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/indexers.go @@ -0,0 +1,40 @@ +/* +Copyright 2022 The Kube Bind 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 kubernetes + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes/resources" +) + +const ( + NamespacesByIdentity = "namespacesByIdentity" +) + +func IndexNamespacesByIdentity(obj interface{}) ([]string, error) { + ns, ok := obj.(*corev1.Namespace) + if !ok { + return nil, nil + } + + if id, found := ns.Annotations[resources.IdentityAnnotationKey]; found { + return []string{id}, nil + } + + return nil, nil +} diff --git a/contrib/example-backend-kcp/kubernetes/manager.go b/contrib/example-backend-kcp/kubernetes/manager.go new file mode 100644 index 000000000..582a42477 --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/manager.go @@ -0,0 +1,168 @@ +/* +Copyright 2022 The Kube Bind 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 kubernetes + +import ( + "context" + "fmt" + + corev1informers "github.com/kcp-dev/client-go/informers/core/v1" + kubeclient "github.com/kcp-dev/client-go/kubernetes" + corev1listers "github.com/kcp-dev/client-go/listers/core/v1" + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + + kuberesources "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes/resources" + "github.com/kube-bind/kube-bind/pkg/indexers" + bindclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned" + bindinformers "github.com/kube-bind/kube-bind/sdk/kcp/informers/externalversions" + bindlisters "github.com/kube-bind/kube-bind/sdk/kcp/listers/kubebind/v1alpha1" +) + +type Manager struct { + namespacePrefix string + providerPrettyName string + + clusterConfig *rest.Config + externalAddress string + externalCA []byte + externalTLSServerName string + + kubeClient kubeclient.ClusterInterface + bindClient bindclient.Interface + + namespaceLister corev1listers.NamespaceClusterLister + namespaceIndexer cache.Indexer + + exportLister bindlisters.APIServiceExportClusterLister + exportIndexer cache.Indexer +} + +func NewKubernetesManager( + namespacePrefix, providerPrettyName string, + config *rest.Config, + externalAddress string, + externalCA []byte, + externalTLSServerName string, + namespaceInformer corev1informers.NamespaceClusterInformer, + exportInformer bindinformers.SharedInformerFactory, +) (*Manager, error) { + config = rest.CopyConfig(config) + config = rest.AddUserAgent(config, "kube-bind-kcp-example-backend-kubernetes-manager") + + kubeClient, err := kubeclient.NewForConfig(config) + if err != nil { + return nil, err + } + bindClient, err := bindclient.NewForConfig(config) + if err != nil { + return nil, err + } + + m := &Manager{ + namespacePrefix: namespacePrefix, + providerPrettyName: providerPrettyName, + + clusterConfig: config, + externalAddress: externalAddress, + externalCA: externalCA, + externalTLSServerName: externalTLSServerName, + + kubeClient: kubeClient, + bindClient: bindClient, + + namespaceLister: namespaceInformer.Lister(), + namespaceIndexer: namespaceInformer.Informer().GetIndexer(), + + exportLister: exportInformer.KubeBind().V1alpha1().APIServiceExports().Lister(), + exportIndexer: exportInformer.KubeBind().V1alpha1().APIServiceBindings().Informer().GetIndexer(), + } + + indexers.AddIfNotPresentOrDie(m.namespaceIndexer, cache.Indexers{ + NamespacesByIdentity: IndexNamespacesByIdentity, + }) + + return m, nil +} + +func (m *Manager) HandleResources(ctx context.Context, identity, resource, group string) ([]byte, error) { + logger := klog.FromContext(ctx).WithValues("identity", identity, "resource", resource, "group", group) + ctx = klog.NewContext(ctx, logger) + + // TODO: Fix this + fakeCluster := logicalcluster.NewPath("fake-cluster") + + // try to find an existing namespace by annotation, or create a new one. + nss, err := m.namespaceIndexer.ByIndex(NamespacesByIdentity, identity) + if err != nil { + return nil, err + } + if len(nss) > 1 { + logger.Error(fmt.Errorf("found multiple namespaces for identity %q", identity), "found multiple namespaces for identity") + return nil, fmt.Errorf("found multiple namespaces for identity %q", identity) + } + var ns string + if len(nss) == 1 { + ns = nss[0].(*corev1.Namespace).Name + } else { + nsObj, err := kuberesources.CreateNamespace(ctx, m.kubeClient, fakeCluster, m.namespacePrefix, identity) + if err != nil { + return nil, err + } + logger.Info("Created namespace", "namespace", nsObj.Name) + ns = nsObj.Name + } + logger = logger.WithValues("namespace", ns) + ctx = klog.NewContext(ctx, logger) + + // first look for ClusterBinding to get old secret name + kubeconfigSecretName := kuberesources.KubeconfigSecretName + cb, err := m.bindClient.KubeBindV1alpha1().ClusterBindings(ns).Get(ctx, kuberesources.ClusterBindingName, metav1.GetOptions{}) + if err != nil && !errors.IsNotFound(err) { + return nil, err + } else if errors.IsNotFound(err) { + if err := kuberesources.CreateClusterBinding(ctx, m.bindClient, ns, "kubeconfig", m.providerPrettyName); err != nil { + return nil, err + } + } else { + logger.V(3).Info("Found existing ClusterBinding") + kubeconfigSecretName = cb.Spec.KubeconfigSecretRef.Name // reuse old name + } + + sa, err := kuberesources.CreateServiceAccount(ctx, m.kubeClient, fakeCluster, ns, kuberesources.ServiceAccountName) + if err != nil { + return nil, err + } + + saSecret, err := kuberesources.CreateSASecret(ctx, m.kubeClient, fakeCluster, ns, sa.Name) + if err != nil { + return nil, err + } + + kfgSecret, err := kuberesources.GenerateKubeconfig(ctx, m.kubeClient, m.clusterConfig, fakeCluster, m.externalAddress, m.externalCA, m.externalTLSServerName, saSecret.Name, ns, kubeconfigSecretName) + if err != nil { + return nil, err + } + + return kfgSecret.Data["kubeconfig"], nil +} diff --git a/contrib/example-backend-kcp/kubernetes/resources/cluster_binding.go b/contrib/example-backend-kcp/kubernetes/resources/cluster_binding.go new file mode 100644 index 000000000..8c78d072b --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/resources/cluster_binding.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 The Kube Bind 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" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + bindclient "github.com/kube-bind/kube-bind/sdk/kcp/clientset/versioned" +) + +func CreateClusterBinding(ctx context.Context, client bindclient.Interface, ns, secretName, providerPrettyName string) error { + logger := klog.FromContext(ctx) + + clusterBinding := &kubebindv1alpha1.ClusterBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: ClusterBindingName, + Namespace: ns, + }, + Spec: kubebindv1alpha1.ClusterBindingSpec{ + ProviderPrettyName: providerPrettyName, + KubeconfigSecretRef: kubebindv1alpha1.LocalSecretKeyRef{ + Name: secretName, + Key: "kubeconfig", + }, + }, + } + + logger.V(3).Info("Creating ClusterBinding") + _, err := client.KubeBindV1alpha1().ClusterBindings(ns).Create(ctx, clusterBinding, metav1.CreateOptions{}) + return err +} diff --git a/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go b/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go new file mode 100644 index 000000000..cd94ca8ba --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go @@ -0,0 +1,129 @@ +/* +Copyright 2022 The Kube Bind 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" + "fmt" + "time" + + "github.com/kcp-dev/client-go/kubernetes" + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" +) + +func GenerateKubeconfig(ctx context.Context, + client kubernetes.ClusterInterface, + clusterConfig *rest.Config, + cluster logicalcluster.Path, + externalAddress string, + externalCA []byte, + externalTLSServerName string, + saSecretName, ns, kubeconfigSecretName string, +) (*corev1.Secret, error) { + logger := klog.FromContext(ctx) + + if externalAddress == "" { + externalAddress = clusterConfig.Host + } + if externalCA == nil { + externalCA = clusterConfig.CAData + } + + var saSecret *corev1.Secret + logger.V(2).Info("Waiting for service account secret to be updated with a token", "name", saSecretName) + if err := wait.PollImmediateWithContext(ctx, 500*time.Millisecond, 10*time.Second, func(ctx context.Context) (done bool, err error) { + saSecret, err = client.CoreV1().Cluster(cluster).Secrets(ns).Get(ctx, saSecretName, v1.GetOptions{}) + if err != nil && !errors.IsNotFound(err) { + return false, err + } else if errors.IsNotFound(err) { + return false, nil + } + return saSecret.Data["token"] != nil && saSecret.Data["ca.crt"] != nil, nil + }); err != nil { + return nil, err + } + + cfg := clientcmdapi.Config{ + Clusters: map[string]*clientcmdapi.Cluster{ + "default": { + Server: externalAddress, + TLSServerName: externalTLSServerName, + CertificateAuthorityData: externalCA, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "default": { + Cluster: "default", + Namespace: ns, + AuthInfo: "default", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "default": { + Token: string(saSecret.Data["token"]), + }, + }, + CurrentContext: "default", + } + + kubeconfig, err := clientcmd.Write(cfg) + if err != nil { + return nil, fmt.Errorf("failed to encode kubeconfig: %w", err) + } + + kubeconfigSecret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: kubeconfigSecretName, + Namespace: ns, + }, + Data: map[string][]byte{ + "kubeconfig": kubeconfig, + }, + } + + logger.V(1).Info("Creating kubeconfig secret", "name", kubeconfigSecretName) + if secret, err := client.CoreV1().Cluster(cluster).Secrets(ns).Create(ctx, kubeconfigSecret, v1.CreateOptions{}); err != nil && !errors.IsAlreadyExists(err) { + return nil, err + } else if err == nil { + return secret, nil + } + + var updated *corev1.Secret + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + existing, err := client.CoreV1().Cluster(cluster).Secrets(ns).Get(ctx, kubeconfigSecret.Name, v1.GetOptions{}) + if err != nil { + return err + } + existing.Data = kubeconfigSecret.Data + logger.V(1).Info("Updating kubeconfig secret", "name", kubeconfigSecretName) + updated, err = client.CoreV1().Cluster(cluster).Secrets(ns).Update(ctx, existing, v1.UpdateOptions{}) + return err + }); err != nil { + return nil, err + } + return updated, nil +} diff --git a/contrib/example-backend-kcp/kubernetes/resources/namespace.go b/contrib/example-backend-kcp/kubernetes/resources/namespace.go new file mode 100644 index 000000000..71541406b --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/resources/namespace.go @@ -0,0 +1,62 @@ +/* +Copyright 2022 The Kube Bind 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" + "strings" + + "github.com/kcp-dev/client-go/kubernetes" + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + IdentityAnnotationKey = "example-backend.kube-bind.io/identity" +) + +func CreateNamespace(ctx context.Context, client kubernetes.ClusterInterface, cluster logicalcluster.Path, generateName, id string) (*corev1.Namespace, error) { + if !strings.HasSuffix(generateName, "-") { + generateName = generateName + "-" + } + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateName, + Annotations: map[string]string{ + IdentityAnnotationKey: id, + }, + }, + } + + ns, err := client.CoreV1().Cluster(cluster).Namespaces().Create(ctx, namespace, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return nil, err + } else if errors.IsAlreadyExists(err) { + ns, err := client.CoreV1().Cluster(cluster).Namespaces().Get(ctx, namespace.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if ns.Annotations[IdentityAnnotationKey] != id { + return nil, errors.NewAlreadyExists(corev1.Resource("namespace"), ns.Name) + } + } + + return ns, err +} diff --git a/contrib/example-backend-kcp/kubernetes/resources/rbac.go b/contrib/example-backend-kcp/kubernetes/resources/rbac.go new file mode 100644 index 000000000..95a822bc1 --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/resources/rbac.go @@ -0,0 +1,50 @@ +/* +Copyright 2022 The Kube Bind 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" + + kubeclient "github.com/kcp-dev/client-go/kubernetes" + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" +) + +func CreateServiceAccount(ctx context.Context, client kubeclient.ClusterInterface, cluster logicalcluster.Path, ns, name string) (*corev1.ServiceAccount, error) { + logger := klog.FromContext(ctx) + + sa, err := client.CoreV1().Cluster(cluster).ServiceAccounts(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + sa = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + } + + logger.Info("Creating service account", "name", sa.Name) + return client.CoreV1().Cluster(cluster).ServiceAccounts(ns).Create(ctx, sa, metav1.CreateOptions{}) + } + } + + return sa, err +} diff --git a/contrib/example-backend-kcp/kubernetes/resources/resources.go b/contrib/example-backend-kcp/kubernetes/resources/resources.go new file mode 100644 index 000000000..d54e02308 --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/resources/resources.go @@ -0,0 +1,28 @@ +/* +Copyright 2022 The Kube Bind 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 + +const ( + ServiceAccountTokenType = "kubernetes.io/service-account-token" + ServiceAccountTokenAnnotation = "kubernetes.io/service-account.name" + ServiceAccountName = "kube-binder" + KubeconfigSecretName = "kubeconfig" + ClusterBindingName = "cluster" + + //TODO(MQ): maybe think of a better label name. + ExportedCRDsLabel = "kube-bind.io/exported" +) diff --git a/contrib/example-backend-kcp/kubernetes/resources/secret.go b/contrib/example-backend-kcp/kubernetes/resources/secret.go new file mode 100644 index 000000000..fa769c97d --- /dev/null +++ b/contrib/example-backend-kcp/kubernetes/resources/secret.go @@ -0,0 +1,56 @@ +/* +Copyright 2022 The Kube Bind 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" + + "github.com/kcp-dev/client-go/kubernetes" + "github.com/kcp-dev/logicalcluster/v3" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" +) + +func CreateSASecret(ctx context.Context, client kubernetes.ClusterInterface, cluster logicalcluster.Path, ns, saName string) (*corev1.Secret, error) { + logger := klog.FromContext(ctx) + + secret, err := client.CoreV1().Cluster(cluster).Secrets(ns).Get(ctx, saName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + secret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: saName, + Namespace: ns, + Annotations: map[string]string{ + ServiceAccountTokenAnnotation: saName, + }, + }, + Type: ServiceAccountTokenType, + } + + logger.V(1).Info("Creating service account secret", "name", secret.Name) + return client.CoreV1().Cluster(cluster).Secrets(ns).Create(ctx, secret, metav1.CreateOptions{}) + } + + return nil, err + } + + return secret, nil +} diff --git a/contrib/example-backend-kcp/options/cookie.go b/contrib/example-backend-kcp/options/cookie.go new file mode 100644 index 000000000..874621f78 --- /dev/null +++ b/contrib/example-backend-kcp/options/cookie.go @@ -0,0 +1,74 @@ +/* +Copyright 2022 The Kube Bind 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 options + +import ( + "encoding/base64" + "fmt" + + "github.com/spf13/pflag" +) + +type Cookie struct { + SigningKey string + EncryptionKey string +} + +func NewCookie() *Cookie { + return &Cookie{} +} + +func (options *Cookie) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&options.SigningKey, "cookie-signing-key", options.SigningKey, "The key which is used to sign cookies, base64 encoded. Valid lengths are 32 or 64 bytes.") + fs.StringVar(&options.EncryptionKey, "cookie-encryption-key", options.EncryptionKey, "The key which is used to encrypt cookies, base64 encoded, optional. Valid lengths are 16, 24, or 32 bytes selecting AES-128, AES-192, or AES-256.") +} + +func (options *Cookie) Complete() error { + return nil +} + +func (options *Cookie) Validate() error { + if options.SigningKey == "" { + return fmt.Errorf("cookie signing key must not be empty") + } + + if err := checkKey(options.SigningKey, 32, 64); err != nil { + return fmt.Errorf("invalid signing key: %w", err) + } + + if options.EncryptionKey != "" { + if err := checkKey(options.SigningKey, 16, 24, 32); err != nil { + return fmt.Errorf("invalid encryption key: %w", err) + } + } + + return nil +} + +func checkKey(key string, validLengths ...int) error { + b, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return err + } + for _, validLength := range validLengths { + if len(b) == validLength { + return nil + } + } + + return fmt.Errorf("invalid key length: %d", len(b)) +} diff --git a/contrib/example-backend-kcp/options/oidc.go b/contrib/example-backend-kcp/options/oidc.go new file mode 100644 index 000000000..548d99b94 --- /dev/null +++ b/contrib/example-backend-kcp/options/oidc.go @@ -0,0 +1,73 @@ +/* +Copyright 2022 The Kube Bind 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 options + +import ( + "fmt" + "os" + + "github.com/spf13/pflag" +) + +type OIDC struct { + IssuerClientID string + IssuerClientSecret string + IssuerURL string + CallbackURL string + AuthorizeURL string + OIDCCAFile string +} + +func NewOIDC() *OIDC { + return &OIDC{} +} + +func (options *OIDC) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&options.IssuerClientID, "oidc-issuer-client-id", options.IssuerClientID, "Issuer client ID") + fs.StringVar(&options.IssuerClientSecret, "oidc-issuer-client-secret", options.IssuerClientSecret, "OpenID client secret") + fs.StringVar(&options.IssuerURL, "oidc-issuer-url", options.IssuerURL, "Callback URL for OpenID responses.") + fs.StringVar(&options.CallbackURL, "oidc-callback-url", options.CallbackURL, "OpenID callback URL") + fs.StringVar(&options.AuthorizeURL, "oidc-authorize-url", options.AuthorizeURL, "OpenID authorize URL") + fs.StringVar(&options.OIDCCAFile, "oidc-ca-file", options.OIDCCAFile, "OpenID CA file") +} + +func (options *OIDC) Complete() error { + return nil +} + +func (options *OIDC) Validate() error { + if options.IssuerClientID == "" { + return fmt.Errorf("OIDC issuer client ID cannot be empty") + } + if options.IssuerClientSecret == "" { + return fmt.Errorf("OIDC issuer client secret cannot be empty") + } + if options.IssuerURL == "" { + return fmt.Errorf("OIDC issuer URL cannot be empty") + } + if options.CallbackURL == "" { + return fmt.Errorf("OIDC callback URL cannot be empty") + } + + if options.OIDCCAFile != "" { + if _, err := os.Stat(options.OIDCCAFile); err != nil { + return fmt.Errorf("OIDC CA file %s does not exist", options.OIDCCAFile) + } + } + + return nil +} diff --git a/contrib/example-backend-kcp/options/options.go b/contrib/example-backend-kcp/options/options.go new file mode 100644 index 000000000..08a9c8b3d --- /dev/null +++ b/contrib/example-backend-kcp/options/options.go @@ -0,0 +1,196 @@ +/* +Copyright 2022 The Kube Bind 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 options + +import ( + "fmt" + "net/url" + "os" + "strings" + + "github.com/spf13/pflag" + + "k8s.io/component-base/logs" + logsv1 "k8s.io/component-base/logs/api/v1" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" +) + +type Options struct { + Logs *logs.Options + OIDC *OIDC + Cookie *Cookie + Serve *Serve + + ExtraOptions +} +type ExtraOptions struct { + KubeConfig string + + KubeBindWorkspacePath string + KubeBindAPIExportName string + + NamespacePrefix string + PrettyName string + ConsumerScope string + ExternalAddress string + ExternalCAFile string + ExternalCA []byte + TLSExternalServerName string + + TestingAutoSelect string + + // DevMode will use kubeconfig provided (assumes it is kcp-admin) + // and will create in-memory kubeconfig from legacy secret token. + // TODO(mjudeikis): Move fully to TokenRequest api so this behaviour is default. + DevMode bool +} + +type completedOptions struct { + Logs *logs.Options + OIDC *OIDC + Cookie *Cookie + Serve *Serve + + ExtraOptions +} + +type CompletedOptions struct { + *completedOptions +} + +func NewOptions() *Options { + // Default to -v=2 + logs := logs.NewOptions() + logs.Verbosity = logsv1.VerbosityLevel(2) + + return &Options{ + Logs: logs, + OIDC: NewOIDC(), + Cookie: NewCookie(), + Serve: NewServe(), + + ExtraOptions: ExtraOptions{ + NamespacePrefix: "cluster", + PrettyName: "KCP Backend", + ConsumerScope: string(kubebindv1alpha1.NamespacedScope), + }, + } +} + +func (options *Options) AddFlags(fs *pflag.FlagSet) { + logsv1.AddFlags(options.Logs, fs) + options.OIDC.AddFlags(fs) + options.Cookie.AddFlags(fs) + options.Serve.AddFlags(fs) + + fs.StringVar(&options.KubeConfig, "kubeconfig", options.KubeConfig, "path to a kubeconfig. Only required if out-of-cluster") + fs.StringVar(&options.KubeBindWorkspacePath, "workspace-path", options.KubeBindWorkspacePath, "path of the kubebind workspace, where master apiexport is located") + fs.StringVar(&options.KubeBindAPIExportName, "apiexport-name", options.KubeBindAPIExportName, "name of the apiexport to use") + + fs.StringVar(&options.NamespacePrefix, "namespace-prefix", options.NamespacePrefix, "The prefix to use for cluster namespaces") + fs.StringVar(&options.PrettyName, "pretty-name", options.PrettyName, "Pretty name for the backend") + fs.StringVar(&options.ConsumerScope, "consumer-scope", options.ConsumerScope, "How consumers access the service provider cluster. In Kubernetes, \"namespaced\" allows namespace isolation. In kcp, \"cluster\" allows workspace isolation, and with that allows cluster-scoped resources to bind and it is generally more performant.") + fs.StringVar(&options.ExternalAddress, "external-address", options.ExternalAddress, "The external address for the service provider cluster, including https:// and port. If not specified, service account's hosts are used.") + fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.") + fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.") + + fs.StringVar(&options.TestingAutoSelect, "testing-auto-select", options.TestingAutoSelect, ". that is automatically selected on th bind screen for testing") + fs.MarkHidden("testing-auto-select") // nolint: errcheck + + fs.BoolVar(&options.DevMode, "dev-mode", options.DevMode, "Use kubeconfig provided (assumes it is kcp-admin) and will create in-memory kubeconfig from legacy secret token") + fs.MarkHidden("dev-mode") // nolint: errcheck +} + +func (options *Options) Complete() (*CompletedOptions, error) { + if err := options.OIDC.Complete(); err != nil { + return nil, err + } + if err := options.Cookie.Complete(); err != nil { + return nil, err + } + if err := options.Serve.Complete(); err != nil { + return nil, err + } + + // normalize the scope + if strings.ToLower(options.ConsumerScope) == "namespaced" { + options.ConsumerScope = string(kubebindv1alpha1.NamespacedScope) + } + if strings.ToLower(options.ConsumerScope) == "cluster" { + options.ConsumerScope = string(kubebindv1alpha1.ClusterScope) + } + + if options.ExternalCAFile != "" && options.ExternalCA != nil { + return nil, fmt.Errorf("cannot specify both --external-ca-file and set ExternalCA") + } + if options.ExternalCAFile != "" { + ca, err := os.ReadFile(options.ExternalCAFile) + if err != nil { + return nil, fmt.Errorf("error reading external CA file: %v", err) + } + options.ExternalCA = ca + } + + return &CompletedOptions{ + completedOptions: &completedOptions{ + Logs: options.Logs, + OIDC: options.OIDC, + Cookie: options.Cookie, + Serve: options.Serve, + ExtraOptions: options.ExtraOptions, + }, + }, nil +} + +func (options *CompletedOptions) Validate() error { + if options.NamespacePrefix == "" { + return fmt.Errorf("namespace prefix cannot be empty") + } + if options.PrettyName == "" { + return fmt.Errorf("pretty name cannot be empty") + } + + if err := options.OIDC.Validate(); err != nil { + return err + } + if err := options.Cookie.Validate(); err != nil { + return err + } + if options.ConsumerScope != string(kubebindv1alpha1.NamespacedScope) && options.ConsumerScope != string(kubebindv1alpha1.ClusterScope) { + return fmt.Errorf("consumer scope must be either %q or %q", kubebindv1alpha1.NamespacedScope, kubebindv1alpha1.ClusterScope) + } + + if options.ExternalAddress != "" { + if !strings.HasPrefix(options.ExternalAddress, "https://") { + return fmt.Errorf("external hostname must start with https://") + } + _, err := url.Parse(options.ExternalAddress) + if err != nil { + return fmt.Errorf("invalid external hostname: %v", err) + } + } + + if options.KubeBindAPIExportName == "" { + return fmt.Errorf("apiexport-name cannot be empty") + } + if options.KubeBindWorkspacePath == "" { + return fmt.Errorf("workspace-path cannot be empty") + } + + return nil +} diff --git a/contrib/example-backend-kcp/options/serve.go b/contrib/example-backend-kcp/options/serve.go new file mode 100644 index 000000000..d15addb7a --- /dev/null +++ b/contrib/example-backend-kcp/options/serve.go @@ -0,0 +1,68 @@ +/* +Copyright 2022 The Kube Bind 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 options + +import ( + "fmt" + "net" + + "github.com/spf13/pflag" +) + +type Serve struct { + ListenIP string + ListenPort int + ListenAddress string + CertFile, KeyFile string + + // Listener is used to pre-wire a port zero listener for testing. + Listener net.Listener +} + +func NewServe() *Serve { + return &Serve{ + ListenAddress: "127.0.0.1:8080", + } +} + +func (options *Serve) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&options.ListenIP, "listen-ip", options.ListenIP, "The host IP where the backend is running") + fs.MarkDeprecated("listen-ip", "Use listen-address instead") // nolint: errcheck + fs.IntVar(&options.ListenPort, "listen-port", options.ListenPort, "The host port where the backend is running") + fs.MarkDeprecated("listen-port", "Use listen-address instead") // nolint: errcheck + fs.StringVar(&options.ListenAddress, "listen-address", options.ListenAddress, "The address where the backend should be listening on, defaults to 127.0.0.1:8080.") + fs.StringVar(&options.CertFile, "tls-cert-file", options.CertFile, "The TLS certificate file the webserver will use.") + fs.StringVar(&options.KeyFile, "tls-key-file", options.KeyFile, "The TLS private key file the webserver will use.") +} + +func (options *Serve) Complete() error { + return nil +} + +func (options *Serve) Validate() error { + if (options.ListenIP == "") != (options.ListenAddress == "") { + return fmt.Errorf("either listen-ip or listen-address must be provided") + } + if options.CertFile == "" && options.KeyFile != "" { + return fmt.Errorf("TLS key file cannot be specified without TLS cert file") + } + if options.CertFile != "" && options.KeyFile == "" { + return fmt.Errorf("TLS cert file cannot be specified without TLS key file") + } + + return nil +} diff --git a/contrib/example-backend-kcp/template/files.go b/contrib/example-backend-kcp/template/files.go new file mode 100644 index 000000000..96179b5d9 --- /dev/null +++ b/contrib/example-backend-kcp/template/files.go @@ -0,0 +1,24 @@ +/* +Copyright 2022 The Kube Bind 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 template + +import ( + "embed" +) + +//go:embed * +var Files embed.FS diff --git a/contrib/example-backend-kcp/template/icon/github-icon.svg b/contrib/example-backend-kcp/template/icon/github-icon.svg new file mode 100644 index 000000000..5d6072823 --- /dev/null +++ b/contrib/example-backend-kcp/template/icon/github-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/contrib/example-backend-kcp/template/icon/google-icon.svg b/contrib/example-backend-kcp/template/icon/google-icon.svg new file mode 100644 index 000000000..d667afdf5 --- /dev/null +++ b/contrib/example-backend-kcp/template/icon/google-icon.svg @@ -0,0 +1,16 @@ + + + + logo_googleg_48dp + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/contrib/example-backend-kcp/template/icon/microsoft-icon.svg b/contrib/example-backend-kcp/template/icon/microsoft-icon.svg new file mode 100644 index 000000000..739c395ab --- /dev/null +++ b/contrib/example-backend-kcp/template/icon/microsoft-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/contrib/example-backend-kcp/template/login.html b/contrib/example-backend-kcp/template/login.html new file mode 100644 index 000000000..7dd16d6b6 --- /dev/null +++ b/contrib/example-backend-kcp/template/login.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + login + + +
+
+
+ +
+
+
+

MangoDB

+

+ +
+
+
+
+ + diff --git a/contrib/example-backend-kcp/template/resources.gohtml b/contrib/example-backend-kcp/template/resources.gohtml new file mode 100644 index 000000000..d0d813ac3 --- /dev/null +++ b/contrib/example-backend-kcp/template/resources.gohtml @@ -0,0 +1,33 @@ + + + + + + + + + + + Resources + + +
+ {{$sid := .SessionID}}{{range .CRDs}} +
+

{{.Spec.Names.Singular}}

+
    +
  • Group: {{.Spec.Group}}
  • +
  • Scope: {{.Spec.Scope}}
  • +
+
+ Bind +
+
+ {{end}} +
+ + + + + + \ No newline at end of file diff --git a/contrib/example-backend-kcp/template/styles.css b/contrib/example-backend-kcp/template/styles.css new file mode 100644 index 000000000..743b6fe99 --- /dev/null +++ b/contrib/example-backend-kcp/template/styles.css @@ -0,0 +1,252 @@ + +@import url('https://fonts.googleapis.com/css?family=Montserrat:400,800'); + +* { + box-sizing: border-box; +} + +body { + background: #f6f5f7; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-family: 'Montserrat', sans-serif; + height: 100vh; + margin: -20px 0 50px; +} + +h1 { + font-weight: bold; + margin: 0; +} + +h2 { + text-align: center; +} + +p { + font-size: 14px; + font-weight: 100; + line-height: 20px; + letter-spacing: 0.5px; + margin: 20px 0 30px; +} + +span { + font-size: 12px; +} + +a { + color: #333; + font-size: 14px; + text-decoration: none; + margin: 15px 0; +} + +button { + border-radius: 20px; + border: 1px solid #138D75; + background-color: #138D75; + color: #FFFFFF; + font-size: 12px; + font-weight: bold; + padding: 12px 45px; + letter-spacing: 1px; + text-transform: uppercase; + transition: transform 80ms ease-in; +} + +button:active { + transform: scale(0.95); +} + +button:focus { + outline: none; +} + +button.ghost { + background-color: transparent; + border-color: #FFFFFF; +} + +form { + background-color: #FFFFFF; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0 50px; + height: 100%; + text-align: center; +} + +input { + background-color: #eee; + border: none; + padding: 12px 15px; + margin: 8px 0; + width: 100%; +} + +.container { + background-color: #fff; + border-radius: 10px; + box-shadow: 0 14px 28px rgba(0,0,0,0.25), + 0 10px 10px rgba(0,0,0,0.22); + position: relative; + overflow: hidden; + width: 768px; + max-width: 100%; + min-height: 480px; +} + +.form-container { + position: absolute; + top: 0; + height: 100%; + transition: all 0.6s ease-in-out; +} + +.sign-in-container { + left: 0; + width: 50%; + z-index: 2; +} + +.container.right-panel-active .sign-in-container { + transform: translateX(100%); +} + +.right-side-container { + left: 0; + width: 50%; + opacity: 0; + z-index: 1; +} + +.container.right-panel-active .right-side-container { + transform: translateX(100%); + opacity: 1; + z-index: 5; + animation: show 0.6s; +} + +@keyframes show { + 0%, 49.99% { + opacity: 0; + z-index: 1; + } + + 50%, 100% { + opacity: 1; + z-index: 5; + } +} + +.overlay-container { + position: absolute; + top: 0; + left: 50%; + width: 50%; + height: 100%; + overflow: hidden; + transition: transform 0.6s ease-in-out; + z-index: 100; +} + +.container.right-panel-active .overlay-container{ + transform: translateX(-100%); +} + +.overlay { + background: #0B5345; + /*background: -webkit-linear-gradient(to right, #FF4B2B, #FF416C);*/ + /*background: linear-gradient(to right, #FF4B2B, #FF416C);*/ + background-repeat: no-repeat; + background-size: cover; + background-position: 0 0; + color: #FFFFFF; + position: relative; + left: -100%; + height: 100%; + width: 200%; + transform: translateX(0); + transition: transform 0.6s ease-in-out; +} + +.container.right-panel-active .overlay { + transform: translateX(50%); +} + +.overlay-panel { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0 40px; + text-align: center; + top: 0; + height: 100%; + width: 50%; + transform: translateX(0); + transition: transform 0.6s ease-in-out; +} + +.overlay-left { + transform: translateX(-20%); +} + +.container.right-panel-active .overlay-left { + transform: translateX(0); +} + +.overlay-right { + right: 0; + transform: translateX(0); +} + +.container.right-panel-active .overlay-right { + transform: translateX(20%); +} + +.social-container { + margin: 20px 0; +} + +.social-container a { + border: 0px solid #DDDDDD; + border-radius: 50%; + display: inline-flex; + justify-content: center; + align-items: center; + margin: 0 5px; + height: 40px; + width: 40px; +} + +footer { + background-color: #222; + color: #fff; + font-size: 14px; + bottom: 0; + position: fixed; + left: 0; + right: 0; + text-align: center; + z-index: 999; +} + +footer p { + margin: 10px 0; +} + +footer i { + color: red; +} + +footer a { + color: #3c97bf; + text-decoration: none; +} \ No newline at end of file diff --git a/contrib/example-backend-kcp/tools.go b/contrib/example-backend-kcp/tools.go new file mode 100644 index 000000000..396998274 --- /dev/null +++ b/contrib/example-backend-kcp/tools.go @@ -0,0 +1,28 @@ +/* +Copyright 2025 The Kube Bind 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 ( + _ "github.com/kcp-dev/kcp/sdk/cmd/apigen" + + _ "k8s.io/code-generator/cmd/applyconfiguration-gen" + _ "k8s.io/code-generator/cmd/client-gen" + _ "k8s.io/code-generator/cmd/deepcopy-gen" + _ "k8s.io/code-generator/cmd/informer-gen" + _ "k8s.io/code-generator/cmd/lister-gen" + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" +) From dc577dc17e2640b92096b7f773dfec4c84b7cb9f Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Fri, 3 Jan 2025 22:28:09 +0200 Subject: [PATCH 2/8] docker-compose poc --- .ko.yaml | 4 + contrib/example-backend-kcp/Makefile | 9 +- .../docker-compose/cert-generator/Dockerfile | 35 ++++++ .../cert-generator/generate_certs.sh | 20 ++++ .../hack/docker-compose/dex/Dockerfile | 41 +++++++ .../hack/docker-compose/dex/kcp-config.yaml | 32 +++++ .../hack/docker-compose/docker-compose.yaml | 113 ++++++++++++++++++ .../hack/docker-compose/kcp/Dockerfile | 71 +++++++++++ .../hack/docker-compose/kube-bind/Dockerfile | 44 +++++++ .../example-backend-kcp/options/options.go | 5 + 10 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile create mode 100644 contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh create mode 100644 contrib/example-backend-kcp/hack/docker-compose/dex/Dockerfile create mode 100644 contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml create mode 100644 contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml create mode 100644 contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile create mode 100644 contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile diff --git a/.ko.yaml b/.ko.yaml index 95d92e3d5..98b59a6d4 100644 --- a/.ko.yaml +++ b/.ko.yaml @@ -10,3 +10,7 @@ builds: dir: ./cmd/example-backend ldflags: - "{{ .Env.LDFLAGS }}" +- id: example-backend-kcp + dir: ./contrib/example-backend-kcp/cmd/backend + ldflags: + - "{{ .Env.LDFLAGS }}" diff --git a/contrib/example-backend-kcp/Makefile b/contrib/example-backend-kcp/Makefile index 0ce4e04fc..2e571fa5d 100644 --- a/contrib/example-backend-kcp/Makefile +++ b/contrib/example-backend-kcp/Makefile @@ -120,4 +120,11 @@ codegen: crds .PHONY: imports imports: $(OPENSHIFT_GOIMPORTS) - $(OPENSHIFT_GOIMPORTS) -m github.com/kube-bind/kube-bind/contrib/example-backend-kcp \ No newline at end of file + $(OPENSHIFT_GOIMPORTS) -m github.com/kube-bind/kube-bind/contrib/example-backend-kcp + +run-docker-compose: + docker-compose -f ./hack/docker-compose/docker-compose.yaml up --build + +rm-docker-compose: + docker-compose -f ./hack/docker-compose/docker-compose.yaml down -v + docker volume rm docker-compose_kcp_config docker-compose_kube_bind_data diff --git a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile new file mode 100644 index 000000000..ecc5b2d27 --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile @@ -0,0 +1,35 @@ +# Stage 1: Build genkey tool +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git + +# Set GOBIN to install binaries to /go/bin +ENV GOBIN=/go/bin + +# Install genkey tool +RUN go install github.com/mjudeikis/genkey@24d5855234ab9622a9d3ac99dab8277b0d24e5c4 + +# Stage 2: Create the final image +FROM alpine:latest + +RUN apk add --no-cache ca-certificates bash + +WORKDIR /cert-generator + +# Copy the generate_certs.sh script +COPY generate_certs.sh . + +# Make the script executable +RUN chmod +x generate_certs.sh + +# Copy the genkey binary from the builder stage +COPY --from=builder /go/bin/genkey /usr/local/bin/genkey + +# Volume to share certificates +VOLUME /certs + +# Entry point to generate certificates +ENTRYPOINT ["./generate_certs.sh"] \ No newline at end of file diff --git a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh new file mode 100644 index 000000000..4ff9d43fc --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +CERT_DIR=/certs +CERT_NAME=dex + +echo "Generating TLS certificates using genkey..." + +# Generate certificates using genkey +genkey $CERT_NAME + +# Rename or move the generated files if necessary +# Assuming genkey outputs CERT_NAME.pem and CERT_NAME.key.pem in the current directory +mv ${CERT_NAME}.pem ${CERT_DIR}/${CERT_NAME}.pem +mv ${CERT_NAME}.key ${CERT_DIR}/${CERT_NAME}.key +mv ${CERT_NAME}.crt ${CERT_DIR}/${CERT_NAME}.crt + +echo "Certificates generated successfully at ${CERT_DIR}/" + +sleep infinity \ No newline at end of file diff --git a/contrib/example-backend-kcp/hack/docker-compose/dex/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/dex/Dockerfile new file mode 100644 index 000000000..1b02a77e2 --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/dex/Dockerfile @@ -0,0 +1,41 @@ +# Stage 1: Build Dex +FROM golang:1.23.3-alpine3.20@sha256:c694a4d291a13a9f9d94933395673494fc2cc9d4777b85df3a7e70b3492d3574 AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git make alpine-sdk ca-certificates openssl clang lld + +# Clone the Dex repository +RUN git clone https://github.com/mjudeikis/dex.git -b mjudeikis/groups.support /app/dex --dept 1 + +WORKDIR /app/dex + +ENV CGO_ENABLED=1 + +RUN go mod download + +# Build Dex binary +RUN make build + +# Stage 2: Create the final image +FROM alpine:latest + +RUN apk add --no-cache ca-certificates bash + +WORKDIR /app + +# Copy Dex binary +COPY --from=builder /app/dex/bin/dex /usr/local/bin/dex + +# Create necessary directories +RUN mkdir -p /dex/config /certs /data + +# Copy Dex configuration +COPY ./kcp-config.yaml /dex/config/kcp-config.yaml + +# Expose Dex port +EXPOSE 5556 + +# Run Dex +CMD ["dex", "serve", "/dex/config/kcp-config.yaml"] \ No newline at end of file diff --git a/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml b/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml new file mode 100644 index 000000000..2d3670a27 --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml @@ -0,0 +1,32 @@ +issuer: https://dex:5556/dex +web: + https: 0.0.0.0:5556 + tlsCert: /certs/dex.pem + tlsKey: /certs/dex.pem +storage: + type: sqlite3 + config: + file: /data/dex.db +staticClients: + - id: kcp-dev + public: true + redirectURIs: + - http://localhost:8000 # oidc-login callback url + - https://127.0.0.1:8080/callback # kube-bind callback url + - https://127.0.0.1:6443/callback # kube-bind callback url + name: 'KCP App' + secret: "Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg==" + +# Enable password authentication +enablePasswordDB: true + +# Static user passwords +staticPasswords: + - email: "admin@example.com" + # bcrypt hash of the string "password": $2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + groups: + - "system:kcp:admin" + - "system:admin" \ No newline at end of file diff --git a/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml b/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml new file mode 100644 index 000000000..fab912575 --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml @@ -0,0 +1,113 @@ +services: + cert-generator: + build: + context: ./cert-generator + dockerfile: Dockerfile + volumes: + - certs:/certs + # Run the certificate generator once and exit + entrypoint: ["./generate_certs.sh"] + # Healthcheck to ensure certificates are generated + healthcheck: + test: ["CMD", "sh", "-c", "test -f /certs/dex.pem"] + interval: 5s + timeout: 2s + retries: 10 + # Ensure it runs before Dex and KCP + restart: "no" + + dex: + build: + context: ./dex + dockerfile: Dockerfile + ports: + - "5556:5556" # Exposes Dex on port 5556 + volumes: + - ./dex/kcp-config.yaml:/dex/config/kcp-config.yaml:ro # Mounts Dex config as read-only + - certs:/certs:ro # Mounts certificates as read-only + - dex_data:/data # Mounts Dex data directory for SQLite DB + command: dex serve /dex/config/kcp-config.yaml + depends_on: + cert-generator: + condition: service_healthy + + kcp: + build: + context: ./kcp + dockerfile: Dockerfile + ports: + - "6443:6443" # Exposes KCP on port 6443 + volumes: + - certs:/certs:ro # Mounts certificates as read-only + - kcp_data:/app # Mounts KCP data directory + command: > + kcp start + --oidc-issuer-url=https://dex:5556/dex + --oidc-client-id=kcp-dev + --oidc-groups-claim=groups + --oidc-ca-file=/certs/dex.pem + depends_on: + cert-generator: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "-s", "-k", "https://localhost:6443/healthz"] + interval: 5s + timeout: 2s + retries: 10 + # Ensure it runs before Dex and KCP + restart: "no" + + kube-bind-init: + build: + context: ./../../../../ + dockerfile: ./contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile + image: kube-bind-image + volumes: + - certs:/certs:ro # Mounts certificates as read-only + - kcp_data:/kcp/config # Mounts KCP configuration directory + command: > + kubebind-bootstrap init --kcp-kubeconfig=/kcp/config/.kcp/admin.kubeconfig + depends_on: + kcp: + condition: service_healthy + + kube-bind: + image: kube-bind-image + ports: + - "6444:6444" # Exposes Kube-Bind on port 6444 (adjust as needed) + volumes: + - certs:/certs:ro # Mounts certificates as read-only + - kcp_data:/kcp/config # Mounts KCP configuratißon directory + command: > + kubebind start + -v 4 + --tls-cert-file=/certs/dex.pem + --tls-key-file=/certs/127.0.0.1.key.pem + --listen-address=0.0.0.0:6444 + --oidc-issuer-client-secret=Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg== + --oidc-issuer-client-id=kcp-dev + --oidc-issuer-url=https://dex:5556/dex + --oidc-callback-url=https://127.0.0.1:6444/callback + --oidc-authorize-url=https://127.0.0.1:6444/authorize + --oidc-ca-file=/certs/dex.pem + --pretty-name="CorpAAA.com" + --namespace-prefix="kube-bind-" + --cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= + --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= + --workspace-path="root:kube-bind" + --apiexport-name="kube-bind.io" + --kubeconfig=/kcp/config/.kcp/admin.kubeconfig + --dev-mode + depends_on: + cert-generator: + condition: service_healthy + kcp: + condition: service_healthy + kube-bind-init: + condition: service_completed_successfully + +volumes: + dex_data: + certs: + kcp_data: + kube_bind_data: diff --git a/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile new file mode 100644 index 000000000..ea9aa282f --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile @@ -0,0 +1,71 @@ +# Stage 1: Build KCP +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git + +# Clone the KCP repository with a shallow clone +RUN git clone https://github.com/kcp-dev/kcp.git /app/kcp --depth 1 + +WORKDIR /app/kcp + +# Build KCP binary +RUN go build -o kcp ./cmd/kcp + +# Stage 2: Create the final image +FROM alpine:latest + +# Install necessary packages +RUN apk add --no-cache ca-certificates bash curl tar + +WORKDIR /app + +# Copy KCP binary +COPY --from=builder /app/kcp/kcp /usr/local/bin/kcp + +# Install kubectl +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + && chmod +x kubectl \ + && mv kubectl /usr/local/bin/kubectl + +# Install krew +ENV KREW=/root/.krew + +RUN set -x \ + && apk add --no-cache git \ + && curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew-linux_amd64.tar.gz" \ + && tar zxvf krew-linux_amd64.tar.gz \ + && ./krew-linux_amd64 install krew \ + && rm -rf krew-linux_amd64.tar.gz \ + && mv /root/.krew/bin/kubectl-krew /usr/local/bin/kubectl-krew + +# Update PATH for krew plugins +ENV PATH="${KREW}/bin:${PATH}" + +# Verify installations +RUN kubectl version --client && kubectl krew version + +# Add krew plugin index and install desired plugins +RUN kubectl krew index add kcp-dev https://github.com/kcp-dev/krew-index.git \ + && kubectl krew install kcp-dev/kcp \ + && kubectl krew install kcp-dev/ws \ + && kubectl krew install kcp-dev/create-workspace + +# (Optional) Pre-install common krew plugins +# RUN kubectl krew install ctx ns + +ENV KUBECONFIG=/app/.kcp/admin.kubeconfig + +# Set up the alias for kubectl +RUN echo "alias k=kubectl" >> /root/.bashrc + +# Ensure that bash is the default shell +SHELL ["/bin/bash", "-c"] + +# Expose KCP port +EXPOSE 6443 + +# Run KCP (command is overridden by docker-compose) +CMD ["kcp"] diff --git a/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile new file mode 100644 index 000000000..139275537 --- /dev/null +++ b/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile @@ -0,0 +1,44 @@ +# Stage 1: Build Kube-Bind +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git + +# Clone the kube-bind repository (replace with the correct repository if different) +COPY / ./ + +WORKDIR /app/contrib/example-backend-kcp + +RUN go mod download + +# Build Kube-Bind binary +RUN go build -o kubebind ./cmd/backend/ +RUN go build -o kubebind-bootstrap ./cmd/bootstrap/ + +# Stage 2: Create the final image +FROM alpine:latest + +# Install necessary packages +RUN apk add --no-cache ca-certificates bash curl tar + +WORKDIR /app + +# Copy Kube-Bind binary +COPY --from=builder /app/contrib/example-backend-kcp/kubebind /usr/local/bin/kubebind +COPY --from=builder /app/contrib/example-backend-kcp/kubebind-bootstrap /usr/local/bin/kubebind-bootstrap + +ENV PATH="${KREW}/bin:${PATH}" + +# Expose Kube-Bind port +EXPOSE 6444 + +# Set up the alias for kubectl (optional, similar to KCP) +RUN echo "alias k=kubectl" >> /root/.bashrc + +# Ensure that bash is the default shell +SHELL ["/bin/bash", "-c"] + +# Run Kube-Bind (command is overridden by docker-compose) +CMD ["kubebind"] \ No newline at end of file diff --git a/contrib/example-backend-kcp/options/options.go b/contrib/example-backend-kcp/options/options.go index 08a9c8b3d..c33479e5d 100644 --- a/contrib/example-backend-kcp/options/options.go +++ b/contrib/example-backend-kcp/options/options.go @@ -58,6 +58,8 @@ type ExtraOptions struct { // and will create in-memory kubeconfig from legacy secret token. // TODO(mjudeikis): Move fully to TokenRequest api so this behaviour is default. DevMode bool + // DevInit is used to determine if it should bootstrap itself. Required higher level of priviledged access. + DevInit bool } type completedOptions struct { @@ -114,6 +116,9 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&options.DevMode, "dev-mode", options.DevMode, "Use kubeconfig provided (assumes it is kcp-admin) and will create in-memory kubeconfig from legacy secret token") fs.MarkHidden("dev-mode") // nolint: errcheck + + fs.BoolVar(&options.DevInit, "dev-init", options.DevInit, "If true, will bootstrap itself. Required higher level of priviledged access") + fs.MarkHidden("dev-init") // nolint: errcheck } func (options *Options) Complete() (*CompletedOptions, error) { From 7fc7eba8e55e32fcdaf054b9ae5496ba798d1bc2 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Sat, 4 Jan 2025 16:27:52 +0200 Subject: [PATCH 3/8] nit updates --- contrib/example-backend-kcp/README.md | 28 ++++++++++++++- .../cert-generator/generate_certs.sh | 3 ++ .../hack/docker-compose/docker-compose.yaml | 2 +- .../hack/docker-compose/kcp/Dockerfile | 3 ++ .../hack/docker-compose/kube-bind/Dockerfile | 34 +++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/contrib/example-backend-kcp/README.md b/contrib/example-backend-kcp/README.md index 1c69a61cc..3eb6654e2 100644 --- a/contrib/example-backend-kcp/README.md +++ b/contrib/example-backend-kcp/README.md @@ -52,7 +52,33 @@ bin/backend start \ 3. Try example `mangodb` as consumer. -TODO(mjudeikis) +Exec init `kube-bind` backend: + +``` +docker exec -it docker-compose-kube-bind-1 sh + +kubectl ws create mangodb --enter +kubectl kcp bind apiexport root:kube-bind:kube-bind.io --name kube-bind.io +``` + +Create crd for mangodb: +``` +kubectl create -f https://raw.githubusercontent.com/kube-bind/kube-bind/refs/heads/main/deploy/examples/crd-mangodb.yaml +``` + +At this point you will need to restart kube-bind backend to get it running. +TODO(mjudeikis): Fix this. + +From outside create separete kind cluster to be consumer + +``` +kubectl bind --insecure-skip-tls-verify https://0.0.0.0:6444/export +``` + + + + + # Raodmap & limitations diff --git a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh index 4ff9d43fc..6120fbd7e 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh +++ b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh @@ -15,6 +15,9 @@ mv ${CERT_NAME}.pem ${CERT_DIR}/${CERT_NAME}.pem mv ${CERT_NAME}.key ${CERT_DIR}/${CERT_NAME}.key mv ${CERT_NAME}.crt ${CERT_DIR}/${CERT_NAME}.crt +chmod 644 ${CERT_DIR}/${CERT_NAME}.* + echo "Certificates generated successfully at ${CERT_DIR}/" + sleep infinity \ No newline at end of file diff --git a/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml b/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml index fab912575..4e52444b1 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml +++ b/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml @@ -82,7 +82,7 @@ services: kubebind start -v 4 --tls-cert-file=/certs/dex.pem - --tls-key-file=/certs/127.0.0.1.key.pem + --tls-key-file=/certs/dex.pem --listen-address=0.0.0.0:6444 --oidc-issuer-client-secret=Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg== --oidc-issuer-client-id=kcp-dev diff --git a/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile index ea9aa282f..f22150ae5 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile +++ b/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile @@ -53,6 +53,9 @@ RUN kubectl krew index add kcp-dev https://github.com/kcp-dev/krew-index.git \ && kubectl krew install kcp-dev/ws \ && kubectl krew install kcp-dev/create-workspace +RUN kubectl krew index add bind https://github.com/kube-bind/krew-index.git \ + && kubectl krew install bind/bind + # (Optional) Pre-install common krew plugins # RUN kubectl krew install ctx ns diff --git a/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile index 139275537..b12d042f6 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile +++ b/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile @@ -29,8 +29,42 @@ WORKDIR /app COPY --from=builder /app/contrib/example-backend-kcp/kubebind /usr/local/bin/kubebind COPY --from=builder /app/contrib/example-backend-kcp/kubebind-bootstrap /usr/local/bin/kubebind-bootstrap +# Install kubectl +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + && chmod +x kubectl \ + && mv kubectl /usr/local/bin/kubectl + +# Install krew +ENV KREW=/root/.krew + +RUN set -x \ + && apk add --no-cache git \ + && curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew-linux_amd64.tar.gz" \ + && tar zxvf krew-linux_amd64.tar.gz \ + && ./krew-linux_amd64 install krew \ + && rm -rf krew-linux_amd64.tar.gz \ + && mv /root/.krew/bin/kubectl-krew /usr/local/bin/kubectl-krew + +# Update PATH for krew plugins ENV PATH="${KREW}/bin:${PATH}" +# Verify installations +RUN kubectl version --client && kubectl krew version + +# Add krew plugin index and install desired plugins +RUN kubectl krew index add kcp-dev https://github.com/kcp-dev/krew-index.git \ + && kubectl krew install kcp-dev/kcp \ + && kubectl krew install kcp-dev/ws \ + && kubectl krew install kcp-dev/create-workspace + +RUN kubectl krew index add bind https://github.com/kube-bind/krew-index.git \ + && kubectl krew install bind/bind + +# (Optional) Pre-install common krew plugins +# RUN kubectl krew install ctx ns + +ENV KUBECONFIG=/kcp/config/.kcp/admin.kubeconfig + # Expose Kube-Bind port EXPOSE 6444 From a91a998b85bc60ae07ab8d86858b2d87cdf49fcb Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Sat, 4 Jan 2025 16:34:54 +0200 Subject: [PATCH 4/8] Fix local make build targets Signed-off-by: Mangirdas Judeikis On-behalf-of: SAP mangirdas.judeikis@sap.com --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 2171a228d..d741b2284 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ GO_INSTALL = ./hack/go-install.sh ROOT_DIR=$(abspath .) TOOLS_DIR=hack/tools +ROOT_DIR=$(abspath .) TOOLS_GOBIN_DIR := $(abspath $(TOOLS_DIR)) GOBIN_DIR=$(abspath ./bin ) PATH := $(GOBIN_DIR):$(TOOLS_GOBIN_DIR):$(PATH) From 07570ec03c5d550955a1f23b6cb2f53a09214537 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Sat, 4 Jan 2025 20:08:28 +0200 Subject: [PATCH 5/8] fix docker compose --- contrib/example-backend-kcp/backend/server.go | 1 + .../clusterworkspace-kube-bind-provider.yaml | 10 + .../config/kube-bind-provider/bootstrap.go | 197 ++++++++++++++++++ .../kube-bind-provider/resources/bootstrap.go | 48 +++++ .../kube-bind-provider/resources/crd.yaml | 40 ++++ .../example-backend-kcp/bootstrap/server.go | 14 +- .../hack/docker-compose/dex/kcp-config.yaml | 1 + .../hack/docker-compose/docker-compose.yaml | 4 +- 8 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind-provider.yaml create mode 100644 contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go create mode 100644 contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go create mode 100644 contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/crd.yaml diff --git a/contrib/example-backend-kcp/backend/server.go b/contrib/example-backend-kcp/backend/server.go index 4d141d935..4f9f73714 100644 --- a/contrib/example-backend-kcp/backend/server.go +++ b/contrib/example-backend-kcp/backend/server.go @@ -75,6 +75,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) { config.Options.OIDC.IssuerClientSecret, callback, config.Options.OIDC.IssuerURL, + config.Options.OIDC.ExternalIssuerURL, config.Options.OIDC.OIDCCAFile, ) if err != nil { diff --git a/contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind-provider.yaml b/contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind-provider.yaml new file mode 100644 index 000000000..b84139bd8 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/config/clusterworkspace-kube-bind-provider.yaml @@ -0,0 +1,10 @@ +apiVersion: tenancy.kcp.io/v1alpha1 +kind: Workspace +metadata: + name: kube-bind-provider + annotations: + bootstrap.kcp.io/create-only: "true" +spec: + type: + name: organization + path: root diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go new file mode 100644 index 000000000..e852effa9 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go @@ -0,0 +1,197 @@ +/* +Copyright 2025 The Kube Bind 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 kubebindprovider + +import ( + "context" + "time" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + kcpdynamic "github.com/kcp-dev/client-go/dynamic" + apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpclient "github.com/kcp-dev/kcp/sdk/client/clientset/versioned" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + "github.com/kcp-dev/logicalcluster/v3" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" + + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources" +) + +var ( + // RootClusterName is the workspace to host common APIs. + RootClusterName = logicalcluster.NewPath("root:kube-bind-provider") +) + +// 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, + kcpClientSet kcpclientset.ClusterInterface, + apiExtensionClusterClient kcpapiextensionsclientset.ClusterInterface, + dynamicClusterClient kcpdynamic.ClusterInterface, + batteriesIncluded sets.Set[string], +) error { + kcpClient := kcpClientSet.Cluster(RootClusterName) + + computeDiscoveryClient := apiExtensionClusterClient.Cluster(RootClusterName).Discovery() + computeDynamicClient := dynamicClusterClient.Cluster(RootClusterName) + + crdClient := apiExtensionClusterClient.ApiextensionsV1().Cluster(RootClusterName).CustomResourceDefinitions() + + err := bindAPIExport(ctx, kcpClient, "kube-bind.io") + if err != nil { + return err + } + + time.Sleep(10 * time.Second) + + return resources.Bootstrap(ctx, kcpClientSet, computeDiscoveryClient, computeDynamicClient, crdClient, batteriesIncluded) +} + +func bindAPIExport(ctx context.Context, kcpClient kcpclient.Interface, exportName string) error { + logger := klog.FromContext(ctx) + + binding := &apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: exportName, + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.BindingReference{ + Export: &apisv1alpha1.ExportBindingReference{ + Path: "root:kube-bind", + Name: exportName, + }, + }, + }, + } + + binding.Spec.PermissionClaims = []apisv1alpha1.AcceptablePermissionClaim{ + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "rbac.authorization.k8s.io", + Resource: "clusterrolebindings", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "rbac.authorization.k8s.io", + Resource: "clusterroles", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "", + Resource: "serviceaccounts", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "", + Resource: "configmaps", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "", + Resource: "secrets", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: apisv1alpha1.SchemeGroupVersion.Group, + Resource: "apiexports", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + All: true, + GroupResource: apisv1alpha1.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + }, + State: apisv1alpha1.ClaimAccepted, + }, + } + + _, err := kcpClient.ApisV1alpha1().APIBindings().Create(ctx, binding, metav1.CreateOptions{}) + if err == nil { + return nil + } + if !apierrors.IsAlreadyExists(err) { + return err + } + + if err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) { + existing, err := kcpClient.ApisV1alpha1().APIBindings().Get(ctx, exportName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "error getting APIBinding", "name", exportName) + // Always keep trying. Don't ever return an error out of this function. + return false, nil + } + + logger.V(2).Info("Updating API binding") + existing.Spec = binding.Spec + + _, err = kcpClient.ApisV1alpha1().APIBindings().Update(ctx, existing, metav1.UpdateOptions{}) + if err == nil { + return true, nil + } + if apierrors.IsConflict(err) { + logger.V(2).Info("API binding update conflict, retrying") + return false, nil + } + + logger.Error(err, "error updating APIBinding") + // Always keep trying. Don't ever return an error out of this function. + return false, nil + }); err != nil { + return err + } + + return nil +} diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go new file mode 100644 index 000000000..a518f3393 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 The Kube Bind 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" + + confighelpers "github.com/kcp-dev/kcp/config/helpers" + kcpclientcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" +) + +//go:embed *.yaml +var KubeFS 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, + kcpClient kcpclientcluster.ClusterInterface, + discoveryClient discovery.DiscoveryInterface, + dynamicClient dynamic.Interface, + crdClient apiextensionsv1.CustomResourceDefinitionInterface, + batteriesIncluded sets.Set[string], +) error { + // create resources in core cluster + return confighelpers.Bootstrap(ctx, discoveryClient, dynamicClient, batteriesIncluded, KubeFS, confighelpers.ReplaceOption()) +} diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/crd.yaml b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/crd.yaml new file mode 100644 index 000000000..d6a62b688 --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/crd.yaml @@ -0,0 +1,40 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: mangodbs.mangodb.com + labels: + kube-bind.io/exported: "true" +spec: + group: mangodb.com + names: + kind: MangoDB + listKind: MangoDBList + plural: mangodbs + singular: mangodb + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + tier: + type: string + enum: + - Dedicated + - Shared + default: Shared + status: + type: object + properties: + phase: + type: string + required: + - spec + subresources: + status: {} diff --git a/contrib/example-backend-kcp/bootstrap/server.go b/contrib/example-backend-kcp/bootstrap/server.go index b6d9b193e..91a746fa2 100644 --- a/contrib/example-backend-kcp/bootstrap/server.go +++ b/contrib/example-backend-kcp/bootstrap/server.go @@ -25,6 +25,7 @@ import ( bootstrapconfig "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/config" bootstrapcore "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/core" bootstrapkubebind "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/kube-bind" + bootstrapkubebindprovider "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider" ) type Server struct { @@ -72,7 +73,18 @@ func (s *Server) Start(ctx context.Context) error { s.Config.DynamicClusterClient, fakeBatteries, ); err != nil { - logger.Error(err, "failed to bootstrap core workspace") + logger.Error(err, "failed to bootstrap workspace") + return nil // don't klog.Fatal. This only happens when context is cancelled. + } + + if err := bootstrapkubebindprovider.Bootstrap( + ctx, + s.Config.KcpClusterClient, + s.Config.ApiextensionsClient, + s.Config.DynamicClusterClient, + fakeBatteries, + ); err != nil { + logger.Error(err, "failed to bootstrap provider workspace") return nil // don't klog.Fatal. This only happens when context is cancelled. } diff --git a/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml b/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml index 2d3670a27..d86afecb1 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml +++ b/contrib/example-backend-kcp/hack/docker-compose/dex/kcp-config.yaml @@ -14,6 +14,7 @@ staticClients: - http://localhost:8000 # oidc-login callback url - https://127.0.0.1:8080/callback # kube-bind callback url - https://127.0.0.1:6443/callback # kube-bind callback url + - https://localhost:6444/callback # kube-bind callback url name: 'KCP App' secret: "Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg==" diff --git a/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml b/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml index 4e52444b1..3827c4c08 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml +++ b/contrib/example-backend-kcp/hack/docker-compose/docker-compose.yaml @@ -87,8 +87,8 @@ services: --oidc-issuer-client-secret=Z2Fyc2lha2FsYmlzdmFuZGVuekWplCg== --oidc-issuer-client-id=kcp-dev --oidc-issuer-url=https://dex:5556/dex - --oidc-callback-url=https://127.0.0.1:6444/callback - --oidc-authorize-url=https://127.0.0.1:6444/authorize + --oidc-callback-url=https://localhost:6444/callback + --oidc-authorize-url=https://localhost:6444/authorize --oidc-ca-file=/certs/dex.pem --pretty-name="CorpAAA.com" --namespace-prefix="kube-bind-" From 33e7137b9f372fb6a9677e35b69e1b5d5a8a363a Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Wed, 6 Aug 2025 10:46:28 +0300 Subject: [PATCH 6/8] sdk --- .github/workflows/unit-runtime.yaml | 40 +++ Makefile | 2 +- contrib/example-backend-kcp/127.0.0.1.crt | Bin 0 -> 776 bytes contrib/example-backend-kcp/127.0.0.1.key | Bin 0 -> 1193 bytes contrib/example-backend-kcp/127.0.0.1.pem | 47 +++ contrib/example-backend-kcp/backend/config.go | 1 - contrib/example-backend-kcp/backend/server.go | 10 +- .../example-backend-kcp/bootstrap/config.go | 1 - .../bootstrap/config/config/bootstrap.go | 1 - .../bootstrap/config/core/bootstrap.go | 1 - .../config/core/resources/bootstrap.go | 1 - .../config/kube-bind-provider/bootstrap.go | 1 - .../resources/apiservicebindings.yaml | 9 + .../kube-bind-provider/resources/bootstrap.go | 1 - .../bootstrap/options/options.go | 1 - .../example-backend-kcp/cmd/backend/main.go | 1 - .../example-backend-kcp/cmd/bootstrap/main.go | 1 - .../committer/committer.go | 1 - .../config/kube-bind/bootstrap.go | 1 - .../config/kube-bind/resources/bootstrap.go | 1 - .../clusterbinding_controller.go | 1 - .../clusterbinding_reconcile.go | 1 - .../serviceexport/serviceexport_controller.go | 1 - .../serviceexport/serviceexport_reconcile.go | 3 +- .../serviceexportrequest_controller.go | 1 - .../serviceexportrequest_reconcile.go | 3 +- .../servicenamespace_controller.go | 1 - .../servicenamespace_reconcile.go | 1 - contrib/example-backend-kcp/dex.db | Bin 0 -> 98304 bytes contrib/example-backend-kcp/go.mod | 36 +- contrib/example-backend-kcp/go.sum | 60 ++++ .../hack/dex-config-dev.yaml | 149 ++++++++ contrib/example-backend-kcp/http/handler.go | 1 - .../example-backend-kcp/kubernetes/manager.go | 1 - .../kubernetes/resources/kubeconfig.go | 1 - .../kubernetes/resources/namespace.go | 1 - .../kubernetes/resources/rbac.go | 1 - .../kubernetes/resources/secret.go | 1 - .../example-backend-kcp/options/options.go | 1 - .../e2e/bind/fixtures/provider/bootstrap.go | 41 +++ .../e2e/bind/fixtures/provider/crd-foo.yaml | 47 +++ .../bind/fixtures/provider/crd-mangodb.yaml | 58 ++++ .../test/e2e/bind/happy-case_test.go | 122 +++++++ .../test/e2e/framework/backend.go | 165 +++++++++ .../test/e2e/framework/bind.go | 106 ++++++ .../test/e2e/framework/browser.go | 37 ++ .../test/e2e/framework/clients.go | 52 +++ .../test/e2e/framework/kcp.go | 145 ++++++++ .../test/e2e/framework/konnector.go | 75 ++++ .../test/e2e/framework/kubeconfig.go | 63 ++++ contrib/example-backend-kcp/tools.go | 2 +- docs/content/setup/setup-with-gke.md | 324 ++++++++++++++++++ dump/apiserviceexport.yaml | 39 +++ dump/apiserviceexportrequest.yaml | 11 + dump/apiservicenamespaces.yaml | 4 + dump/clusterbindings.yaml | 8 + 56 files changed, 1643 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/unit-runtime.yaml create mode 100644 contrib/example-backend-kcp/127.0.0.1.crt create mode 100644 contrib/example-backend-kcp/127.0.0.1.key create mode 100644 contrib/example-backend-kcp/127.0.0.1.pem create mode 100644 contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/apiservicebindings.yaml create mode 100644 contrib/example-backend-kcp/dex.db create mode 100644 contrib/example-backend-kcp/hack/dex-config-dev.yaml create mode 100644 contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/bootstrap.go create mode 100644 contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-foo.yaml create mode 100644 contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-mangodb.yaml create mode 100644 contrib/example-backend-kcp/test/e2e/bind/happy-case_test.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/backend.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/bind.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/browser.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/clients.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/kcp.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/konnector.go create mode 100644 contrib/example-backend-kcp/test/e2e/framework/kubeconfig.go create mode 100644 docs/content/setup/setup-with-gke.md create mode 100644 dump/apiserviceexport.yaml create mode 100644 dump/apiserviceexportrequest.yaml create mode 100644 dump/apiservicenamespaces.yaml create mode 100644 dump/clusterbindings.yaml diff --git a/.github/workflows/unit-runtime.yaml b/.github/workflows/unit-runtime.yaml new file mode 100644 index 000000000..1b5b59bab --- /dev/null +++ b/.github/workflows/unit-runtime.yaml @@ -0,0 +1,40 @@ +jobs: + unit: + runs-on: [ubuntu-latest] + timeout-minutes: 8 + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + # If you thinking to upgrade - don't. https://github.com/creack/pty/pull/109 + # We would lose windows ssh support :/ windows agent stops building on windows + go-version: v1.23.4 + id: go + - name: Check out code + uses: actions/checkout@v2 + + - name: Unit tests RUNTIME + id: coverage + shell: bash + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' + run: | + make test-runtime + COVERAGE=$(go tool cover -func profile.cov | grep total: | awk '{print $3}') + echo "\n\nCoverage will be $COVERAGE" + echo "::set-env name=COVERAGE::$COVERAGE" + - run: | + echo "${{env.COVERAGE}}" + echo $COVERAGE + - name: 'Comment PR' + if: ${{ github.ref != 'refs/heads/main' }} + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `${{env.COVERAGE}}` + }) \ No newline at end of file diff --git a/Makefile b/Makefile index d741b2284..5ebfd60b2 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,7 @@ require-%: build: WHAT ?= ./cmd/... ./cli/cmd/... build: require-jq require-go require-git verify-go-versions ## Build the project - mkdir -p $(GOBIN_DIR) + mkdir -p $(GOBIN_DIR)$(MAKE) imports set -x; for W in $(WHAT); do \ pushd . && cd $${W%..}; \ GOOS=$(OS) GOARCH=$(ARCH) CGO_ENABLED=0 go build $(BUILDFLAGS) -ldflags="$(LDFLAGS)" -o $(GOBIN_DIR) ./...; \ diff --git a/contrib/example-backend-kcp/127.0.0.1.crt b/contrib/example-backend-kcp/127.0.0.1.crt new file mode 100644 index 0000000000000000000000000000000000000000..015c0cb64afb17ac6c8ae5c7028857f93d3d16a6 GIT binary patch literal 776 zcmXqLVrDUDVtliJnTe5!Ns!@~=#ld)h0d8TX3uxnHT9&a0WTY;R+~rLcV0$DZdL{Z z5knyZ0XF7P7G_~iLnCuN10XaskQ3)MGBq$Xv@olexHxn*F)AS&&&bNa z+{DPw0CWx)QxhX2!y~4;AJ4Wg-fmN)*>uZeP2RG56>O`V#2ubDIc@ED6Wsdc=x*ib zE|wFEtywlMiOsi(XXi8Dd9e1*MAy&CB0Vo^zg2H%(>=V0D=nup?(!AS3wKZ7aed?d zTcuB7bMEF$_lI}(a&;?~8Q%HwBkJ@{mt5J;+P>w}WmN8Xym~D-d#Sv2i9?m|<)eN} z#gj|=jFT-@PWr|g3q{P#C0$$$-H?3d9p+5jGHPLPS(MOFaVvBXXbv;}jU^j11O6 z(_1zu_+LI$XA)#JQPS}p`>AbKn}2OmS83^a;vyKcc;2-M`tGZxF7=hGb1D3J_}}(! ztE1A4%~#5Mqce6hrmx5;krO z&slfe@K(uNae`Mh_LRf@=I8aI%+H#BI4Ui;pM1)UYtrk9^2>A5Iaa<)l2tfU*x;-) zF-}*Z&gg-4NIAo#l{W*{uzR+7S^6`ZG|Vng{Qr*Y6SHKf-{ho)jy{6Z=ZmoR{i(Un ze)ZnF!`oMe@z1{bU+;45&u4ntw|U*!MRy%jw4C+0>iUsf18bA~lMP0^flm5@F6=8U L_n3w4|7isPT{0jC literal 0 HcmV?d00001 diff --git a/contrib/example-backend-kcp/127.0.0.1.key b/contrib/example-backend-kcp/127.0.0.1.key new file mode 100644 index 0000000000000000000000000000000000000000..c3b782662395951d4a23a26e7c23837afd7b2bc8 GIT binary patch literal 1193 zcmV;a1XlYnf&`@k0RRGm0RaHw0)F`Bx1+Z_d?|w3NUUzA-gpM8LKi^if3>{EsG z#k(WtL^+XjI|Q+$U2i;J2M#y6!F}A3Me`#RjOcy#d$$HH!@LS+Y>8ge)Jf3Y%iKlm zNBbm>AhT_=X-DDQy$XvVb}`)a_*%=kL~R}OD@}Ko93gy4kr5`(VKzdEn#ZILc zV|0!)V>u+rOFA0BxZfOaBL%A8TWs3p>XpB1z zmSdllALFFX0FiC_aHV8pk|A)o(m)1c6UFJvY-QN1Dloj~qHJo(Npy}thLV;+ejtdX zo9^|N7OQh0cz(qZ*p@E>0|5X50)hbn0J-n&z#b?B!?HR*_>5NXt(6f!)6?wDuCoXt zld%&dU-yw&HQ$8ni?S|a-8Shvqk>kBrr+kp0$Oe!>cn(xt$f5OJ|`A>G+;4#4v*b^ z;TZ8LM1sy#&>qslC4jj87i# zpi2tfw3A*gTq|zZ?~uc;kD-i#=>ma)0QmATXHLipt@{?DkoeL=45o^gwq2n@wRT}l zs<~mvz-7@5PWq}3o=^}n(hI)%r~kx;lg?%q5@&S!iFT8-hbHc)eWgdKI}fj2a`e0p z7Cw7MxS$=+n#=c=c)SSTR?65rrzQepzQGF98VFs>U4dl7-7krd9cx*MXoB?*V*-JJ z0O)D$XQyE_^Ruv*z!-?5&HTapvGGNt0J=q;3kE!a+jog zLTUSe=^LbjPYCPi7xV@@Qu}Fg#i`mr+8^qFE~sum(c}af_di*#r?d$mQ2QXlRVb4@ zuYufZ9ZKCjMmHR0a_QF6wDezvPHk@W#R7qW0Evjqu!VlnAETSHzN0A)@B2oOV5vjF zW*vLY?szi9#Xh?WJ~|K}M%Q-BjP3qeQEp|m@C?+emkD1$R~M43S(~xA2^)F_&Eau^ z5L1(0Mr_ECp2?pMrZ>-)YbSq0TwwmIodz4*3{rsQaM!a>U&O82^e+m-W}a4kd6udZ!;-I4wgYi|{zwgb3|I55&uR6?`}1GY7MrqjL*uo(AoF6< zV<8^&zErrUJ}W?L zP=`es382ntTL7w^)+0%R-T}$CvrSJ4`n_|I5gueKA#I{Vyw3eW>dR^M&~I1_ zqfMGhVJHNT6^DxRusGP#CAN=kXq$Oh`S&DCu-^#YP^CH!XxR7j_#}3{8;??}2?b0q HhWV`!*33a^ literal 0 HcmV?d00001 diff --git a/contrib/example-backend-kcp/127.0.0.1.pem b/contrib/example-backend-kcp/127.0.0.1.pem new file mode 100644 index 000000000..c177e60fe --- /dev/null +++ b/contrib/example-backend-kcp/127.0.0.1.pem @@ -0,0 +1,47 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDiAn745rejtzx8 +KYLaSKxupt54BqpCF0DngkK1iOxThfTFuyPnRDmRczsEsaRdbzxfBw43ucF93JFF +8yMUjOh99nu3Bi3DvApmbIle09RJ0N3L3EXsR/skjiCzbbNpR+HcvQqLIXYx3PT4 +Wsu5RG0d8ytNd5ccJNxI6usRm6UfO3RAek3TxU6lF2N0jjNjOSTJTV0zEFtuPQNP +q3QXW/ZJoSq+GP94aCPxLwl/g25pCX9DuRmxZYsCA2iMOw2WY5+VH+OkzgCRbfpw +pWRjkiFwuNJABmITxenLbGXYqyowvOeibGrJSXSOQIaSlkF+IIikm+71lharcyB4 +fsUR2JYvAgMBAAECggEBALnv7cAeKATDsjo/+IxW762VET/T0+zNrrMIIpOxEyRf +95FZNd+E7IuyLmLdNuk7o4JWjqbf5sUCWm4e6sR0bK18xCk+JxZ6NGAxeQ6P3X3h +HSgjj08lpQNc/qA/ZzP9VF9DE1KFc/Tv4IYWRLamCdNzBDQWaDZaSPSgeEYjM8St +nCDM+WGei2d1f9oWAyf+MlFTKpz7kylEb1OuwbmGINL1Hew3KU7CUfdh/42j8WB2 +FT+GODduPiu/Ecs238j0QBhCGst0f4OPH68x0UY3XvvVFM358g2SBotghhmrXGq9 +BNWcqb2/BoxPHu+gSwrdtJNeLlwrbtfvkMOuj6GMgekCgYEA+PIxZ07ICq37FqKQ ++NJDDKaKl7ZdoUK1dmFMqrlhyMBl0Q1O+qoPnlAQMtILvvmn/8SGk85mFhJndPqJ +dpOzhybup32lR6k7D69dcvS8DxY+e0a4oB3Pmsv3lni8CN9Wytg7pyYCY77BCtMa +CF3LXYFkw90viZAda1mJaIL1D2MCgYEA6GntZ6dhNPOzsJfAGLyISidluXM6y7vZ +GZnIlwbtaxH6+h2gxlr/qyKzCOAmhbFyl6R7Qmn7gOkbpINPCOvoF/QGO1L7aXLF +qdpA2h/qfy6obkDR5AQZ9z9Zrqe0CSBQ+yDCVSiTPK+B3GodSt09RjccZnLp1tK0 +9F+FTm1u9cUCgYEAiYjMsIV+0R+jm7K+oykO7/tGkGCpQ8FmHXvN7ngyxMU+uws+ +OhAgRtd2y4zt/llRbmW18AzUq5cJX0BXF5KsWZuxuAkbegbN4XGCEFOTXkZsyJCe +yZ8OpjfPlmsnf0NcYP6rnkHKii7F2eQc+shO5V7qO6eEbtyW1EsINhw7pX8CgYEA +iY+Y6s8RJAxSgOVw17NPX8St2vQvCsNmnlZ9eZaqE8OSr1O2A3F8/kgNe+VgJ6V9 +0++Q3SBpskVHDTqDHx6yQus2fQqCsEk0YXJDDFfzrc9p9cf781/SFpuyc0Pjtbsg +82LSYyEe9L5UuKc+Kz+DsvmPn7vIWFRisnmPJ3pyQJkCgYBHk3odpZt1jMxuw5qH +I1DJ01CloFCHRRkJoNZk0mZXplsAqp3WI0mC3gHJt7NNTwn6vXOQER5kKyFtokO8 +zv1B6stp9dBvWAyjTZpKYSgEjxWHivOwONjSJbaPbWibeVj59yRLsN8I3lClOg5o +2Pfz+CR2vRuPUqsJBUwvhvmtEA== +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDBDCCAeygAwIBAgIRAMYVxM+pEs43owdvQLqVyTUwDQYJKoZIhvcNAQELBQAw +FDESMBAGA1UEAxMJMTI3LjAuMC4xMB4XDTI1MDExODE5MTYzNloXDTI2MDExODE5 +MTYzNlowFDESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4gJ++Oa3o7c8fCmC2kisbqbeeAaqQhdA54JCtYjsU4X0xbsj +50Q5kXM7BLGkXW88XwcON7nBfdyRRfMjFIzoffZ7twYtw7wKZmyJXtPUSdDdy9xF +7Ef7JI4gs22zaUfh3L0KiyF2Mdz0+FrLuURtHfMrTXeXHCTcSOrrEZulHzt0QHpN +08VOpRdjdI4zYzkkyU1dMxBbbj0DT6t0F1v2SaEqvhj/eGgj8S8Jf4NuaQl/Q7kZ +sWWLAgNojDsNlmOflR/jpM4AkW36cKVkY5IhcLjSQAZiE8Xpy2xl2KsqMLznomxq +yUl0jkCGkpZBfiCIpJvu9ZYWq3MgeH7FEdiWLwIDAQABo1EwTzAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAaBgNVHREE +EzARggkxMjcuMC4wLjGHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBADtSl4SwIE/T +wn40UjqRGUHuB8q2OrP6sickhIzkRBFco57WkC9HqxrSjncnCiD84f893YVBIpiz +1HeNW2i7AWeobHQeEc5t2bUecqZmy1MbiTmPpoxvMOrKu7rdgF7KV8wtSg1+5wW4 +2EskbqjIDSVdykDfg+d/FQPmgvhBIqDfY8o2CpLrkR+nbGcIqe5iHSDMcYBDLJFe +LSB+MuA7VHcAkqnZUKwHSYZKOU8DNICbcCH/7gryAxlVTpNioUFMEZefFAWO/Hzf +B9Xe7sO3qVYPm9n/LtN9+eYuK9sNRwcVusYhOZrjetfEbTA7NG/JgDINUUIvEUQH +qTm8Nla/+To= +-----END CERTIFICATE----- diff --git a/contrib/example-backend-kcp/backend/config.go b/contrib/example-backend-kcp/backend/config.go index cf0c64020..030155de5 100644 --- a/contrib/example-backend-kcp/backend/config.go +++ b/contrib/example-backend-kcp/backend/config.go @@ -30,7 +30,6 @@ import ( apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" kcpclusterclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" "github.com/kcp-dev/logicalcluster/v3" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" diff --git a/contrib/example-backend-kcp/backend/server.go b/contrib/example-backend-kcp/backend/server.go index 4f9f73714..4415f6eed 100644 --- a/contrib/example-backend-kcp/backend/server.go +++ b/contrib/example-backend-kcp/backend/server.go @@ -33,7 +33,7 @@ import ( "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/deploy" examplehttp "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/http" examplekube "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/kubernetes" - kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" ) type Server struct { @@ -117,7 +117,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) { config.Options.TestingAutoSelect, signingKey, encryptionKey, - kubebindv1alpha1.Scope(config.Options.ConsumerScope), + kubebindv1alpha2.Scope(config.Options.ConsumerScope), s.Kubernetes, config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions().Lister(), ) @@ -129,7 +129,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) { // construct controllers s.ClusterBinding, err = clusterbinding.NewController( config.ClientConfig, - kubebindv1alpha1.Scope(config.Options.ConsumerScope), + kubebindv1alpha2.Scope(config.Options.ConsumerScope), config.BindInformers.KubeBind().V1alpha1().ClusterBindings(), config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), config.KubeInformers.Rbac().V1().ClusterRoles(), @@ -142,7 +142,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) { } s.ServiceNamespace, err = servicenamespace.NewController( config.ClientConfig, - kubebindv1alpha1.Scope(config.Options.ConsumerScope), + kubebindv1alpha2.Scope(config.Options.ConsumerScope), config.BindInformers.KubeBind().V1alpha1().APIServiceNamespaces(), config.BindInformers.KubeBind().V1alpha1().ClusterBindings(), config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), @@ -163,7 +163,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) { } s.ServiceExportRequest, err = serviceexportrequest.NewController( config.ClientConfig, - kubebindv1alpha1.Scope(config.Options.ConsumerScope), + kubebindv1alpha2.Scope(config.Options.ConsumerScope), config.BindInformers.KubeBind().V1alpha1().APIServiceExportRequests(), config.BindInformers.KubeBind().V1alpha1().APIServiceExports(), config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions(), diff --git a/contrib/example-backend-kcp/bootstrap/config.go b/contrib/example-backend-kcp/bootstrap/config.go index c0e10ab70..2c621bf98 100644 --- a/contrib/example-backend-kcp/bootstrap/config.go +++ b/contrib/example-backend-kcp/bootstrap/config.go @@ -22,7 +22,6 @@ import ( kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" kcpdynamic "github.com/kcp-dev/client-go/dynamic" kcpclusterclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" diff --git a/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go index 4cf5294f2..0e168511e 100644 --- a/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go +++ b/contrib/example-backend-kcp/bootstrap/config/config/bootstrap.go @@ -26,7 +26,6 @@ import ( "github.com/kcp-dev/kcp/sdk/apis/core" kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" "github.com/kcp-dev/logicalcluster/v3" - "k8s.io/apimachinery/pkg/util/sets" ) diff --git a/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go index 03d42bcfc..4bf185184 100644 --- a/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go +++ b/contrib/example-backend-kcp/bootstrap/config/core/bootstrap.go @@ -23,7 +23,6 @@ import ( kcpdynamic "github.com/kcp-dev/client-go/dynamic" kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" "github.com/kcp-dev/logicalcluster/v3" - "k8s.io/apimachinery/pkg/util/sets" "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/core/resources" diff --git a/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go index 5ff4c78c8..5690331e3 100644 --- a/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go +++ b/contrib/example-backend-kcp/bootstrap/config/core/resources/bootstrap.go @@ -22,7 +22,6 @@ import ( confighelpers "github.com/kcp-dev/kcp/config/helpers" kcpclientcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/discovery" diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go index e852effa9..91f5915d6 100644 --- a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/bootstrap.go @@ -26,7 +26,6 @@ import ( kcpclient "github.com/kcp-dev/kcp/sdk/client/clientset/versioned" kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" "github.com/kcp-dev/logicalcluster/v3" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/apiservicebindings.yaml b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/apiservicebindings.yaml new file mode 100644 index 000000000..437c74a0d --- /dev/null +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/apiservicebindings.yaml @@ -0,0 +1,9 @@ +apiVersion: kube-bind.io/v1alpha1 +kind: APIServiceBinding +metadata: + name: my-api-binding +spec: + kubeconfigSecretRef: + namespace: my-secret-namespace + name: my-provider-kubeconfig + key: kubeconfig diff --git a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go index a518f3393..8efe611c2 100644 --- a/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go +++ b/contrib/example-backend-kcp/bootstrap/config/kube-bind-provider/resources/bootstrap.go @@ -22,7 +22,6 @@ import ( confighelpers "github.com/kcp-dev/kcp/config/helpers" kcpclientcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/discovery" diff --git a/contrib/example-backend-kcp/bootstrap/options/options.go b/contrib/example-backend-kcp/bootstrap/options/options.go index fa1ba4661..e76aedc25 100644 --- a/contrib/example-backend-kcp/bootstrap/options/options.go +++ b/contrib/example-backend-kcp/bootstrap/options/options.go @@ -20,7 +20,6 @@ import ( "fmt" "github.com/spf13/pflag" - "k8s.io/component-base/logs" logsv1 "k8s.io/component-base/logs/api/v1" ) diff --git a/contrib/example-backend-kcp/cmd/backend/main.go b/contrib/example-backend-kcp/cmd/backend/main.go index 8e13e8445..f42c570a4 100644 --- a/contrib/example-backend-kcp/cmd/backend/main.go +++ b/contrib/example-backend-kcp/cmd/backend/main.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/spf13/pflag" - genericapiserver "k8s.io/apiserver/pkg/server" logsv1 "k8s.io/component-base/logs/api/v1" "k8s.io/component-base/version" diff --git a/contrib/example-backend-kcp/cmd/bootstrap/main.go b/contrib/example-backend-kcp/cmd/bootstrap/main.go index b68debb4a..bd9d87a24 100644 --- a/contrib/example-backend-kcp/cmd/bootstrap/main.go +++ b/contrib/example-backend-kcp/cmd/bootstrap/main.go @@ -22,7 +22,6 @@ import ( "os" "github.com/spf13/pflag" - genericapiserver "k8s.io/apiserver/pkg/server" logsv1 "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" diff --git a/contrib/example-backend-kcp/committer/committer.go b/contrib/example-backend-kcp/committer/committer.go index 9c1ea03b1..409682535 100644 --- a/contrib/example-backend-kcp/committer/committer.go +++ b/contrib/example-backend-kcp/committer/committer.go @@ -23,7 +23,6 @@ import ( jsonpatch "github.com/evanphx/json-patch" "github.com/google/go-cmp/cmp" "github.com/kcp-dev/logicalcluster/v3" - "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/contrib/example-backend-kcp/config/kube-bind/bootstrap.go b/contrib/example-backend-kcp/config/kube-bind/bootstrap.go index 5ad4da84e..13b73b485 100644 --- a/contrib/example-backend-kcp/config/kube-bind/bootstrap.go +++ b/contrib/example-backend-kcp/config/kube-bind/bootstrap.go @@ -26,7 +26,6 @@ import ( kcpclient "github.com/kcp-dev/kcp/sdk/client/clientset/versioned" kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" "github.com/kcp-dev/logicalcluster/v3" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" diff --git a/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go b/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go index a518f3393..8efe611c2 100644 --- a/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go +++ b/contrib/example-backend-kcp/config/kube-bind/resources/bootstrap.go @@ -22,7 +22,6 @@ import ( confighelpers "github.com/kcp-dev/kcp/config/helpers" kcpclientcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/discovery" diff --git a/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go index a9d5243d1..36788c39e 100644 --- a/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go +++ b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_controller.go @@ -29,7 +29,6 @@ import ( corelisters "github.com/kcp-dev/client-go/listers/core/v1" rbaclisters "github.com/kcp-dev/client-go/listers/rbac/v1" "github.com/kcp-dev/logicalcluster/v3" - v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" diff --git a/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go index cc38b7997..5f1c8a4de 100644 --- a/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go +++ b/contrib/example-backend-kcp/controllers/clusterbinding/clusterbinding_reconcile.go @@ -23,7 +23,6 @@ import ( "time" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" diff --git a/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go index 3c2ba9dea..c84c8480c 100644 --- a/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go +++ b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_controller.go @@ -26,7 +26,6 @@ import ( apiextensionsinformers "github.com/kcp-dev/client-go/apiextensions/informers/apiextensions/v1" apiextensionslisters "github.com/kcp-dev/client-go/apiextensions/listers/apiextensions/v1" "github.com/kcp-dev/logicalcluster/v3" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" diff --git a/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go index a2af00887..b14af4bff 100644 --- a/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go +++ b/contrib/example-backend-kcp/controllers/serviceexport/serviceexport_reconcile.go @@ -20,14 +20,13 @@ import ( "context" "github.com/kcp-dev/logicalcluster/v3" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/klog/v2" kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" - kubebindhelpers "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1/helpers" + kubebindhelpers "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions" ) diff --git a/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go index d42c0f45c..5ff189f0c 100644 --- a/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go +++ b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_controller.go @@ -26,7 +26,6 @@ import ( apiextensionsinformers "github.com/kcp-dev/client-go/apiextensions/informers/apiextensions/v1" apiextensionslisters "github.com/kcp-dev/client-go/apiextensions/listers/apiextensions/v1" "github.com/kcp-dev/logicalcluster/v3" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" diff --git a/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go index 9f209b770..f3218a2f1 100644 --- a/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go +++ b/contrib/example-backend-kcp/controllers/serviceexportrequest/serviceexportrequest_reconcile.go @@ -21,7 +21,6 @@ import ( "time" "github.com/kcp-dev/logicalcluster/v3" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,7 +28,7 @@ import ( "k8s.io/klog/v2" kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" - "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1/helpers" + "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions" ) diff --git a/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go index 70a2b1e06..7251396bd 100644 --- a/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go +++ b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_controller.go @@ -30,7 +30,6 @@ import ( corelisters "github.com/kcp-dev/client-go/listers/core/v1" rbaclisters "github.com/kcp-dev/client-go/listers/rbac/v1" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" diff --git a/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go index 3a7c2be78..546cc5218 100644 --- a/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go +++ b/contrib/example-backend-kcp/controllers/servicenamespace/servicenamespace_reconcile.go @@ -22,7 +22,6 @@ import ( "reflect" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" diff --git a/contrib/example-backend-kcp/dex.db b/contrib/example-backend-kcp/dex.db new file mode 100644 index 0000000000000000000000000000000000000000..e67e5413468834714a1892b44d2580acec333366 GIT binary patch literal 98304 zcmeI5*^eW~dBDj%W_D(FmXfVCy|x8W0|}$aE+ux8Y?31wYm&qJz7NA_^hq|^7yIDl z2*_-tBzA%XHeevhlLHvA5kKWGF!JEH1V|kC4+xC>0r67+13{{rbC5IScrC28^5)|V z$vV5ce&1JBUsW}$Yn8lUa2{UKB+Bsk9j`f@PRBPq9*4tq;&3>=jNa$3uOP>ra}WA< zF1a^2(J|g1f9I~1*!3GL&6ixixf9&~_4Xg_|IGQx{>S%!dH+Xy-`^7)Ki%baez+s; zy|eeuhC?|Vr>zIu?#GXv&p3mkMNZ*)QII)O=X70A_8MstFRM%{)VHc2+b|;ACS%;dDk(IsIzcyk1YXRw{ZZO`~QArbjn4LGI}`Dp`>k z?kd9tM3E9CoxBP2WeS;+t{ue^60=CcNt7U7JVQ_ z`!c1^=fk}AzsKi0w*NhidK?`5rN)@#eLeO=wH;1{Fo z>lOA!Mct@~H)*g94^+siuPKScSx@N%G>hrk~y|N$lKp|Mbc3w)@RDozEWI2+zaMzxP&9o`;^{ zTw`cnx1sygwE=*QV7=+Is5omoWV?Kkoh^6aa8vnKtb7t%@$O>VH?=4iT@#62IPZn- zZn<;sZaSNt?n0NBeOr%jZ@Hg7+LV!53@fn3_nl|{TifnOkDMPz7wwiEyNJ8Jf`1YA z+`6oD$Y4gM^7SFxaChe-)o-6Izu@6#nQDD|sV%A|dwV=p? zQ;_j^Wi(@Jg6iCMzx}rJbfLGB(9`USpEXS7^ttD% zI9&hj`cK!tyZ+hrPp*G(eb0638o4MG3@;!61b_e#00KY&2mk>f00e*l5C8%|U>$)6 zd$*l$@8Lc?Xf4-a;Rn0Ozq{n$LH?a3|2^ctx8&bO{_Q3I7V>W``R^kC-6j7WE?`$5bf zi3A?o8&UBx?bm}Q-W$vrnNj`bWXda3b3E6$fS)FUdML#70^Ha?l$pVJ!ZUQ>Pq&=z zr(gXVpW8p>?(H<(?wvQ>)}^{NgY)<1TyQWBvU-STXUvf24Q(O@hK%Y(dux32z}i&j z;9K83Ks%Mm0(6q{LgHvW6 zl;z2QozICLL4*u#J~W4cIXxMKbc&|uVo(ieypB($^Ywq%uUx^9~p5|C{&Szc>vzfB+Bx0zd!= z00AHX1b_e#00KY&2)xP!PCL%mpMI6!&n?}jFrEZ>b|4B9)*lKC28_(lr-q0J*o)f~ zUfk^ozAzOg!U5XL(&4Z-K!^O^h>wbR{S1o{1jYvg1VOQqAlo0-jRK;AZf&@@TL9g* zfgPh81~A|IVImaqMHY7psOZUR^5y#ft1Lk%8xQ~jKmZ5;0U!VbfB+Bx0zd!=0D)JY zfVKX=ak|1<|984v4i|d8@+6>uKmZ5;0U!VbfB+Bx0zd!=00AJdk-({V%U=6FTl#*Q z%kA$g)QBIBL$Zc`pU!9$jT3%uHW4}D)2w}e%Ej&e7@vfAjg|~Sol*iLaiSEY=F(t! z)f;VAfFEQ2_i+sOV~a)a-x7T)-(D$h0qse^2E?%u(s9ltXe}6&bymX-e}KV1!{W*T zhNpf00e*l5C8%|;AIl9e&N4y9^y8-vJW>-%G^S+Z*rPo@U%JNdJ|j>4yE3N z(FNYXd*{FZf0>1Ya{~b&00e*l5C8%|00;m9AOHk_01yBIzh469zyJUJo&}B$1b_e# z00KY&2mk>f00e*l5C8%|00_KH0`~9!Z-2*u{^11#fWXTl@VAC@=i%S*fB%g?zh6KP zB-Ow9?E}@MMS;n2vxDPrADB9KaD1Q(J@k0fp@D)9se!5L0rWlRIg`js7&(AyE7!@mzMja+kU$HYuR#v)8v3*&7* z>kAjTWGOsKWKA+sFk5E3Jc{}{q-v0YKNpD?MIysgz5SHzm3h36%fp7ZgSV%R{va#Q zN1>B+l<739`3iz-Ni154#tuG0?x`whme?+SvE{F0#Ic_^4hG*3hA{Lz+Ad1>J4u}n z69Hc!%;J7OjbY)4kK=tb+7C92A!+)V2s&zr573ddQa8(lZ>Z&CGT+9>NuE<`$xuGg zGW_^hsgx2yo{F4w19Yo`&zTOXGpaPyBIy=2RLaCj9Lr>kC_Ng?r)+G-5k(A-8&XL& zicu|$MQYkuuc^ImR_80}c9@%(B<`K!-AK8o6Q#H^_REr(ZuAU3Ec$23wA7AHdl<%= znR25nrGhCzoe$DPV?;zTuOtTscqWvNida6>!O+8u+lo&cce+uP9{RH*t&_&I7R&lh zvQ&Q9>_+N=>0m5krExq!;wRx+e9))5Rk^}6m5JCRv+d!`$o2Ds3aevTx*kAhCiAs% z!pH>b!(r0w)d%_Z0A~WUQqCqQiV_>)u6&Zn^`)lHs@XK($&y1R8kzVT)n34_YnoSK zVib=K`CFwPsAKrCFL3M+p-1=nf<8;?-}-IUJA@G-3=c=R039SKzt6{nSpt29`5-~y zR1gnEs1SOj@mfKT{ZzKxr9wHbo)wxJ)l>(mRH)q_Wz}G%%*>NmmmGGp$z0c$YtFgI zFeXJqY_K&KGtI28pQ%*5N?KRQN;s-E#N0`tubvcGCdS12UT?ZpNlX&$T%kGdB?)#I zWqowQ?6$*sxz(d+U(PJ1x|4>YG>IM=Q#&$xzO)p|#3!{}haig0m~YlSNrg}NIF?P; zi|zUtZ%za;Fb>2FamI#3rZrHPub5vG@`lF;b}n_f076yu`=-KbWj_F$kU z#wjzDAGB(sB9G|lpcX1eX8DrBL@D1aIMPZu)od9xGbCZrqDEI{Sh$3qk6fY5L9)(Z z(W=Du{XT+?Q6-%#g(e}QI7$nnDcNsDdlfHMEUHAR($5G*m0?!|{p7%EgBzJSl}i;w z-?$skL=3f-z%T+IHD?{HKJGW;$(BKwQ8KAFqCURU=m+skohH3rxkI1C0(_AlqzlPL zuiK_eEzR(by-JigDav%HlIYiXA|KH5<`}Qe3|wzT%%&ctOX0yR5tgHoxgqB8>YyCR zp~p*$cum#2gM1-5uV+)&3+@^8_tMovoKI3p;yE@8TBNZpyXn|QO$Rw#x$6k zq!JT$+BFM#)z2uSRyX8jg+P45Q+kT1;YqBNFYp>)oJI!qK(#Da$Vx)=lH<%QP3t83 zQEJ~PVcmf^$+TIyGOG>ZYStU_tK3W^`G#6a47_YEGig;=hRyW~-jNoL^ONKxR8JL? zm0GwNZ!*2Kch*UUdmP>;jSRt9oK*yOUWuEfxL2xYGp%N@OZ0p5CNW5{I>Sydu13wv z#R*lcq`O=(nLqJ$)Lv{-QM-c4M?1kPKTBlPerMQBdug&x#KMDOwPl18Qj19OTs|5~ z2GvA=S{HO$Q)W zYqMlIWR#+PdX_JsD;}XM4ox|#GVJ(LX2)BVV~kNvX|x&&V8yl;BD(Puo$ZISl@>AW z_Eb#qafx84>+SlhX=>cg$a;B}RupWK@cRS(Ij7Y}WNTV$6mk`gl(T6LOZG?YdRRz| z@@1ANnpEGMqzo#N?-Wi_M0t`44hTvOPb)alV55VuA2ZrjvsO_Br7UN2Q&DeKhPmEM zo(&ju#jFLHYpKy`wHFfSJwb1={zj-2W#mYz+2I1gPCGZP2iS@-Q2PbGny+K6vRPmy zE{jbveX2U}4Jf`FqRVWspBa&2n4dQ0Q`3jlYn`z#IAr4SEY8U)(M?o@YNFms`$sWD z59-OlAT}EZd*;9^#>+**#7Jf)4p=sw!Yj>GbQ~S^vV$Pw9T-h%79_~Jnh9H%Y*D5N zA0gzYTDcUc@sT()_7=kR9FZK6^Yn-eBTb3KhDvsv^X6ko4Z|yZ#mjY@-rl^-v|_Cw zV~AaVtgby(4>TQ8yX+6^+G_G9FJw^Wh!O{^=fg- zsxu|m^y1xEOZAc`Ufz_(y+{d*_bVqvh@X4gerZ$;=do;aG8$4bB7M^6&nKOe$c%`q zl-|L{u_P5zn&k<5qEWMQM!>~(6(3`B){99}lk8;JS))P()V3aulPN#Tv^XNymc0WU zX+UP4E~Zmt)AaXHJIBaV#w<4)I^FCWqx$gRBj}m_pnIl|F0D-u7GAMC7W)HO>;F#I z|2WV;ynp}@00KY&2mk>f00e*l5C8%|00;nqSB${+?T0(oGUHd z4gJu~EpbucWW!E&o(%f00e*l5C8%|00;m9An+Lx@ZM>-cbf00e*l5C8%|00;m9An+0i!2bU)5k8z62mk>f00e*l5C8%|00;m9AOHk_ zz-L4N*8e{v&~O?c00e*l5C8%|00;m9AOHk_01yBIFOdMO|Gz}|aB3g`1b_e#00KY& z2mk>f00e*l5C8(75rO^Pk>j58Cl1$NKWIOA?EK08ueX1>|FgSy_dmY>%lkjt`~IHb z_~|aU^TQo!@14DOHeS?e>%q4B@nh#R&Y)Qy+VSKP9nX7br+i{3M<+P4&UW;P_ujVq@X-0)M>f5) zGmwTdq&lOl$>KSC-I!J5 zSV2Q@NmCQnNlR8_hFceb;23KoctvA4l2TPo8B-#Oz9iM~2(rQTIPFrZyY`ifO0UPU zq#$ofz=)I}>EuoNbeTe?q_vlHb`VaY1aVzimgC7WrwP2kv1_i#((JReJxwvy=aF&<0(@#Jcox? zEnzv5=~JS}$-OJoZb(Q0xbcUG#`NJmJ7AB8`0JJw4w>jVozO?GS+DB zWlEpVhk5OPkI#2(|9cwsIMPuOZMAFZGE%o|n*TicKTW+*Xk8IG12w}|7qKYaWxYB2 znT}-*sMg7FJcV`vc3dy`sk#!JjW3wi>dVT7J-Rg?_T=q5g21-Xn+OcF{Iu z7H#daiL&elLuSPu1w#f5ne!3KWv6m@gl%uR6K^>kg3NMLeIyE~Pe9d3vHg~-PueyV zcHz7iy1V7hy?bMpvw;P<$aUu1dVG7!{q)hMjLc%#fGxi7JoDe$c0YRL{5WcNL5tAE z=k1lwXA$wEkk&S~uvFNBg@W z$Ns$^I9xw>ed)n}?f=LAKi>bh``_F9m%YE+{o~!I_kOVRzc-A?Aqofp0U!VbfB+Bx z0xycdGyV;GvilR4Z3nU3SU|J2_FTuZ+r457EytXhlV?Ge1-xo8UZ3^023V%Pnrblt zwwPkEX8RZZjbrUav^^bkW-_mt5qh$FKI47VM07Yk()yAtr)tGh^exWDE8@Zlyf*0bD6%=&!r%yoa;{qUjl z#M7a9?$tY$Wk5|O$3k{YCx=U@|yP6)~OHnw%qYIH=$E0U7si#OJe7)`=?KK zx7}~P>3p^rh@Xd_fA6iJJP$p?xn`y0dUw;OuB{;32yR-5S-ZSwmoKuj ../../ github.com/kube-bind/kube-bind/sdk/apis => ../../sdk/apis + github.com/kube-bind/kube-bind/sdk/client => ../../sdk/client github.com/kube-bind/kube-bind/sdk/kcp => ../../sdk/kcp ) @@ -47,37 +48,51 @@ replace ( require ( github.com/coreos/go-oidc v2.2.1+incompatible + github.com/dexidp/dex/api/v2 v2.1.0 github.com/evanphx/json-patch v5.6.0+incompatible github.com/google/go-cmp v0.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/securecookie v1.1.2 + github.com/headzoo/surf v1.0.1 github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a github.com/kcp-dev/kcp v0.26.0 + github.com/kcp-dev/kcp/pkg/apis v0.11.0 github.com/kcp-dev/kcp/sdk v0.26.0 github.com/kcp-dev/logicalcluster/v3 v3.0.5 github.com/kube-bind/kube-bind v0.4.6 - github.com/kube-bind/kube-bind/sdk/apis v0.4.6 + github.com/kube-bind/kube-bind/cli v0.0.0-20250515145715-d9f20e7c840d + github.com/kube-bind/kube-bind/sdk/apis v0.4.8 github.com/kube-bind/kube-bind/sdk/kcp v0.4.6 + github.com/martinlindhe/base36 v1.1.1 github.com/pierrec/lz4 v2.6.1+incompatible + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace + github.com/stretchr/testify v1.10.0 github.com/vmihailenco/msgpack/v4 v4.3.13 golang.org/x/oauth2 v0.24.0 + google.golang.org/grpc v1.65.0 k8s.io/api v0.32.0 k8s.io/apiextensions-apiserver v0.32.0 k8s.io/apimachinery v0.32.0 k8s.io/apiserver v0.32.0 + k8s.io/cli-runtime v0.32.0 k8s.io/client-go v0.32.0 k8s.io/code-generator v0.32.0 k8s.io/component-base v0.32.0 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241210054802-24370beab758 + sigs.k8s.io/controller-runtime v0.19.0 sigs.k8s.io/controller-tools v0.16.1 + sigs.k8s.io/yaml v1.4.0 ) require ( cel.dev/expr v0.18.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/andybalholm/cascadia v1.3.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 @@ -89,10 +104,12 @@ require ( 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.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -102,34 +119,44 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.0.1 // 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/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // 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/imdario/mergo v0.3.12 // 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/kube-bind/kube-bind/sdk/client v0.4.6 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mdp/qrterminal/v3 v3.2.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.36.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/cachecontrol v0.1.0 // 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/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cobra v1.8.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // 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 @@ -157,7 +184,6 @@ require ( google.golang.org/appengine v1.6.7 // 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 @@ -166,8 +192,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + rsc.io/qr v0.2.0 // 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/kustomize/api v0.18.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/contrib/example-backend-kcp/go.sum b/contrib/example-backend-kcp/go.sum index e05d12ff1..951ea7fff 100644 --- a/contrib/example-backend-kcp/go.sum +++ b/contrib/example-backend-kcp/go.sum @@ -1,7 +1,13 @@ cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 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= @@ -24,16 +30,22 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dexidp/dex/api/v2 v2.1.0 h1:V7XTnG2HM2bqWZMABDQpf4EA6F+0jWPsv9pGaUIDo+k= +github.com/dexidp/dex/api/v2 v2.1.0/go.mod h1:s91/6CI290JhYN1F8aiRifLF71qRGLVZvzq68uC6Ln4= 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 v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -44,6 +56,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos 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-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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= @@ -88,6 +102,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -96,6 +112,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 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/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 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= @@ -104,6 +122,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 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/headzoo/surf v1.0.1 h1:wk3+LT8gjnCxEwfBJl6MhaNg154En5KjgmgzAG9uMS0= +github.com/headzoo/surf v1.0.1/go.mod h1:/bct0m/iMNEqpn520y01yoaWxsAEigGFPnvyR1ewR5M= +github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca h1:utFgFwgxaqx5OthzE3DSGrtOq7rox5r2sxZ2wbfTuK0= +github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca/go.mod h1:8926sG02TCOX4RFRzIMFIzRw4xuc/TwO2gtN7teMJZ4= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -120,6 +142,8 @@ github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a h1:O9SNM3MqMlwoE github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a/go.mod h1:h5jC8rEbkyGUgV86+sgtMMcl950ooGzk+iLrQnbCR6o= github.com/kcp-dev/kcp v0.26.0 h1:A0auaa7M7QxcF7/HL/ydkx4qa+puhr6IU6DFkL5rwtQ= github.com/kcp-dev/kcp v0.26.0/go.mod h1:zjSxe+dtgsSgaDZGLCizSO0UxfASGEL3IezcLjw0iUQ= +github.com/kcp-dev/kcp/pkg/apis v0.11.0 h1:K6p+tNHNcvfACCPLcHgY0EMLeaIwR1jS491FyLfXMII= +github.com/kcp-dev/kcp/pkg/apis v0.11.0/go.mod h1:8cUAmfMJcksauz53UtsLYG8Phhx62rvuCnd/5t/Zihk= github.com/kcp-dev/kcp/sdk v0.26.0 h1:QB0BiidlW4ERXGb6A/W93IBCo0g1zlE6T/bcOMyONX4= github.com/kcp-dev/kcp/sdk v0.26.0/go.mod h1:XjabYVlKkpuRr1qATymS0gMTEjC6McuuwdoVGSar2fE= github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20240918143026-ab5c3a6448cb h1:6vSaQJE2W9etXQFdHL9xWDUuzv8f8oTZ3tsXBL9UxMU= @@ -130,6 +154,8 @@ github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20240918143 github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:5F0wbie5xX1jDEg5sk5dr+KF8rwFkYtZFHDhSF/UsG4= github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20240918143026-ab5c3a6448cb h1:1J6FC8pvCrMeWdnM2bbMZuhHGeeLG+JvQ1uUPnwwlC8= github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:EC5je+P5ix2QCV4zbwxlY8Zk5MJe4e1eKXxjCMd/7Eo= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20240918143026-ab5c3a6448cb h1:NzycNVl4dZut4sJ1cN7MIH3hV6IEEMUtM0IiFbf1ZzU= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:IH8L/VJclv2sIZ6SrvWe//+HnuExxQakj5YED82eWo0= github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20240918143026-ab5c3a6448cb h1:yuzQJrRaTpdylN62AQH7IsQth4gf4pSHX/MBcEEcuYw= github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20240918143026-ab5c3a6448cb/go.mod h1:l7HaB8VBHdNA72/wtAohDsemuLiVNdW6hx9lNB5J088= github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20240918143026-ab5c3a6448cb h1:tXzwNHi7U49G65ldKyikILcq1Ymni9F8Dho325mrnWw= @@ -150,18 +176,30 @@ 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/kube-bind/kube-bind/cli v0.0.0-20250515145715-d9f20e7c840d h1:XNKsPss6lwmEZdNIhiWxVB/dhT8apYwyBgtMAddAUrE= +github.com/kube-bind/kube-bind/cli v0.0.0-20250515145715-d9f20e7c840d/go.mod h1:+Si5+7jDEdgWGwZw0Pho2VGtkNnonEgDLVMThVN2LC4= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/martinlindhe/base36 v1.1.1 h1:1F1MZ5MGghBXDZ2KJ3QfxmiydlWOGB8HCEtkap5NkVg= +github.com/martinlindhe/base36 v1.1.1/go.mod h1:vMS8PaZ5e/jV9LwFKlm0YLnXl/hpOihiBxKkIoc3g08= 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/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= +github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -172,6 +210,8 @@ github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -192,6 +232,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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= @@ -206,6 +248,8 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -224,6 +268,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= @@ -283,6 +329,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= @@ -295,16 +342,21 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.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.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= @@ -360,12 +412,20 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= 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/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/controller-tools v0.16.1 h1:gvIsZm+2aimFDIBiDKumR7EBkc+oLxljoUVfRbDI6RI= sigs.k8s.io/controller-tools v0.16.1/go.mod h1:0I0xqjR65YTfoO12iR+mZR6s6UAVcUARgXRlsu0ljB0= 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/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= +sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= +sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= +sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/contrib/example-backend-kcp/hack/dex-config-dev.yaml b/contrib/example-backend-kcp/hack/dex-config-dev.yaml new file mode 100644 index 000000000..9e2bdbe91 --- /dev/null +++ b/contrib/example-backend-kcp/hack/dex-config-dev.yaml @@ -0,0 +1,149 @@ +# DEPRECATED: use config.yaml.dist and config.dev.yaml examples in the repository root. +# TODO: keep this until all references are updated. + +# The base path of dex and the external name of the OpenID Connect service. +# This is the canonical URL that all clients MUST use to refer to dex. If a +# path is provided, dex's HTTP service will listen at a non-root URL. +issuer: http://127.0.0.1:5556/dex + +# The storage configuration determines where dex stores its state. Supported +# options include SQL flavors and Kubernetes third party resources. +# +# See the documentation (https://dexidp.io/docs/storage/) for further information. +storage: + type: memory + config: + file: examples/dex.db + + # type: mysql + # config: + # host: localhost + # port: 3306 + # database: dex + # user: mysql + # password: mysql + # ssl: + # mode: "false" + + # type: postgres + # config: + # host: localhost + # port: 5432 + # database: dex + # user: postgres + # password: postgres + # ssl: + # mode: disable + + # type: etcd + # config: + # endpoints: + # - http://localhost:2379 + # namespace: dex/ + + # type: kubernetes + # config: + # kubeConfigFile: $HOME/.kube/config + +# Configuration for the HTTP endpoints. +web: + http: 0.0.0.0:5556 + # Uncomment for HTTPS options. + # https: 127.0.0.1:5554 + # tlsCert: /etc/dex/tls.crt + # tlsKey: /etc/dex/tls.key + +# Configuration for dex appearance +# frontend: +# issuer: dex +# logoURL: theme/logo.png +# dir: web/ +# theme: light + +# Configuration for telemetry +telemetry: + http: 0.0.0.0:5558 + # enableProfiling: true + +# Uncomment this block to enable the gRPC API. This values MUST be different +# from the HTTP endpoints. +grpc: + addr: 127.0.0.1:5557 +# tlsCert: examples/grpc-client/server.crt +# tlsKey: examples/grpc-client/server.key +# tlsClientCA: examples/grpc-client/ca.crt + +# Uncomment this block to enable configuration for the expiration time durations. +# Is possible to specify units using only s, m and h suffixes. +# expiry: +# deviceRequests: "5m" +# signingKeys: "6h" +# idTokens: "24h" +# refreshTokens: +# reuseInterval: "3s" +# validIfNotUsedFor: "2160h" # 90 days +# absoluteLifetime: "3960h" # 165 days + +# Options for controlling the logger. +# logger: +# level: "debug" +# format: "text" # can also be "json" + +# Default values shown below +oauth2: + # use ["code", "token", "id_token"] to enable implicit flow for web-only clients +# responseTypes: [ "code" ] # also allowed are "token" and "id_token" + # By default, Dex will ask for approval to share data with application + # (approval for sharing data from connected IdP to Dex is separate process on IdP) + skipApprovalScreen: true + # If only one authentication method is enabled, the default behavior is to + # go directly to it. For connected IdPs, this redirects the browser away + # from application to upstream provider such as the Google login page +# alwaysShowLoginScreen: false + # Uncomment the passwordConnector to use a specific connector for password grants +# passwordConnector: local + +# Instead of reading from an external storage, use this list of clients. +# +# If this option isn't chosen clients may be added through the gRPC API. +staticClients: +- id: kube-bind + redirectURIs: + - 'http://127.0.0.1:8080/callback' + name: 'Kube Bind' + secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + +# - id: example-device-client +# redirectURIs: +# - /device/callback +# name: 'Static Client for Device Flow' +# public: true +connectors: +- type: mockCallback + id: mock + name: Example +# - type: google +# id: google +# name: Google +# config: +# issuer: https://accounts.google.com +# # Connector config values starting with a "$" will read from the environment. +# clientID: $GOOGLE_CLIENT_ID +# clientSecret: $GOOGLE_CLIENT_SECRET +# redirectURI: http://127.0.0.1:5556/dex/callback +# hostedDomains: +# - $GOOGLE_HOSTED_DOMAIN + +# Let dex keep a list of passwords which can be used to login to dex. +# enablePasswordDB: true + +# A static list of passwords to login the end user. By identifying here, dex +# won't look in its underlying storage for passwords. +# +# If this option isn't chosen users may be added through the gRPC API. +# staticPasswords: +# - email: "admin@example.com" +# # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) +# hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" +# username: "admin" +# userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/contrib/example-backend-kcp/http/handler.go b/contrib/example-backend-kcp/http/handler.go index 39f999509..7810910ac 100644 --- a/contrib/example-backend-kcp/http/handler.go +++ b/contrib/example-backend-kcp/http/handler.go @@ -33,7 +33,6 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/securecookie" apiextensionslisters "github.com/kcp-dev/client-go/apiextensions/listers/apiextensions/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" diff --git a/contrib/example-backend-kcp/kubernetes/manager.go b/contrib/example-backend-kcp/kubernetes/manager.go index 582a42477..0caca66f5 100644 --- a/contrib/example-backend-kcp/kubernetes/manager.go +++ b/contrib/example-backend-kcp/kubernetes/manager.go @@ -24,7 +24,6 @@ import ( kubeclient "github.com/kcp-dev/client-go/kubernetes" corev1listers "github.com/kcp-dev/client-go/listers/core/v1" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go b/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go index cd94ca8ba..4524f06e8 100644 --- a/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go +++ b/contrib/example-backend-kcp/kubernetes/resources/kubeconfig.go @@ -23,7 +23,6 @@ import ( "github.com/kcp-dev/client-go/kubernetes" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/contrib/example-backend-kcp/kubernetes/resources/namespace.go b/contrib/example-backend-kcp/kubernetes/resources/namespace.go index 71541406b..77c196226 100644 --- a/contrib/example-backend-kcp/kubernetes/resources/namespace.go +++ b/contrib/example-backend-kcp/kubernetes/resources/namespace.go @@ -22,7 +22,6 @@ import ( "github.com/kcp-dev/client-go/kubernetes" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/contrib/example-backend-kcp/kubernetes/resources/rbac.go b/contrib/example-backend-kcp/kubernetes/resources/rbac.go index 95a822bc1..9d7e60020 100644 --- a/contrib/example-backend-kcp/kubernetes/resources/rbac.go +++ b/contrib/example-backend-kcp/kubernetes/resources/rbac.go @@ -21,7 +21,6 @@ import ( kubeclient "github.com/kcp-dev/client-go/kubernetes" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/contrib/example-backend-kcp/kubernetes/resources/secret.go b/contrib/example-backend-kcp/kubernetes/resources/secret.go index fa769c97d..1889fdcbc 100644 --- a/contrib/example-backend-kcp/kubernetes/resources/secret.go +++ b/contrib/example-backend-kcp/kubernetes/resources/secret.go @@ -21,7 +21,6 @@ import ( "github.com/kcp-dev/client-go/kubernetes" "github.com/kcp-dev/logicalcluster/v3" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/contrib/example-backend-kcp/options/options.go b/contrib/example-backend-kcp/options/options.go index c33479e5d..172371aef 100644 --- a/contrib/example-backend-kcp/options/options.go +++ b/contrib/example-backend-kcp/options/options.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/spf13/pflag" - "k8s.io/component-base/logs" logsv1 "k8s.io/component-base/logs/api/v1" diff --git a/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/bootstrap.go b/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/bootstrap.go new file mode 100644 index 000000000..eeee5577d --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/bootstrap.go @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Kube Bind 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 provider + +import ( + "context" + "embed" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + + "github.com/kube-bind/kube-bind/pkg/bootstrap" +) + +//go:embed *.yaml +var raw embed.FS + +func Bootstrap(t *testing.T, discoveryClient discovery.DiscoveryInterface, dynamicClient dynamic.Interface, batteriesIncluded sets.Set[string]) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + err := bootstrap.Bootstrap(ctx, discoveryClient, dynamicClient, batteriesIncluded, raw) + require.NoError(t, err) +} diff --git a/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-foo.yaml b/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-foo.yaml new file mode 100644 index 000000000..32dbcb06e --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-foo.yaml @@ -0,0 +1,47 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: foos.bar.io + labels: + kube-bind.io/exported: "true" +spec: + group: bar.io + versions: + - name: v1alpha1 + served: true + storage: true + schema: + # schema used for validation + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + deploymentName: + type: string + replicas: + type: integer + minimum: 1 + maximum: 10 + status: + type: object + properties: + availableReplicas: + type: integer + phase: + type: string + enum: + - Pending + - Running + - Succeeded + - Failed + - Unknown + # subresources for the custom resource + subresources: + # enables the status subresource + status: {} + names: + kind: Foo + plural: foos + scope: Cluster diff --git a/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-mangodb.yaml b/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-mangodb.yaml new file mode 100644 index 000000000..0ec795060 --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/bind/fixtures/provider/crd-mangodb.yaml @@ -0,0 +1,58 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: mangodbs.mangodb.com + labels: + kube-bind.io/exported: "true" +spec: + group: mangodb.com + names: + kind: MangoDB + listKind: MangoDBList + plural: mangodbs + singular: mangodb + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + tier: + type: string + enum: + - Dedicated + - Shared + default: Shared + region: + type: string + default: us-east-1 + minLength: 1 + backup: + type: boolean + default: false + tokenSecret: + type: string + minLength: 1 + required: + - tokenSecret + status: + type: object + properties: + phase: + type: string + enum: + - Pending + - Running + - Succeeded + - Failed + - Unknown + required: + - spec diff --git a/contrib/example-backend-kcp/test/e2e/bind/happy-case_test.go b/contrib/example-backend-kcp/test/e2e/bind/happy-case_test.go new file mode 100644 index 000000000..e1173d881 --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/bind/happy-case_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2025 The Kube Bind 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 bind + +import ( + "context" + "fmt" + "path/filepath" + "testing" + "time" + + "github.com/headzoo/surf" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned" + "github.com/kcp-dev/logicalcluster/v3" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/yaml" + + bootstrap "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/config/kube-bind" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/test/e2e/framework" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" + providerfixtures "github.com/kube-bind/kube-bind/test/e2e/bind/fixtures/provider" +) + +func TestClusterScoped(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + restConfig := framework.ClientConfig(t) + ret := rest.CopyConfig(restConfig) + + wsAdminKubeconfigPath := filepath.Join(framework.WorkDir, "admin.kubeconfig") + err := clientcmd.WriteToFile(framework.RestToKubeconfig(ret, ""), wsAdminKubeconfigPath) + if err != nil { + t.Fatalf("Failed to write kubeconfig: %v", err) + } + + t.Logf("Bootstrapping backend with [%s]", wsAdminKubeconfigPath) + err = framework.BootstrapBackend(t, "--kubeconfig="+wsAdminKubeconfigPath) + if err != nil { + t.Fatalf("Failed to bootstrap backend: %v", err) + } + + t.Logf("Starting backend with random port") + addr, _ := framework.StartBackend(t, restConfig, "--kubeconfig="+wsAdminKubeconfigPath, "--listen-port=0", "--consumer-scope="+string(kubebindv1alpha1.ClusterScope)) + t.Logf("Creating provider workspace") + providerConfig, _ := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("provider-")) + + t.Logf("Creating CRDs on provider side") + providerfixtures.Bootstrap(t, framework.DiscoveryClient(t, providerConfig), framework.DynamicClient(t, providerConfig), nil) + + // bind the provider workspace to the backend + t.Logf("Binding provider workspace to backend") + kcpClient, err := kcpclientset.NewForConfig(providerConfig) + if err != nil { + t.Fatalf("Failed to create kcp client: %v", err) + } + + provider := logicalcluster.NewPath("root:kube-bind") + err = bootstrap.BindAPIExport(ctx, kcpClient, "kube-bind.io", provider) + + t.Logf("Creating consumer workspace") + consumerConfig, _ := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("consumer-")) + + serviceGVR := schema.GroupVersionResource{Group: "mangodb.com", Version: "v1alpha1", Resource: "mangodbs"} + + mangodbInstance := ` + apiVersion: mangodb.com/v1alpha1 + kind: MangoDB + metadata: + name: test + spec: + tokenSecret: credentials + ` + + t.Logf("Bound service dry run") + iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams() + authURLDryRunCh := make(chan string, 1) + go simulateBrowser(t, authURLDryRunCh, serviceGVR.Resource) + framework.Bind(t, iostreams, authURLDryRunCh, nil, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector", "--dry-run") + _, err := yaml.YAMLToJSON(bufOut.Bytes()) + require.NoError(t, err) + + time.Sleep(5 * time.Minute) + ctx.Done() +} + +func simulateBrowser(t *testing.T, authURLCh chan string, resource string) { + browser := surf.NewBrowser() + authURL := <-authURLCh + + t.Logf("Browsing to auth URL: %s", authURL) + err := browser.Open(authURL) + require.NoError(t, err) + + t.Logf("Waiting for browser to be at /resources") + framework.BrowerEventuallyAtPath(t, browser, "/resources") + + t.Logf("Clicking %s", resource) + err = browser.Click("a." + resource) + require.NoError(t, err) + + t.Logf("Waiting for browser to be forwarded to client") + framework.BrowerEventuallyAtPath(t, browser, "/callback") +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/backend.go b/contrib/example-backend-kcp/test/e2e/framework/backend.go new file mode 100644 index 000000000..057d5d4ef --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/backend.go @@ -0,0 +1,165 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "context" + "encoding/base64" + "fmt" + "net" + "os" + "testing" + "time" + + dexapi "github.com/dexidp/dex/api/v2" + "github.com/gorilla/securecookie" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + grpcinsecure "google.golang.org/grpc/credentials/insecure" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + + backend "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/backend" + bootstrap "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap" + bootstrapoption "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/bootstrap/options" + "github.com/kube-bind/kube-bind/contrib/example-backend-kcp/options" +) + +func StartBackend(t *testing.T, clientConfig *rest.Config, args ...string) (net.Addr, *backend.Server) { + signingKey := securecookie.GenerateRandomKey(32) + if len(signingKey) == 0 { + panic("error creating signing key") + } + + return StartBackendWithoutDefaultArgs(t, clientConfig, append([]string{ + "--oidc-issuer-client-secret=ZXhhbXBsZS1hcHAtc2VjcmV0", + "--oidc-issuer-client-id=kube-bind", + "--oidc-issuer-url=http://127.0.0.1:5556/dex", + "--workspace-path=root:kube-bind", + "--apiexport-name=kube-bind.io", + "--cookie-signing-key=" + base64.StdEncoding.EncodeToString(signingKey), + }, args...)...) +} + +func BootstrapBackend(t *testing.T, args ...string) error { + return StartBootstrapWithoutDefaultArgs(t, args...) +} + +func StartBootstrapWithoutDefaultArgs(t *testing.T, args ...string) error { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + fs := pflag.NewFlagSet("example-backend-kcp-bootstrap", pflag.ContinueOnError) + + bootstrapoptions := bootstrapoption.NewOptions() + bootstrapoptions.AddFlags(fs) + + err := fs.Parse(args) + require.NoError(t, err) + + // create init server + completed, err := bootstrapoptions.Complete() + if err != nil { + return err + } + if err := completed.Validate(); err != nil { + return err + } + + // start server + config, err := bootstrap.NewConfig(completed) + if err != nil { + return err + } + + server, err := bootstrap.NewServer(ctx, config) + if err != nil { + return err + } + return server.Start(ctx) +} + +func StartBackendWithoutDefaultArgs(t *testing.T, clientConfig *rest.Config, args ...string) (net.Addr, *backend.Server) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + fs := pflag.NewFlagSet("example-backend-kcp", pflag.ContinueOnError) + options := options.NewOptions() + options.AddFlags(fs) + err := fs.Parse(args) + require.NoError(t, err) + + // use a random port via an explicit listener. Then add a kube-bind- client to dex + // with the callback URL set to the listener's address. + options.Serve.Listener, err = net.Listen("tcp", "localhost:0") + require.NoError(t, err) + addr := options.Serve.Listener.Addr() + _, port, err := net.SplitHostPort(addr.String()) + require.NoError(t, err) + options.OIDC.IssuerClientID = "kube-bind-" + port + createDexClient(t, addr) + + completed, err := options.Complete() + require.NoError(t, err) + + config, err := backend.NewConfig(completed) + require.NoError(t, err) + + server, err := backend.NewServer(ctx, config) + require.NoError(t, err) + + server.OptionallyStartInformers(ctx) + err = server.Run(ctx) + os.Exit(1) + require.NoError(t, err) + t.Logf("backend listening on %s", addr) + + return addr, server +} + +func createDexClient(t *testing.T, addr net.Addr) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + _, port, err := net.SplitHostPort(addr.String()) + require.NoError(t, err) + conn, err := grpc.NewClient("127.0.0.1:5557", grpc.WithTransportCredentials(grpcinsecure.NewCredentials())) + require.NoError(t, err) + defer conn.Close() + client := dexapi.NewDexClient(conn) + + _, err = client.CreateClient(ctx, &dexapi.CreateClientReq{ + Client: &dexapi.Client{ + Id: "kube-bind-" + port, + Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0", + RedirectUris: []string{fmt.Sprintf("http://%s/callback", addr)}, + Public: true, + Name: "kube-bind on port " + port, + }, + }) + require.NoError(t, err) + + t.Cleanup(func() { + ctx, cancel := context.WithDeadline(context.Background(), metav1.Now().Add(10*time.Second)) + defer cancel() + conn, err := grpc.NewClient("127.0.0.1:5557", grpc.WithTransportCredentials(grpcinsecure.NewCredentials())) + require.NoError(t, err) + _, err = dexapi.NewDexClient(conn).DeleteClient(ctx, &dexapi.DeleteClientReq{Id: "kube-bind-" + port}) + require.NoError(t, err) + }) +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/bind.go b/contrib/example-backend-kcp/test/e2e/framework/bind.go new file mode 100644 index 000000000..28869467d --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/bind.go @@ -0,0 +1,106 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "bytes" + "context" + "io" + "os" + "os/exec" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "k8s.io/cli-runtime/pkg/genericclioptions" + + bindapiserviceplugin "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-apiservice/plugin" + bindplugin "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind/plugin" +) + +func Bind(t *testing.T, iostreams genericclioptions.IOStreams, authURLCh chan<- string, invocations chan<- SubCommandInvocation, positionalArg string, flags ...string) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + args := flags + if positionalArg != "" { + args = append(args, positionalArg) + } + t.Logf("kubectl bind %s", strings.Join(args, " ")) + + opts := bindplugin.NewBindOptions(iostreams) + cmd := &cobra.Command{} + opts.AddCmdFlags(cmd) + err := cmd.Flags().Parse(flags) + require.NoError(t, err) + + err = opts.Complete([]string{positionalArg}) + require.NoError(t, err) + err = opts.Validate() + require.NoError(t, err) + + opts.Runner = func(cmd *exec.Cmd) error { + bs, err := io.ReadAll(cmd.Stdin) + if err != nil { + return err + } + if invocations != nil { + invocations <- SubCommandInvocation{ + Executable: cmd.Args[0], + Args: cmd.Args[1:], + Stdin: bs, + } + } + t.Logf("Running command: %s\nstdin:\n", cmd.String()) + t.Logf("%s", bs) + + return nil + } + err = opts.Run(ctx, authURLCh) + require.NoError(t, err) +} + +type SubCommandInvocation struct { + Executable string + Args []string + Stdin []byte +} + +func BindAPIService(t *testing.T, stdin []byte, positionalArg string, flags ...string) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + args := flags + if positionalArg != "" { + args = append(args, positionalArg) + } + t.Logf("kubectl bind apiservice %s", strings.Join(args, " ")) + + opts := bindapiserviceplugin.NewBindAPIServiceOptions(genericclioptions.IOStreams{In: bytes.NewReader(stdin), Out: os.Stdout, ErrOut: os.Stderr}) + cmd := &cobra.Command{} + opts.AddCmdFlags(cmd) + err := cmd.Flags().Parse(flags) + require.NoError(t, err) + + err = opts.Complete([]string{positionalArg}) + require.NoError(t, err) + err = opts.Validate() + require.NoError(t, err) + err = opts.Run(ctx) + require.NoError(t, err) +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/browser.go b/contrib/example-backend-kcp/test/e2e/framework/browser.go new file mode 100644 index 000000000..540343d00 --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/browser.go @@ -0,0 +1,37 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "testing" + "time" + + "github.com/headzoo/surf/browser" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/wait" +) + +func BrowerEventuallyAtPath(t *testing.T, browser *browser.Browser, path string) { + require.Eventuallyf(t, func() bool { + if browser.Url().Path == path { + t.Logf("Browser is at %s, waiting for path %s", browser.Url(), path) + return true + } + t.Logf("Waiting for browser to be at %s, current URL: %s", path, browser.Url()) + return false + }, wait.ForeverTestTimeout, time.Millisecond*100, "Browser is not at path %s", path) +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/clients.go b/contrib/example-backend-kcp/test/e2e/framework/clients.go new file mode 100644 index 000000000..d5cf9d9e1 --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/clients.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "testing" + + "github.com/stretchr/testify/require" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +func DynamicClient(t *testing.T, config *rest.Config) dynamic.Interface { + c, err := dynamic.NewForConfig(config) + require.NoError(t, err) + return c +} + +func KubeClient(t *testing.T, config *rest.Config) kubernetes.Interface { + c, err := kubernetes.NewForConfig(config) + require.NoError(t, err) + return c +} + +func ApiextensionsClient(t *testing.T, config *rest.Config) apiextensionsclient.Interface { + c, err := apiextensionsclient.NewForConfig(config) + require.NoError(t, err) + return c +} + +func DiscoveryClient(t *testing.T, config *rest.Config) discovery.DiscoveryInterface { + c, err := discovery.NewDiscoveryClientForConfig(config) + require.NoError(t, err) + return c +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/kcp.go b/contrib/example-backend-kcp/test/e2e/framework/kcp.go new file mode 100644 index 000000000..d1f5ef72e --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/kcp.go @@ -0,0 +1,145 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "context" + "crypto/rand" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + corev1alpha1 "github.com/kcp-dev/kcp/pkg/apis/core/v1alpha1" + tenancyv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/tenancy/v1alpha1" + "github.com/martinlindhe/base36" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +type ( + ClusterWorkspaceOption func(ws *tenancyv1alpha1.Workspace) +) + +var ( + kcpScheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(tenancyv1alpha1.AddToScheme(kcpScheme)) +} + +func WithName(s string, formatArgs ...interface{}) ClusterWorkspaceOption { + return func(ws *tenancyv1alpha1.Workspace) { + ws.Name = fmt.Sprintf(s, formatArgs...) + ws.GenerateName = "" + } +} + +func WithGenerateName(s string, formatArgs ...interface{}) ClusterWorkspaceOption { + return func(ws *tenancyv1alpha1.Workspace) { + s = fmt.Sprintf(s, formatArgs...) + // Workspace.ObjectMeta.GenerateName is broken in kcp: https://github.com/kcp-dev/kcp/pull/2193 + // + // ws.GenerateName = fmt.Sprintf(s, formatArgs...) + // if !strings.HasSuffix(ws.GenerateName, "-") { + // ws.GenerateName += "-" + // } + if !strings.HasSuffix(s, "-") { + s += "-" + } + + token := make([]byte, 4) + rand.Read(token) //nolint:errcheck + base36hash := strings.ToLower(base36.EncodeBytes(token)) + ws.Name = s + base36hash[:5] + ws.GenerateName = "" + } +} + +func NewWorkspace(t *testing.T, config *rest.Config, options ...ClusterWorkspaceOption) (*rest.Config, string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + + httpClient, err := rest.HTTPClientFor(config) + require.NoError(t, err) + mapper, err := apiutil.NewDynamicRESTMapper(config, httpClient) + require.NoError(t, err) + tenancyClient, err := client.New(config, client.Options{Scheme: kcpScheme, Mapper: mapper}) + require.NoError(t, err) + + ws := &tenancyv1alpha1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "e2e-workspace-", + }, + } + + // workaround broken GenerateName for workspaces: https://github.com/kcp-dev/kcp/pull/2193 + ws.ObjectMeta.Name = ws.ObjectMeta.GenerateName + ws.ObjectMeta.GenerateName = "" + token := make([]byte, 4) + rand.Read(token) //nolint:errcheck + base36hash := strings.ToLower(base36.EncodeBytes(token)) + ws.Name += base36hash[:5] + + for _, opt := range options { + opt(ws) + } + err = tenancyClient.Create(ctx, ws) + require.NoError(t, err) + + ret := rest.CopyConfig(config) + ret.Host += ":" + ws.Name + + wsKubeconfigPath := filepath.Join(WorkDir, ws.Name+".kubeconfig") + err = clientcmd.WriteToFile(RestToKubeconfig(ret, ""), wsKubeconfigPath) + require.NoError(t, err) + + t.Cleanup(func() { + ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(time.Second*30)) + defer cancelFn() + + err := tenancyClient.Get(ctx, client.ObjectKey{Name: ws.Name}, ws) + if errors.IsNotFound(err) { + return + } + require.NoError(t, err) + tenancyClient.Delete(ctx, ws) //nolint:errcheck + os.Remove(wsKubeconfigPath) + }) + + require.Eventually(t, func() bool { + background, cancel := context.WithDeadline(context.Background(), metav1.Now().Add(10*time.Second)) + defer cancel() + err := tenancyClient.Get(background, client.ObjectKey{Name: ws.Name}, ws) + require.NoError(t, err) + return ws.Status.Phase == corev1alpha1.LogicalClusterPhaseReady + }, wait.ForeverTestTimeout, time.Millisecond*100, "failed to wait for workspace %s to become ready", ws.Name) + t.Logf("Created %s workspace %s", ws.Spec.Type, ws.Name) + + return ret, wsKubeconfigPath +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/konnector.go b/contrib/example-backend-kcp/test/e2e/framework/konnector.go new file mode 100644 index 000000000..504832b38 --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/konnector.go @@ -0,0 +1,75 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "context" + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + + "github.com/kube-bind/kube-bind/deploy/crd" + "github.com/kube-bind/kube-bind/pkg/konnector" + "github.com/kube-bind/kube-bind/pkg/konnector/options" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha1" +) + +func StartKonnector(t *testing.T, clientConfig *rest.Config, args ...string) *konnector.Server { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + crdClient, err := apiextensionsclient.NewForConfig(clientConfig) + require.NoError(t, err) + err = crd.Create(ctx, + crdClient.ApiextensionsV1().CustomResourceDefinitions(), + metav1.GroupResource{Group: kubebindv1alpha1.GroupName, Resource: "apiservicebindings"}, + ) + require.NoError(t, err) + + fs := pflag.NewFlagSet("konnector", pflag.ContinueOnError) + options := options.NewOptions() + options.AddFlags(fs) + err = fs.Parse(args) + require.NoError(t, err) + + completed, err := options.Complete() + require.NoError(t, err) + + config, err := konnector.NewConfig(completed) + require.NoError(t, err) + + server, err := konnector.NewServer(config) + require.NoError(t, err) + prepared, err := server.PrepareRun(ctx) + require.NoError(t, err) + + prepared.OptionallyStartInformers(ctx) + go func() { + err := prepared.Run(ctx) + select { + case <-ctx.Done(): + default: + require.NoError(t, err) + } + }() + + return server +} diff --git a/contrib/example-backend-kcp/test/e2e/framework/kubeconfig.go b/contrib/example-backend-kcp/test/e2e/framework/kubeconfig.go new file mode 100644 index 000000000..41ecaf785 --- /dev/null +++ b/contrib/example-backend-kcp/test/e2e/framework/kubeconfig.go @@ -0,0 +1,63 @@ +/* +Copyright 2022 The Kube Bind 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 framework + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func ClientConfig(t *testing.T) *rest.Config { + rules := clientcmd.NewDefaultClientConfigLoadingRules() + rules.ExplicitPath = os.Getenv("KUBECONFIG") + clientcmdConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, nil) + config, err := clientcmdConfig.ClientConfig() + require.NoError(t, err) + + return config +} + +func RestToKubeconfig(config *rest.Config, namespace string) clientcmdapi.Config { + return clientcmdapi.Config{ + Kind: "Config", + APIVersion: "v1", + Clusters: map[string]*clientcmdapi.Cluster{ + "default": { + Server: config.Host, + CertificateAuthorityData: config.CAData, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "default": { + Cluster: "default", + Namespace: namespace, + AuthInfo: "default", + }, + }, + CurrentContext: "default", + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "default": { + Token: config.BearerToken, + }, + }, + } +} diff --git a/contrib/example-backend-kcp/tools.go b/contrib/example-backend-kcp/tools.go index 396998274..bdac2bfe5 100644 --- a/contrib/example-backend-kcp/tools.go +++ b/contrib/example-backend-kcp/tools.go @@ -17,8 +17,8 @@ limitations under the License. package main import ( + _ "github.com/kcp-dev/code-generator/v3/cmd/cluster-client-gen" _ "github.com/kcp-dev/kcp/sdk/cmd/apigen" - _ "k8s.io/code-generator/cmd/applyconfiguration-gen" _ "k8s.io/code-generator/cmd/client-gen" _ "k8s.io/code-generator/cmd/deepcopy-gen" diff --git a/docs/content/setup/setup-with-gke.md b/docs/content/setup/setup-with-gke.md new file mode 100644 index 000000000..69fe917d3 --- /dev/null +++ b/docs/content/setup/setup-with-gke.md @@ -0,0 +1,324 @@ +# Deplopying Kube-Bind into GKE clusters + +This guide will walk you through setting up kube-bind between two Kubernetes clusters running in GKE, where + +**Backend cluster**: + * Deploys dex, cert-manager and kube-bind/example-backend + * Provides kube-bind compatible backend for MangoDB resources + +**App cluster**: + * Provides an application consuming MangoDBs + +## Pre-requisites + +To start, you'll need following tools available in your system or a VM: + +* [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) +* [`kubectl`](https://kubernetes.io/docs/tasks/tools/) +* [`kubectl-bind`](https://github.com/kube-bind/kube-bind/releases/latest) (a kubectl plugin) +* [`helm`](https://helm.sh/docs/intro/quickstart/) +* [`jq`](https://jqlang.github.io/jq/download/) + +To install `kubectl-bind` plugin, please download the archive for your platform from the link above, extract it, and place the `kubectl-bind` executable in your system's `$PATH`. + +## Provider cluster + +The provider cluster we'll prepare in this section will provide a kube-bind compatible backend that will provide a controller for a demo resource "MangoDB" we'll consume in another cluster later. + +> What is MangoDB? It is just an example CRD to demonstrate kube-bind's capabilities and testing, without any workloads. See its definition in [/test/e2e/bind/fixtures/provider/crd-mangodb.yaml](/test/e2e/bind/fixtures/provider/crd-mangodb.yaml). + +### Step zero: Images + +Get images either from our Github Container registry: https://github.com/orgs/kube-bind/packages?repo_name=kube-bind +or build them locally: `make build image-local` and publish to your own registry. + +### Step one: create the Backend cluster + +Get yourself a vanilla GKE cluster: + +``` +gcloud container clusters get-credentials kube-bind-provider --region us-central1 --project kube-bind +export KUBECONFIG=provider.kubeconfig +``` + + +### Step two: deploy an identity provider + +kube-bind relies on OAuth2 for securely authenticating consumer and producer clusters. There are many ways to handle that in Kubernetes, for example with [DEX IDP](https://github.com/dexidp/dex). It depends on cert-manager, which we'll deploy first: + +```sh +helm repo add jetstack https://charts.jetstack.io +helm install \ + --create-namespace \ + --namespace pki \ + --version v1.16.2 \ + --set crds.enabled=true \ + cert-manager jetstack/cert-manager +``` + + +Homepage URL: The public URL where Dex will be hosted, e.g., https://dex.dev.genericcontrolplane.io +Authorization callback URL: This is the most critical part. It must be your Dex public URL followed by /callback. For example: https://dex.dev.genericcontrolplane.io/callback. + +For this write-up we use demo secrets. + +```sh +cat << EOF_ClusterIssuer | +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + # The ACME server URL for Let's Encrypt's staging environment. + server: https://acme-staging-v02.api.letsencrypt.org/directory + # Email address used for ACME registration. + email: mangirdas@judeikis.lt + # Name of a secret used to store the ACME account private key. + privateKeySecretRef: + name: letsencrypt-staging-account-key + # Enable the HTTP-01 challenge provider. + solvers: + - http01: + ingress: + class: gce # This is the default Ingress class on GKE +EOF_ClusterIssuer +kubectl apply -f - +``` + +```sh +helm repo add dex https://charts.dexidp.io +cat << EOF_DEXDeploymentConfig | +config: + # The issuer URL is still the public HTTPS URL. + issuer: https://dex.dev.genericcontrolplane.io + + storage: + type: kubernetes + config: + inCluster: true + + # CORRECTED: Dex itself should now listen on plain HTTP inside the cluster. + # TLS termination will be handled by the Ingress. + web: + http: 0.0.0.0:5556 + + # Your connectors and staticClients remain the same... + connectors: + - type: mockCallback + id: mock + name: Example + staticClients: + - id: kube-bind + redirectURIs: + - 'https://dex.dev.genericcontrolplane.io/callback' + name: 'Kube Bind' + secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + +# This section configures the Kubernetes Service for Dex. +# It should be ClusterIP because the Ingress will route traffic to it. +service: + type: ClusterIP + +# This section re-enables and configures the Ingress for cert-manager +ingress: + enabled: true + # Use 'gce' for the default GKE Ingress controller. + # For newer GKE versions, you might not need the class annotation at all. + className: "gce" + annotations: + # Use the staging issuer first for testing. + cert-manager.io/cluster-issuer: "letsencrypt-staging" + # This annotation is for GKE to use a static IP. + kubernetes.io/ingress.global-static-ip-name: "dex-static-ip" + hosts: + - host: dex.dev.genericcontrolplane.io + paths: + - path: / + pathType: ImplementationSpecific + tls: + # This tells the Ingress to use a certificate for the specified host. + # Cert-manager will see this, create a Certificate resource, + # and automatically populate the 'dex-tls' secret with the new cert. + - secretName: dex-tls + hosts: + - dex.dev.genericcontrolplane.io +EOF_DEXDeploymentConfig +helm upgrade -i \ + --create-namespace \ + --namespace dex \ + dex dex/dex \ + -f - +``` + +### Step three: deploy the MangoDB kube-bind backend + +Now we'll deploy a kube-bind--compatible backend for MangoDB. Let's start with kube-bind CRDs: + +```sh +kubectl apply -f deploy/crd +``` + +And now CRDs for MangoDB: + +```sh +kubectl apply -f test/e2e/bind/fixtures/provider/crd-mangodb.yaml +``` + +To set up the MangoDB backend we'll need: +* ServiceAccount and ClusterRoleBinding for kube-bind's user, +* Deployment that runs the MangoDB backend +* Service that exposes the backend's address + +```sh +kubectl create namespace backend +# This is the address that will be used when generating kubeconfigs the App cluster, +# and so we need to be able to reach it from outside. +export BACKEND_KUBE_API_EXTERNAL_ADDRESS="$(kubectl config view --minify -o json | jq '.clusters[0].cluster.server' -r)" +# For demo example let's just bind "cluster-admin" ClusterRole to backend's "default" ServiceAccount. +kubectl create clusterrolebinding backend-admin --clusterrole cluster-admin --serviceaccount backend:default +# Create a new Deployment for the MangoDB backend. +kubectl --namespace backend \ + create deployment mangodb \ + --image ghcr.io/kube-bind/example-backend:v0.5.0-rc1 \ + --port 8080 \ + -- /ko-app/example-backend \ + --listen-address 0.0.0.0:8080 \ + --external-address "${BACKEND_KUBE_API_EXTERNAL_ADDRESS}" \ + --oidc-issuer-client-secret=xxxxxxxxxxxx== \ + --oidc-issuer-client-id=faros \ + --oidc-issuer-url=https://auth.faros.sh \ + --oidc-callback-url=http://xxxxxxxxxx:8080/callback \ + --pretty-name="BigCorp.com" \ + --namespace-prefix="kube-bind-" \ + --cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= \ + --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= +# Expose mangodb's container port 8080 as a NodePort at 30080. We've already configured +# Kind to expose 30800 at host's 8080. +kubectl --namespace backend \ + create service nodeport mangodb \ + --tcp 8080 \ + --node-port 30080 +``` + +```sh +kubectl --namespace backend expose deployment mangodb --type=LoadBalancer --port=8080 --target-port=8080 +``` + + +And that's really all there's to it. After that, you should see a kubectl output similar to this: + +```shell +$ kubectl --namespace backend get all +NAME READY STATUS RESTARTS AGE +pod/mangodb-6ff44cbbf-x7cjm 1/1 Running 0 100s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/mangodb NodePort 10.96.10.212 8080:30080/TCP 100s + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/mangodb 1/1 1 1 100s + +NAME DESIRED CURRENT READY AGE +replicaset.apps/mangodb-6ff44cbbf 1 1 1 100s +``` + +## App cluster + +The App cluster will consume MangoDB CRs provided by the Backend. + +### Step one: create the App cluster + +Again, let's start by stashing the host's external IP in a variable as we're going to use it often (possibly the same one as for the Backend cluster): + +```sh +export APP_HOST_IP="$(hostname -i | cut -d' ' -f1)" +``` + +Create a Kind cluster named "app": + +```sh +cat << EOF_AppClusterDefinition | kind create cluster --config=- +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +name: app +networking: + apiServerAddress: ${APP_HOST_IP} +EOF_AppClusterDefinition +``` + +### Binding MangoDB backend + +Now we'll bring in MangoDB CRDs from the Backend cluster (you can run `kubectl get crds` to see there are none yet): + +```sh +$ kubectl bind http://${BACKEND_HOST_IP}:8080/export +DISCLAIMER: This is a prototype. It will change in incompatible ways at any time. + +πŸ“¦ Created kube-bind namespace. + + + +To authenticate, visit in your browser: + + http://${BACKEND_HOST_IP}:8080/authorize?c=3QnoGw&p=39595&s=b2YLH6 +``` + +The client is now waiting for you to visit the address similar to the one displayed in the output above. After completing the steps to create an OAuth2 token, it is then used by the kube-bind backend to pass the ServiceAccount's kubeconfig (in the Backend cluster) to the App cluster securely: +1. on the "Log in to dex" landing page, select "Log in with Example", +2. on the "Grant Access" page, click the "Grant Access" button, +3. lastly, click "Bind" when the page displays the mangodb resource. + +Go back to the terminal where `kubectl bind` command was run, and you should see the following output: +``` +πŸ”‘ Successfully authenticated to http://${BACKEND_HOST_IP}:8080/export +πŸ”’ Created secret kube-bind/kubeconfig-x9bd5 for host https://${BACKEND_HOST_IP}:34595, namespace kube-bind-gfsqn +πŸš€ Executing: kubectl bind apiservice --remote-kubeconfig-namespace kube-bind --remote-kubeconfig-name kubeconfig-x9bd5 -f - +✨ Use "-o yaml" and "--dry-run" to get the APIServiceExportRequest. + and pass it to "kubectl bind apiservice" directly. Great for automation. +πŸš€ Deploying konnector v0.4.6 to namespace kube-bind. + Waiting for the konnector to be ready.............. +βœ… Created APIServiceBinding mangodbs.mangodb.com + +NAME PROVIDER READY MESSAGE AGE +apiservicebinding.kube-bind.io/mangodbs.mangodb.com False Pending 0s +``` + +### Step two: demo time! + +Let's see if we have CRDs for the MangoDB resource: + +```sh +$ kubectl get crds +NAME CREATED AT +apiservicebindings.kube-bind.io 2024-12-19T08:46:13Z +mangodbs.mangodb.com 2024-12-19T08:46:17Z +``` + +We do! Now create a CR for it: + +```sh +kubectl create -f - << EOF_MangoDBDefinition +apiVersion: mangodb.com/v1alpha1 +kind: MangoDB +metadata: + name: bob-the-database +spec: + tokenSecret: my-secret + region: eu-west-1 + tier: Shared +EOF_MangoDBDefinition +kubectl describe mangodb bob-the-database +``` + + +kubectl bind http://api.faros.sh:8080/export --konnector-image ghcr.io/kube-bind/konnector:v0.5.0-rc1 + +In the provider now: + +```sh +kubectl patch mangodb bob-the-database -n kube-bind-znxkg-default --subresource status --type='merge' -p='{"status":{"phase":"Running"}}' +``` + + +kind creaet cluster --name jon \ No newline at end of file diff --git a/dump/apiserviceexport.yaml b/dump/apiserviceexport.yaml new file mode 100644 index 000000000..506629c6c --- /dev/null +++ b/dump/apiserviceexport.yaml @@ -0,0 +1,39 @@ +apiVersion: kube-bind.io/v1alpha1 +kind: APIServiceExport +metadata: + name: my-mangodb-export +spec: + group: mangodb.com # Replace with the actual API group of the target resource + names: + kind: MangoDB + plural: mangodbs + singular: mangodb + scope: Namespaced + informerScope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + # Use the schema from the provided CRD with any necessary modifications + type: object + properties: + spec: + type: object + properties: + tier: + type: string + enum: + - Dedicated + - Shared + default: Shared + status: + type: object + properties: + phase: + type: string + required: + - spec + subresources: + status: {} \ No newline at end of file diff --git a/dump/apiserviceexportrequest.yaml b/dump/apiserviceexportrequest.yaml new file mode 100644 index 000000000..2a21ea9be --- /dev/null +++ b/dump/apiserviceexportrequest.yaml @@ -0,0 +1,11 @@ +apiVersion: kube-bind.io/v1alpha1 +kind: APIServiceExportRequest +metadata: + name: my-api-export-request +spec: + resources: + - resources: # List of resources to be exported + group: my-api-group # API group name (optional, defaults to "") + resource: my-api-resource # Name of the resource + versions: # List of API versions to be exported (optional) + - v1beta1 diff --git a/dump/apiservicenamespaces.yaml b/dump/apiservicenamespaces.yaml new file mode 100644 index 000000000..af0160cf4 --- /dev/null +++ b/dump/apiservicenamespaces.yaml @@ -0,0 +1,4 @@ +apiVersion: kube-bind.io/v1alpha1 +kind: APIServiceNamespace +metadata: + name: my-consumer-namespace \ No newline at end of file diff --git a/dump/clusterbindings.yaml b/dump/clusterbindings.yaml new file mode 100644 index 000000000..29d0c4fcb --- /dev/null +++ b/dump/clusterbindings.yaml @@ -0,0 +1,8 @@ +# This YAML object represents a ClusterBinding resource + +apiVersion: kube-bind.io/v1alpha1 +kind: ClusterBinding +metadata: + name: cluster +spec: + providerPrettyName: "bob" \ No newline at end of file From 5e4667ad3687cbd71137d9182435ef367ca5f3ae Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Thu, 7 Aug 2025 08:18:54 +0300 Subject: [PATCH 7/8] add headers --- .../docker-compose/cert-generator/Dockerfile | 14 ++++++++++++++ .../cert-generator/generate_certs.sh | 17 ++++++++++++++++- .../hack/docker-compose/kcp/Dockerfile | 14 ++++++++++++++ .../hack/docker-compose/kube-bind/Dockerfile | 14 ++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile index ecc5b2d27..df1bd7b9f 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile +++ b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/Dockerfile @@ -1,3 +1,17 @@ +# Copyright 2025 The Kube Bind 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. + # Stage 1: Build genkey tool FROM golang:1.23-alpine AS builder diff --git a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh index 6120fbd7e..75e79aea6 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh +++ b/contrib/example-backend-kcp/hack/docker-compose/cert-generator/generate_certs.sh @@ -1,4 +1,19 @@ -#!/bin/bash +#!/usr/bin/env bash + +# Copyright 2025 The Kube Bind 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 -e CERT_DIR=/certs diff --git a/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile index f22150ae5..694fe7a15 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile +++ b/contrib/example-backend-kcp/hack/docker-compose/kcp/Dockerfile @@ -1,3 +1,17 @@ +# Copyright 2025 The Kube Bind 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. + # Stage 1: Build KCP FROM golang:1.23-alpine AS builder diff --git a/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile b/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile index b12d042f6..858b30a2f 100644 --- a/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile +++ b/contrib/example-backend-kcp/hack/docker-compose/kube-bind/Dockerfile @@ -1,3 +1,17 @@ +# Copyright 2025 The Kube Bind 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. + # Stage 1: Build Kube-Bind FROM golang:1.23-alpine AS builder From 462c5288ba17fe8066c427afc5d4064cea5e8565 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Thu, 7 Aug 2025 08:31:25 +0300 Subject: [PATCH 8/8] add mkdir --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 5ebfd60b2..e83d17fab 100644 --- a/Makefile +++ b/Makefile @@ -135,6 +135,7 @@ build: require-jq require-go require-git verify-go-versions ## Build the project .PHONY: build-all build-all: + mkdir -p bin GOOS=$(OS) GOARCH=$(ARCH) $(MAKE) build WHAT=./cmd/... install: WHAT ?= ./cmd/... ./cli/cmd/...