From c1dcfc48e4693883fcf32b9338316d23d2668039 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Thu, 23 Oct 2025 09:12:15 +0300 Subject: [PATCH 1/3] Add CatalogAPI and Template Signed-off-by: Mangirdas Judeikis On-behalf-of: @SAP mangirdas.judeikis@sap.com --- .coderabbit.yaml | 4 +- .gitignore | 3 +- README.md | 22 ++ .../serviceexportrequest_controller.go | 1 + backend/http/handler.go | 122 +++----- backend/kubernetes/manager.go | 32 +++ backend/kubernetes/resources/resources.go | 1 - backend/template/login.html | 49 ---- backend/template/resources.gohtml | 224 ++++++++++++++- .../bind-apiservice/plugin/servicebindings.go | 100 +++---- contrib/kcp/README.md | 14 +- .../deploy/examples/collection-wildwest.yaml | 9 + .../kcp/deploy/examples/template-cowboys.yaml | 32 +++ .../deploy/examples/template-sheriffs.yaml | 35 +++ .../resources/apiexport-kube-bind.io.yaml | 14 +- ...chema-apiservicebindings.kube-bind.io.yaml | 4 +- ...apiserviceexportrequests.kube-bind.io.yaml | 2 +- ...schema-apiserviceexports.kube-bind.io.yaml | 4 +- ...piserviceexporttemplates.kube-bind.io.yaml | 268 +++++++++++++++++ ...sourceschema-collections.kube-bind.io.yaml | 128 +++++++++ contrib/kcp/test/e2e/browser.go | 16 +- contrib/kcp/test/e2e/kcp_test.go | 11 +- .../crd/kube-bind.io_apiservicebindings.yaml | 2 +- ...kube-bind.io_apiserviceexportrequests.yaml | 2 +- .../crd/kube-bind.io_apiserviceexports.yaml | 2 +- ...ube-bind.io_apiserviceexporttemplates.yaml | 272 ++++++++++++++++++ deploy/crd/kube-bind.io_collections.yaml | 132 +++++++++ deploy/examples/collection.yaml | 8 + deploy/examples/template-foo.yaml | 33 +++ deploy/examples/template-mangodb.yaml | 23 ++ docs/content/.pages | 1 + docs/content/index.md | 7 + docs/content/setup/index.md | 8 + docs/content/setup/quickstart.md | 3 + docs/content/usage/.pages | 4 + docs/content/usage/index.md | 16 ++ docs/content/usage/migration.md | 24 ++ .../claimedresources_controller.go | 6 +- .../namespacelifecycle_controller.go | 2 +- pkg/resources/resources.go | 6 +- pkg/resources/resources_test.go | 18 +- .../v1alpha2/apiserviceexportrequest_types.go | 2 +- .../apiserviceexporttemplate_types.go | 98 +++++++ .../kubebind/v1alpha2/collection_types.go | 94 ++++++ sdk/apis/kubebind/v1alpha2/register.go | 4 + .../v1alpha2/zz_generated.deepcopy.go | 244 +++++++++++++++- .../typed/kubebind/v1alpha2/collection.go | 70 +++++ .../kubebind/v1alpha2/fake/fake_collection.go | 50 ++++ .../v1alpha2/fake/fake_kubebind_client.go | 4 + .../kubebind/v1alpha2/generated_expansion.go | 2 + .../kubebind/v1alpha2/kubebind_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../kubebind/v1alpha2/collection.go | 102 +++++++ .../kubebind/v1alpha2/interface.go | 7 + .../listers/kubebind/v1alpha2/collection.go | 70 +++++ .../kubebind/v1alpha2/expansion_generated.go | 8 + .../bind/fixtures/provider/collection.yaml | 8 + .../bind/fixtures/provider/template-foo.yaml | 33 +++ .../fixtures/provider/template-mangodb.yaml | 33 +++ test/e2e/bind/happy-case_test.go | 79 +---- test/e2e/framework/backend.go | 2 + 61 files changed, 2278 insertions(+), 303 deletions(-) delete mode 100644 backend/template/login.html create mode 100644 contrib/kcp/deploy/examples/collection-wildwest.yaml create mode 100644 contrib/kcp/deploy/examples/template-cowboys.yaml create mode 100644 contrib/kcp/deploy/examples/template-sheriffs.yaml create mode 100644 contrib/kcp/deploy/resources/apiresourceschema-apiserviceexporttemplates.kube-bind.io.yaml create mode 100644 contrib/kcp/deploy/resources/apiresourceschema-collections.kube-bind.io.yaml create mode 100644 deploy/crd/kube-bind.io_apiserviceexporttemplates.yaml create mode 100644 deploy/crd/kube-bind.io_collections.yaml create mode 100644 deploy/examples/collection.yaml create mode 100644 deploy/examples/template-foo.yaml create mode 100644 deploy/examples/template-mangodb.yaml create mode 100644 docs/content/usage/.pages create mode 100644 docs/content/usage/index.md create mode 100644 docs/content/usage/migration.md create mode 100644 sdk/apis/kubebind/v1alpha2/apiserviceexporttemplate_types.go create mode 100644 sdk/apis/kubebind/v1alpha2/collection_types.go create mode 100644 sdk/client/clientset/versioned/typed/kubebind/v1alpha2/collection.go create mode 100644 sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_collection.go create mode 100644 sdk/client/informers/externalversions/kubebind/v1alpha2/collection.go create mode 100644 sdk/client/listers/kubebind/v1alpha2/collection.go create mode 100644 test/e2e/bind/fixtures/provider/collection.yaml create mode 100644 test/e2e/bind/fixtures/provider/template-foo.yaml create mode 100644 test/e2e/bind/fixtures/provider/template-mangodb.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 435d9be63..0f07ecb7a 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -30,8 +30,8 @@ reviews: # Specify file patterns to include or exclude in a review using glob patterns (e.g., !dist/**, src/**). These patterns also apply to 'git sparse-checkout', including specified patterns and ignoring excluded ones (starting with '!') when cloning the repository. # Default: [] path_filters: [ - sdk/client/**, - '**/*.yaml', + '!sdk/client/**', + '!**/*.yaml', ] # Configuration for auto review diff --git a/.gitignore b/.gitignore index d419328f1..f0d1aa73d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ coverage.* /dex /bin docs/generators/cli-doc/cli-doc -dex/ \ No newline at end of file +dex/ +apiserviceexport.yaml diff --git a/README.md b/README.md index a896fdd9c..1004416f8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,28 @@ All the actions shown between the clusters are done by the konnector, except: th To get familiar with setting up the environment, please check out docs at [kube-bind.io](https://docs.kube-bind.io/main/setup). +## API Changes in v0.6.0 release + +### Catalog API +Introduction of new `Collection` and `APIServiceExportTemplate` CRDs for better service organization: +- **Collections**: Function as folders in the UI, grouping related modules +- **APIServiceExportTemplate**: Group multiple CRDs with their related resources and permission claims and creates `APIServiceExportRequest` + +### Enhanced Permission Claims +Major improvements to `PermissionClaims` in APIServiceExportSpec: +- **NamedResources**: Specify exact resources by name and namespace +- **Combined Selectors**: Use both label selectors AND named resources (both must match) +- **Granular Control**: More precise access control for service resources + +### Provider-side Namespace Management +Enhanced namespace management on the provider side: +- **APIServiceNamespace Controller**: Automatically creates Roles and RoleBindings +- **Namespace Isolation**: Each consumer gets isolated provider-side namespaces +- **RBAC Automation**: Proper permissions created based on scope (namespaced vs cluster-scoped) +- **Namespace Pre-provisioning**: Providers can pre-create namespaces for better UX + +**Important**: When `ClusterScope` mode is used, cluster-wide permissions are created instead of namespaced ones. + ## API Changes in v0.5.0 release Version v0.5.0 includes significant architectural improvements to the API structure: diff --git a/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go b/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go index 2c1969a66..4330a7f78 100644 --- a/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go +++ b/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go @@ -182,6 +182,7 @@ func getBoundSchemaMapper(clusterName string, cl cluster.Cluster) handler.TypedE //+kubebuilder:rbac:groups=kubebind.k8s.io,resources=apiserviceexportrequests/finalizers,verbs=update //+kubebuilder:rbac:groups=kubebind.k8s.io,resources=apiserviceexports,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=kubebind.k8s.io,resources=apiresourceschemas,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kubebind.k8s.io,resources=resources=apiservicenamespaces,verbs=get;list;watch;create // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. diff --git a/backend/http/handler.go b/backend/http/handler.go index db30335d1..b4a0b1e3a 100644 --- a/backend/http/handler.go +++ b/backend/http/handler.go @@ -33,19 +33,15 @@ import ( "github.com/gorilla/securecookie" "golang.org/x/oauth2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" componentbaseversion "k8s.io/component-base/version" "k8s.io/klog/v2" "github.com/kube-bind/kube-bind/backend/kubernetes" - "github.com/kube-bind/kube-bind/backend/kubernetes/resources" "github.com/kube-bind/kube-bind/backend/session" "github.com/kube-bind/kube-bind/backend/template" bindversion "github.com/kube-bind/kube-bind/pkg/version" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" - "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2/helpers" ) var ( @@ -346,12 +342,14 @@ func createSessionState(authCode *AuthCode, token *oauth2.Token) (*session.State } type UISchema struct { - Name string - Version string - Group string - Kind string - Scope string // "Namespaced" or "Cluster" - Resource string + Scope string // "Namespaced" or "Cluster" + + Name string + Description string + + Resources []kubebindv1alpha2.APIServiceExportResource + PermissionClaims []kubebindv1alpha2.PermissionClaim + Namespaces []kubebindv1alpha2.Namespaces // SessionID SessionID string @@ -376,44 +374,26 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { return } - exportedSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster) + templates, err := h.listCollectionTemplates(r.Context(), providerCluster) if err != nil { - logger.Error(err, "failed to get dynamic resources") + logger.Error(err, "failed to get template resources") http.Error(w, "internal error", http.StatusInternalServerError) return } - result := make([]UISchema, 0, len(exportedSchemas)) - for _, item := range exportedSchemas { + result := make([]UISchema, 0, len(templates.Items)) + for _, item := range templates.Items { if !strings.EqualFold(h.scope.String(), string(item.Spec.Scope)) && h.scope != kubebindv1alpha2.ClusterScope { continue } - if len(item.Spec.Versions) == 0 { - logger.Error(fmt.Errorf("no versions found"), "skipping schema", "name", item.Name) - continue - } - // pick first served version - ver := "" - for _, v := range item.Spec.Versions { - if v.Served { - ver = v.Name - break - } - } - if ver == "" { - logger.Error(fmt.Errorf("no served versions found"), "skipping schema", "name", item.Name) - continue - } result = append(result, UISchema{ - Name: item.GetName(), - Kind: item.Spec.Names.Kind, - Scope: string(item.Spec.Scope), - Version: ver, - Group: item.Spec.Group, - // Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it. - Resource: item.Spec.Names.Plural, - SessionID: sessionID, + Name: item.GetName(), + Scope: string(item.Spec.Scope), + PermissionClaims: item.Spec.PermissionClaims, + Resources: item.Spec.Resources, + Namespaces: item.Spec.Namespaces, + SessionID: sessionID, }) } @@ -436,9 +416,7 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { logger := getLogger(r) - group := r.URL.Query().Get("group") - resource := r.URL.Query().Get("resource") - version := r.URL.Query().Get("version") + templateName := r.URL.Query().Get("template") providerCluster := mux.Vars(r)["cluster"] prepareNoCache(w) @@ -466,24 +444,26 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { return } + // Module consist of many resources and permissionClaims. Read it and translate to + template, err := h.kubeManager.GetTemplates(r.Context(), providerCluster, templateName) + if err != nil { + logger.Error(err, "failed to get template") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + request := kubebindv1alpha2.APIServiceExportRequestResponse{ TypeMeta: metav1.TypeMeta{ APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), Kind: "APIServiceExportRequest", }, ObjectMeta: kubebindv1alpha2.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, + Name: templateName, }, Spec: kubebindv1alpha2.APIServiceExportRequestSpec{ - Resources: []kubebindv1alpha2.APIServiceExportRequestResource{ - { - GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource}, - Versions: []string{version}, - }, - }, + Resources: template.Spec.Resources, + PermissionClaims: template.Spec.PermissionClaims, + Namespaces: template.Spec.Namespaces, }, } @@ -543,34 +523,26 @@ func mustRead(f func(name string) ([]byte, error), name string) string { return string(bs) } -func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (kubebindv1alpha2.ExportedSchemas, error) { - labelSelector := labels.Set{ - resources.ExportedCRDsLabel: "true", - } - - parts := strings.SplitN(h.schemaSource, ".", 3) - if len(parts) != 3 { // We check this in validation, but just in case. - return nil, fmt.Errorf("invalid schema source: %q", h.schemaSource) - } - - gvk := schema.GroupVersionKind{ - Kind: parts[0], - Version: parts[1], - Group: parts[2], - } - list, err := h.kubeManager.ListDynamicResources(ctx, cluster, gvk, labelSelector.AsSelector()) +// listCollectionTemplates fetches the list of Collections from the backend cluster. +// Flow is: +// 1. List Collection and check what modules we are targeting +// 2. Get templates from the backend cluster and construct shallow-bound schemas (no crd content). +func (h *handler) listCollectionTemplates(ctx context.Context, cluster string) (*kubebindv1alpha2.APIServiceExportTemplateList, error) { + collections, err := h.kubeManager.ListCollections(ctx, cluster) if err != nil { - return nil, fmt.Errorf("failed to list resources: %w", err) + return nil, fmt.Errorf("failed to list collections: %w", err) } - boundSchemas := make(kubebindv1alpha2.ExportedSchemas, len(list.Items)) - for _, item := range list.Items { - boundSchema, err := helpers.UnstructuredToBoundSchema(item) - if err != nil { - return nil, err + templates := &kubebindv1alpha2.APIServiceExportTemplateList{} + for _, collection := range collections.Items { + for _, t := range collection.Spec.Templates { + template, err := h.kubeManager.GetTemplates(ctx, cluster, t.Name) + if err != nil { + return nil, fmt.Errorf("failed to get template %q: %w", t.Name, err) + } + templates.Items = append(templates.Items, *template) } - boundSchemas[boundSchema.ResourceGroupName()] = boundSchema } - return boundSchemas, nil + return templates, nil } diff --git a/backend/kubernetes/manager.go b/backend/kubernetes/manager.go index e8da9ef78..9612550e4 100644 --- a/backend/kubernetes/manager.go +++ b/backend/kubernetes/manager.go @@ -165,6 +165,38 @@ func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster str return &crds, nil } +func (m *Manager) ListCollections(ctx context.Context, cluster string) (*kubebindv1alpha2.CollectionList, error) { + cl, err := m.manager.GetCluster(ctx, cluster) + if err != nil { + return nil, err + } + c := cl.GetClient() + + var collections kubebindv1alpha2.CollectionList + err = c.List(ctx, &collections) + if err != nil { + return nil, err + } + + return &collections, nil +} + +func (m *Manager) GetTemplates(ctx context.Context, cluster, name string) (*kubebindv1alpha2.APIServiceExportTemplate, error) { + cl, err := m.manager.GetCluster(ctx, cluster) + if err != nil { + return nil, err + } + c := cl.GetClient() + + var template kubebindv1alpha2.APIServiceExportTemplate + err = c.Get(ctx, types.NamespacedName{Name: name}, &template) + if err != nil { + return nil, err + } + + return &template, nil +} + func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk schema.GroupVersionKind, selector labels.Selector) (*unstructured.UnstructuredList, error) { cl, err := m.manager.GetCluster(ctx, cluster) if err != nil { diff --git a/backend/kubernetes/resources/resources.go b/backend/kubernetes/resources/resources.go index 7672acb25..8d2b6f0bf 100644 --- a/backend/kubernetes/resources/resources.go +++ b/backend/kubernetes/resources/resources.go @@ -25,6 +25,5 @@ const ( KubeconfigSecretName = "kubeconfig" ClusterBindingName = "cluster" - // TODO(MQ): maybe think of a better label name. ExportedCRDsLabel = "kube-bind.io/exported" ) diff --git a/backend/template/login.html b/backend/template/login.html deleted file mode 100644 index 7dd16d6b6..000000000 --- a/backend/template/login.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - login - - -
-
-
- -
-
-
-

MangoDB

-

- -
-
-
-
- - diff --git a/backend/template/resources.gohtml b/backend/template/resources.gohtml index 4a3b87109..1f653295c 100644 --- a/backend/template/resources.gohtml +++ b/backend/template/resources.gohtml @@ -7,23 +7,223 @@ + + - Resources - CRD + Modules - Kube Bind + -

Exports

-
- {{range .Schemas}} -
-

{{.Name}}

-
    -
  • Scope: {{.Scope}}
  • -
-
- Bind +
+ + +
+ {{range $moduleIdx, $schema := .Schemas}} +
+
+
+
{{$schema.Name}}
+ {{if $schema.Description}}{{$schema.Description}}{{end}} +
+
    +
  • + Scope: {{$schema.Scope}} +
  • +
  • + Resources ({{len $schema.Resources}}): +
    + {{range $schema.Resources}} + {{.Resource}}.{{.Group}} + {{end}} +
    +
  • + {{if $schema.PermissionClaims}} +
  • + Permissions ({{len $schema.PermissionClaims}}): + {{range $i, $claim := $schema.PermissionClaims}} +
    + + {{$claim.Resource}}{{if $claim.Group}}.{{$claim.Group}}{{end}} + {{if $claim.Selector.NamedResources}} ({{len $claim.Selector.NamedResources}}){{end}} + {{if $claim.Selector.LabelSelector}} (labels){{end}} + + {{if or $claim.Selector.NamedResources $claim.Selector.LabelSelector}} +
    + +
    + {{if $claim.Selector.NamedResources}} +
    + Named: + {{range $claim.Selector.NamedResources}} +
    {{.Name}}{{if .Namespace}} ({{.Namespace}}){{end}} + {{end}} +
    + {{end}} + {{if $claim.Selector.LabelSelector}} +
    + Labels: + {{if $claim.Selector.LabelSelector.MatchLabels}} + {{range $key, $value := $claim.Selector.LabelSelector.MatchLabels}} +
    {{$key}}: {{$value}} + {{end}} + {{end}} + {{if $claim.Selector.LabelSelector.MatchExpressions}} + {{range $claim.Selector.LabelSelector.MatchExpressions}} +
    {{.Key}} {{.Operator}} {{range $idx, $val := .Values}}{{if $idx}}, {{end}}{{$val}}{{end}} + {{end}} + {{end}} +
    + {{end}} +
    + {{end}} +
    + {{end}} +
  • + {{end}} + {{if $schema.Namespaces}} +
  • + Namespaces ({{len $schema.Namespaces}}): +
    + {{range $schema.Namespaces}} + {{.Name}} + {{end}} +
    +
  • + {{end}} +
+ +
+ {{end}}
- {{end}}
diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go index f260abb1e..521f1b5e6 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go @@ -44,20 +44,20 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co return nil, err } - var bindings []*kubebindv1alpha2.APIServiceBinding - for _, resource := range request.Spec.Resources { - name := resource.ResourceGroupName() - existing, err := bindClient.KubeBindV1alpha2().APIServiceBindings().Get(ctx, name, metav1.GetOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return nil, err - } else if err == nil { - if existing.Spec.KubeconfigSecretRef.Namespace != "kube-bind" || existing.Spec.KubeconfigSecretRef.Name != secretName { - return nil, fmt.Errorf("found existing APIServiceBinding %s not from this service provider", name) - } - fmt.Fprintf(b.Options.IOStreams.ErrOut, "✅ Updating existing APIServiceBinding %s.\n", existing.Name) - bindings = append(bindings, existing) + // Use request name as the single binding name + bindingName := request.ObjectMeta.Name + existing, err := bindClient.KubeBindV1alpha2().APIServiceBindings().Get(ctx, bindingName, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } else if err == nil { + // Validate existing binding + if existing.Spec.KubeconfigSecretRef.Namespace != "kube-bind" || existing.Spec.KubeconfigSecretRef.Name != secretName { + return nil, fmt.Errorf("found existing APIServiceBinding %s not from this service provider", bindingName) + } + fmt.Fprintf(b.Options.IOStreams.ErrOut, "✅ Reusing existing APIServiceBinding %s.\n", existing.Name) - // checking CRD to match the binding + // Validate all CRDs are owned by this binding + for _, resource := range request.Spec.Resources { crd, err := apiextensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, resource.ResourceGroupName(), metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return nil, err @@ -66,52 +66,46 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co return nil, fmt.Errorf("CustomResourceDefinition %s exists, but is not owned by kube-bind", crd.Name) } } - continue } - // create new APIServiceBinding. - first := true - if err := wait.PollUntilContextCancel(ctx, 1*time.Second, false, func(ctx context.Context) (bool, error) { - if !first { - first = false - fmt.Fprint(b.Options.IOStreams.ErrOut, ".") - } - created, err := bindClient.KubeBindV1alpha2().APIServiceBindings().Create(ctx, &kubebindv1alpha2.APIServiceBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: resource.ResourceGroupName(), - Namespace: "kube-bind", - }, - Spec: kubebindv1alpha2.APIServiceBindingSpec{ - KubeconfigSecretRef: kubebindv1alpha2.ClusterSecretKeyRef{ - LocalSecretKeyRef: kubebindv1alpha2.LocalSecretKeyRef{ - Name: secretName, - Key: "kubeconfig", - }, - Namespace: "kube-bind", + return []*kubebindv1alpha2.APIServiceBinding{existing}, nil + } + + // Create new APIServiceBinding + var created *kubebindv1alpha2.APIServiceBinding + if err := wait.PollUntilContextCancel(ctx, 1*time.Second, false, func(ctx context.Context) (bool, error) { + created, err = bindClient.KubeBindV1alpha2().APIServiceBindings().Create(ctx, &kubebindv1alpha2.APIServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + }, + Spec: kubebindv1alpha2.APIServiceBindingSpec{ + KubeconfigSecretRef: kubebindv1alpha2.ClusterSecretKeyRef{ + LocalSecretKeyRef: kubebindv1alpha2.LocalSecretKeyRef{ + Name: secretName, + Key: "kubeconfig", }, + Namespace: "kube-bind", }, - }, metav1.CreateOptions{}) - if err != nil { - return false, err - } + }, + }, metav1.CreateOptions{}) + if err != nil { + return false, err + } - // best effort status update to have "Pending" in the Ready condition - conditions.MarkFalse(created, - conditionsapi.ReadyCondition, - "Pending", - conditionsapi.ConditionSeverityInfo, - "Pending", - ) - _, _ = bindClient.KubeBindV1alpha2().APIServiceBindings().UpdateStatus(ctx, created, metav1.UpdateOptions{}) + // Best effort status update to have "Pending" in the Ready condition + conditions.MarkFalse(created, + conditionsapi.ReadyCondition, + "Pending", + conditionsapi.ConditionSeverityInfo, + "Pending", + ) + _, _ = bindClient.KubeBindV1alpha2().APIServiceBindings().UpdateStatus(ctx, created, metav1.UpdateOptions{}) - fmt.Fprintf(b.Options.IOStreams.ErrOut, "✅ Created APIServiceBinding %s.%s\n", resource.Resource, resource.Group) - bindings = append(bindings, created) - return true, nil - }); err != nil { - fmt.Fprintln(b.Options.IOStreams.ErrOut, "") - return nil, err - } + return true, nil + }); err != nil { + return nil, err } - return bindings, nil + fmt.Fprintf(b.Options.IOStreams.ErrOut, "✅ Created APIServiceBinding %s for %d resources\n", bindingName, len(request.Spec.Resources)) + return []*kubebindv1alpha2.APIServiceBinding{created}, nil } diff --git a/contrib/kcp/README.md b/contrib/kcp/README.md index 686437b46..d230716b6 100644 --- a/contrib/kcp/README.md +++ b/contrib/kcp/README.md @@ -93,10 +93,14 @@ kubectl kcp bind apiexport root:kube-bind:kube-bind.io \ 7. Create CRD in provider: ```bash -kubectl create -f contrib/kcp/deploy/examples/apiexport.yaml -kubectl create -f contrib/kcp/deploy/examples/apiresourceschema-cowboys.yaml -kubectl create -f contrib/kcp/deploy/examples/apiresourceschema-sheriffs.yaml +kubectl apply -f contrib/kcp/deploy/examples/apiexport.yaml +kubectl apply -f contrib/kcp/deploy/examples/apiresourceschema-cowboys.yaml +kubectl apply -f contrib/kcp/deploy/examples/apiresourceschema-sheriffs.yaml kubectl kcp bind apiexport root:provider:cowboys-stable + +kubectl apply -f contrib/kcp/deploy/examples/template-cowboys.yaml +kubectl apply -f contrib/kcp/deploy/examples/template-sheriffs.yaml +kubectl apply -f contrib/kcp/deploy/examples/collection-wildwest.yaml ``` 8. Get LogicalCluster: @@ -104,7 +108,7 @@ kubectl kcp bind apiexport root:provider:cowboys-stable ```bash kubectl get logicalcluster # NAME PHASE URL AGE -# cluster Ready https://192.168.2.166:6443/clusters/1f4roigyt6meiaf8 +# cluster Ready https://192.168.2.166:6443/clusters/2cc89nxsuivawooq ``` ## Consumer @@ -120,7 +124,7 @@ kubectl ws create consumer --enter 10. Bind the thing: ```bash -./bin/kubectl-bind http://127.0.0.1:8080/clusters/1f4roigyt6meiaf8/exports --dry-run -o yaml > apiserviceexport.yaml +./bin/kubectl-bind http://127.0.0.1:8080/clusters/2cc89nxsuivawooq/exports --dry-run -o yaml > apiserviceexport.yaml # Extract secret for binding process. Note that secret name is not the same as output from command above. Check secret # name by running `kubectl get secret -n kube-bind` diff --git a/contrib/kcp/deploy/examples/collection-wildwest.yaml b/contrib/kcp/deploy/examples/collection-wildwest.yaml new file mode 100644 index 000000000..90e123a8d --- /dev/null +++ b/contrib/kcp/deploy/examples/collection-wildwest.yaml @@ -0,0 +1,9 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: Collection +metadata: + name: wildwest +spec: + description: "A collection of Wild West service definitions including Cowboys and Sheriffs for frontier management" + templates: + - name: cowboys + - name: sheriffs \ No newline at end of file diff --git a/contrib/kcp/deploy/examples/template-cowboys.yaml b/contrib/kcp/deploy/examples/template-cowboys.yaml new file mode 100644 index 000000000..dccca8910 --- /dev/null +++ b/contrib/kcp/deploy/examples/template-cowboys.yaml @@ -0,0 +1,32 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportTemplate +metadata: + name: cowboys +spec: + scope: Namespaced + resources: + - group: wildwest.dev + versions: + - v1alpha1 + resource: cowboys + permissionClaims: + - group: "" + resource: secrets + selector: + namedResources: + - name: cowboy-credentials + - name: cowboy-config + labelSelector: + matchLabels: + app: cowboy + env: production + - group: "" + resource: configmaps + selector: + namedResources: + - name: cowboy-settings + - name: cowboy-environment + labelSelector: + matchLabels: + app: cowboy + component: config \ No newline at end of file diff --git a/contrib/kcp/deploy/examples/template-sheriffs.yaml b/contrib/kcp/deploy/examples/template-sheriffs.yaml new file mode 100644 index 000000000..bd3a8e117 --- /dev/null +++ b/contrib/kcp/deploy/examples/template-sheriffs.yaml @@ -0,0 +1,35 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportTemplate +metadata: + name: sheriffs +spec: + scope: Cluster + resources: + - group: wildwest.dev + versions: + - v1alpha1 + resource: sheriffs + permissionClaims: + - group: "" + resource: secrets + selector: + namedResources: + - name: sheriff-badge-credentials + - name: sheriff-jurisdiction-config + labelSelector: + matchLabels: + app: sheriff + security: high + - group: "" + resource: configmaps + selector: + namedResources: + - name: sheriff-jurisdiction-map + - name: sheriff-enforcement-rules + labelSelector: + matchLabels: + app: sheriff + component: enforcement + namespaces: + - name: sheriff-operations + - name: sheriff-intelligence \ No newline at end of file diff --git a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml index f6269bb0a..f32386106 100644 --- a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -49,7 +49,7 @@ spec: crd: {} - group: kube-bind.io name: apiservicebindings - schema: v250929-62fc678.apiservicebindings.kube-bind.io + schema: v251015-f561c7c.apiservicebindings.kube-bind.io storage: crd: {} - group: kube-bind.io @@ -59,7 +59,12 @@ spec: crd: {} - group: kube-bind.io name: apiserviceexports - schema: v250929-62fc678.apiserviceexports.kube-bind.io + schema: v251015-f561c7c.apiserviceexports.kube-bind.io + storage: + crd: {} + - group: kube-bind.io + name: apiserviceexporttemplates + schema: v251022-ca928ec.apiserviceexporttemplates.kube-bind.io storage: crd: {} - group: kube-bind.io @@ -77,4 +82,9 @@ spec: schema: v250925-56669b8.clusterbindings.kube-bind.io storage: crd: {} + - group: kube-bind.io + name: collections + schema: v251022-ca928ec.collections.kube-bind.io + storage: + crd: {} status: {} diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml index 6a1d7c57a..d61cebf72 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250929-62fc678.apiservicebindings.kube-bind.io + name: v251015-f561c7c.apiservicebindings.kube-bind.io spec: conversion: strategy: None @@ -374,7 +374,7 @@ spec: type: object type: object x-kubernetes-map-type: atomic - namedResource: + namedResources: description: NamedResource is a shorthand for selecting a single resource by name and namespace. items: diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml index d9a3f88f9..7dbb37bef 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml @@ -310,7 +310,7 @@ spec: type: object type: object x-kubernetes-map-type: atomic - namedResource: + namedResources: description: NamedResource is a shorthand for selecting a single resource by name and namespace. items: diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml index 208a38641..ecaf8a31f 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250929-62fc678.apiserviceexports.kube-bind.io + name: v251015-f561c7c.apiserviceexports.kube-bind.io spec: conversion: strategy: None @@ -561,7 +561,7 @@ spec: type: object type: object x-kubernetes-map-type: atomic - namedResource: + namedResources: description: NamedResource is a shorthand for selecting a single resource by name and namespace. items: diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexporttemplates.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexporttemplates.kube-bind.io.yaml new file mode 100644 index 000000000..86d9b67b7 --- /dev/null +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexporttemplates.kube-bind.io.yaml @@ -0,0 +1,268 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v251022-ca928ec.apiserviceexporttemplates.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: APIServiceExportTemplate + listKind: APIServiceExportTemplateList + plural: apiserviceexporttemplates + singular: apiserviceexporttemplate + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.resources[*].group + name: Resources + type: string + - jsonPath: .spec.permissionClaims[*].resource + name: PermissionClaims + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + description: APIServiceExportTemplate groups multiple CRDs with related resources + (permissionClaims) as a Service definition. + 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 template. + properties: + namespaces: + description: |- + namespaces specifies the namespaces that should be bootstrapped as part of this template. + When objects originate from provider side, consumer does not always know the necessary details + This field allows provider to pre-heat the necessary namespaces on provider side by creating + APIServiceNamespace objects attached to the APIServiceExport. More namespaces can be created later by the consumer. + items: + properties: + name: + description: name is the name of the namespace to create on provider + side. + type: string + required: + - name + type: object + type: array + permissionClaims: + description: permissionClaims defines the permission claims required + by this template. + items: + description: |- + PermissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + 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 + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namedResources: + description: NamedResource is a shorthand for selecting a + single resource by name and namespace. + items: + description: NamedResource selects a specific resource by + name and namespace. + properties: + name: + description: |- + Name is the name of the resource. + Name matches the metadata.name field of the underlying object. + type: string + namespace: + description: |- + Namespace represents namespace where an object of the given group/resource may be managed. + Namespaces matches against the metadata.namespace field. If not provided, the object is assumed to be cluster-scoped. + Namespaces field is ignored for namespaced isolation mode. + type: string + required: + - name + type: object + type: array + type: object + required: + - resource + - selector + type: object + type: array + resources: + description: resources defines the CRDs that are part of this template. + 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 + scope: + allOf: + - enum: + - Cluster + - Namespaced + - enum: + - Namespaced + - Cluster + description: scope defines the scope of the resources in this template. + type: string + required: + - resources + - scope + type: object + status: + description: status contains reconciliation information for the template. + properties: + conditions: + description: conditions is a list of conditions that apply to the APIServiceExportTemplate. + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/kcp/deploy/resources/apiresourceschema-collections.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-collections.kube-bind.io.yaml new file mode 100644 index 000000000..8c4127e16 --- /dev/null +++ b/contrib/kcp/deploy/resources/apiresourceschema-collections.kube-bind.io.yaml @@ -0,0 +1,128 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v251022-ca928ec.collections.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: Collection + listKind: CollectionList + plural: collections + singular: collection + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.description + name: Description + type: string + - jsonPath: .spec.templates[*].name + name: Templates + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + description: Collection groups multiple APIServiceExportTemplates into a logical + group. This functions as a folder in the UI. + 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 collection. + properties: + description: + description: description is a human readable description of this collection. + type: string + templates: + description: templates is a list of template references that are part + of this collection. + items: + description: APIServiceExportTemplateReference references an APIServiceExportTemplate + by name. + properties: + name: + description: name is the name of the template. + type: string + required: + - name + type: object + minItems: 1 + type: array + required: + - templates + type: object + status: + description: status contains reconciliation information for the collection. + properties: + conditions: + description: conditions is a list of conditions that apply to the Collection. + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/kcp/test/e2e/browser.go b/contrib/kcp/test/e2e/browser.go index b3d09749e..750d417e0 100644 --- a/contrib/kcp/test/e2e/browser.go +++ b/contrib/kcp/test/e2e/browser.go @@ -33,14 +33,14 @@ import ( "github.com/kube-bind/kube-bind/test/e2e/framework" ) -func performBindingWithBrowser(t *testing.T, backendAddr string, clusterID string, consumerCfg *rest.Config, consumerKubeconfig, resource string) { +func performBindingWithBrowser(t *testing.T, backendAddr string, clusterID string, consumerCfg *rest.Config, consumerKubeconfig, resource, template string) { bindURL := fmt.Sprintf("http://%s/clusters/%s/exports", backendAddr, clusterID) t.Logf("Bind URL: %s", bindURL) // Test binding dry run first (similar to happy-case test) t.Run("Service is bound dry run", func(t *testing.T) { authURLDryRunCh := make(chan string, 1) - go simulateKCPBrowser(t, authURLDryRunCh, resource) + go simulateKCPBrowser(t, authURLDryRunCh, template) iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams() framework.Bind(t, iostreams, authURLDryRunCh, nil, bindURL, "--kubeconfig", consumerKubeconfig, "--dry-run") @@ -51,7 +51,7 @@ func performBindingWithBrowser(t *testing.T, backendAddr string, clusterID strin // Perform actual binding (similar to happy-case test) t.Run("Service is bound", func(t *testing.T) { authURLCh := make(chan string, 1) - go simulateKCPBrowser(t, authURLCh, resource) + go simulateKCPBrowser(t, authURLCh, template) iostreams, _, _, _ := genericclioptions.NewTestIOStreams() invocations := make(chan framework.SubCommandInvocation, 1) @@ -78,8 +78,8 @@ func performBindingWithBrowser(t *testing.T, backendAddr string, clusterID strin }) } -// simulateKCPBrowser simulates browser interaction for KCP binding. -func simulateKCPBrowser(t *testing.T, authURLCh chan string, resource string) { +// simulateKCPBrowser simulates browser interaction for KCP binding using templates. +func simulateKCPBrowser(t *testing.T, authURLCh chan string, template string) { browser := surf.NewBrowser() authURL := <-authURLCh @@ -90,9 +90,9 @@ func simulateKCPBrowser(t *testing.T, authURLCh chan string, resource string) { t.Logf("Waiting for browser to be at /resources") framework.BrowserEventuallyAtPath(t, browser, "/resources") - t.Logf("Clicking %s resource", resource) - err = browser.Click("a." + resource) - require.NoError(t, err, "Failed to click resource link") + t.Logf("Clicking %s template", template) + err = browser.Click("a." + template) + require.NoError(t, err, "Failed to click template link") t.Logf("Waiting for browser to be forwarded to client") framework.BrowserEventuallyAtPath(t, browser, "/callback") diff --git a/contrib/kcp/test/e2e/kcp_test.go b/contrib/kcp/test/e2e/kcp_test.go index 4fc6aa363..96d2495f3 100644 --- a/contrib/kcp/test/e2e/kcp_test.go +++ b/contrib/kcp/test/e2e/kcp_test.go @@ -101,12 +101,15 @@ func testKcpIntegration(t *testing.T, scope kubebindv1alpha2.InformerScope) { ), ) - t.Log("Applying example APIExport and APIResourceSchemas to provider workspace") + t.Log("Applying example APIExport, APIResourceSchemas and templates to provider workspace") framework.ApplyFiles(t, providerCfg, "../../deploy/examples/apiexport.yaml", "../../deploy/examples/apiresourceschema-cowboys.yaml", // namespaced "../../deploy/examples/apiresourceschema-sheriffs.yaml", // cluster scoped + "../../deploy/examples/template-cowboys.yaml", // template for cowboys + "../../deploy/examples/template-sheriffs.yaml", // template for sheriffs + "../../deploy/examples/collection-wildwest.yaml", ) t.Log("Bind the APIExport locally") @@ -138,18 +141,20 @@ func testKcpIntegration(t *testing.T, scope kubebindv1alpha2.InformerScope) { // kube-bind process t.Log("Perform binding process with browser") - var kind, resource string + var kind, resource, template string switch scope { case kubebindv1alpha2.ClusterScope: kind = "Sheriff" resource = "sheriffs" + template = "sheriffs" case kubebindv1alpha2.NamespacedScope: kind = "Cowboy" resource = "cowboys" + template = "cowboys" default: require.Fail(t, "unhandled scope %q", scope) } - performBindingWithBrowser(t, backendAddr, providerClusterID, consumerCfg, consumerKubeconfig, resource) + performBindingWithBrowser(t, backendAddr, providerClusterID, consumerCfg, consumerKubeconfig, resource, template) t.Log("Testing resource creation and synchronization...") testKCPResourceSync(t, consumerCfg, providerCfg, scope, kind, resource) diff --git a/deploy/crd/kube-bind.io_apiservicebindings.yaml b/deploy/crd/kube-bind.io_apiservicebindings.yaml index d94c50a99..e4fb14485 100644 --- a/deploy/crd/kube-bind.io_apiservicebindings.yaml +++ b/deploy/crd/kube-bind.io_apiservicebindings.yaml @@ -381,7 +381,7 @@ spec: type: object type: object x-kubernetes-map-type: atomic - namedResource: + namedResources: description: NamedResource is a shorthand for selecting a single resource by name and namespace. items: diff --git a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml index 8a9c3eea0..dfa03f749 100644 --- a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml @@ -314,7 +314,7 @@ spec: type: object type: object x-kubernetes-map-type: atomic - namedResource: + namedResources: description: NamedResource is a shorthand for selecting a single resource by name and namespace. items: diff --git a/deploy/crd/kube-bind.io_apiserviceexports.yaml b/deploy/crd/kube-bind.io_apiserviceexports.yaml index 2aaf068b9..3ba6270ff 100644 --- a/deploy/crd/kube-bind.io_apiserviceexports.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexports.yaml @@ -564,7 +564,7 @@ spec: type: object type: object x-kubernetes-map-type: atomic - namedResource: + namedResources: description: NamedResource is a shorthand for selecting a single resource by name and namespace. items: diff --git a/deploy/crd/kube-bind.io_apiserviceexporttemplates.yaml b/deploy/crd/kube-bind.io_apiserviceexporttemplates.yaml new file mode 100644 index 000000000..5ac00f14d --- /dev/null +++ b/deploy/crd/kube-bind.io_apiserviceexporttemplates.yaml @@ -0,0 +1,272 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: apiserviceexporttemplates.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: APIServiceExportTemplate + listKind: APIServiceExportTemplateList + plural: apiserviceexporttemplates + singular: apiserviceexporttemplate + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.resources[*].group + name: Resources + type: string + - jsonPath: .spec.permissionClaims[*].resource + name: PermissionClaims + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: APIServiceExportTemplate groups multiple CRDs with related resources + (permissionClaims) as a Service definition. + 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 template. + properties: + namespaces: + description: |- + namespaces specifies the namespaces that should be bootstrapped as part of this template. + When objects originate from provider side, consumer does not always know the necessary details + This field allows provider to pre-heat the necessary namespaces on provider side by creating + APIServiceNamespace objects attached to the APIServiceExport. More namespaces can be created later by the consumer. + items: + properties: + name: + description: name is the name of the namespace to create on + provider side. + type: string + required: + - name + type: object + type: array + permissionClaims: + description: permissionClaims defines the permission claims required + by this template. + items: + description: |- + PermissionClaim selects objects of a GVR that a service provider may + request and that a consumer may accept and allow the service provider access to. + 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 + selector: + description: Selector is a resource selector that selects objects + of a GVR. + properties: + labelSelector: + description: LabelSelector is a label selector that selects + objects of a GVR. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namedResources: + description: NamedResource is a shorthand for selecting + a single resource by name and namespace. + items: + description: NamedResource selects a specific resource + by name and namespace. + properties: + name: + description: |- + Name is the name of the resource. + Name matches the metadata.name field of the underlying object. + type: string + namespace: + description: |- + Namespace represents namespace where an object of the given group/resource may be managed. + Namespaces matches against the metadata.namespace field. If not provided, the object is assumed to be cluster-scoped. + Namespaces field is ignored for namespaced isolation mode. + type: string + required: + - name + type: object + type: array + type: object + required: + - resource + - selector + type: object + type: array + resources: + description: resources defines the CRDs that are part of this template. + 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 + scope: + allOf: + - enum: + - Cluster + - Namespaced + - enum: + - Namespaced + - Cluster + description: scope defines the scope of the resources in this template. + type: string + required: + - resources + - scope + type: object + status: + description: status contains reconciliation information for the template. + properties: + conditions: + description: conditions is a list of conditions that apply to the + APIServiceExportTemplate. + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/crd/kube-bind.io_collections.yaml b/deploy/crd/kube-bind.io_collections.yaml new file mode 100644 index 000000000..b95bc0f89 --- /dev/null +++ b/deploy/crd/kube-bind.io_collections.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: collections.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: Collection + listKind: CollectionList + plural: collections + singular: collection + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.description + name: Description + type: string + - jsonPath: .spec.templates[*].name + name: Templates + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: Collection groups multiple APIServiceExportTemplates into a logical + group. This functions as a folder in the UI. + 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 collection. + properties: + description: + description: description is a human readable description of this collection. + type: string + templates: + description: templates is a list of template references that are part + of this collection. + items: + description: APIServiceExportTemplateReference references an APIServiceExportTemplate + by name. + properties: + name: + description: name is the name of the template. + type: string + required: + - name + type: object + minItems: 1 + type: array + required: + - templates + type: object + status: + description: status contains reconciliation information for the collection. + properties: + conditions: + description: conditions is a list of conditions that apply to the + Collection. + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/examples/collection.yaml b/deploy/examples/collection.yaml new file mode 100644 index 000000000..a456e1a06 --- /dev/null +++ b/deploy/examples/collection.yaml @@ -0,0 +1,8 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: Collection +metadata: + name: all +spec: + templates: + - name: mangodb + - name: foo \ No newline at end of file diff --git a/deploy/examples/template-foo.yaml b/deploy/examples/template-foo.yaml new file mode 100644 index 000000000..80cefc53c --- /dev/null +++ b/deploy/examples/template-foo.yaml @@ -0,0 +1,33 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportTemplate +metadata: + name: foo +spec: + scope: Cluster + resources: + - group: bar.io + versions: + - v1alpha1 + resource: foos + permissionClaims: + - group: "" + resource: configmaps + selector: + namedResources: + - name: named-configmap-only + namespace: foo-secrets-ns + - group: "" + resource: secrets + selector: + labelSelector: + matchLabels: + app: secrets + namedResources: + - name: test-secret + namespace: foo-secrets-ns + - name: named-secret-1 + namespace: foo-secrets-ns + - name: named-secret-2 + namespace: foo-secrets-ns + namespaces: + - name: foo-secrets-ns \ No newline at end of file diff --git a/deploy/examples/template-mangodb.yaml b/deploy/examples/template-mangodb.yaml new file mode 100644 index 000000000..51e182394 --- /dev/null +++ b/deploy/examples/template-mangodb.yaml @@ -0,0 +1,23 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportTemplate +metadata: + name: mangodb +spec: + scope: Namespaced + resources: + - group: mangodb.com + versions: + - v1alpha1 + resource: mangodbs + permissionClaims: + - group: "" + resource: "secrets" + selector: + labelSelector: + matchLabels: + app: "mangodb" + namedResources: + - name: "mangodb-credentials" + namespace: "mangodb-namespace" + namespaces: + - name: mangodb-namespace \ No newline at end of file diff --git a/docs/content/.pages b/docs/content/.pages index 786980b11..38cfd704f 100644 --- a/docs/content/.pages +++ b/docs/content/.pages @@ -2,6 +2,7 @@ nav: - Home: - index.md - Setup: setup + - Usage: usage - Contributing: contributing - Developers: developers - Reference: reference diff --git a/docs/content/index.md b/docs/content/index.md index e84bc0bf1..f57713b9e 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -11,6 +11,13 @@ kube-bind is a prototype project that aims to provide better support for service - The service provider does not inject controllers/operators into the service consumer's cluster. - A single vendor-neutral, OpenSource agent per consumer cluster connects it with the requested services. +## Key Features + +- **Catalog API**: Organize exported services using Collections (folders) and Modules (service definitions) +- **Provider-side Namespace Management**: Automatic namespace provisioning and RBAC setup on provider clusters +- **Enhanced Permission Claims**: Granular resource access with both label selectors and named resource support +- **Multi-backend Support**: Works with standard Kubernetes and KCP backends through multicluster-runtime + ## Quickstart To get started with trying out kube-bind on your local system, check out our [Quickstart](./setup/quickstart.md) instructions. diff --git a/docs/content/setup/index.md b/docs/content/setup/index.md index b6adf12c3..b4afa0077 100644 --- a/docs/content/setup/index.md +++ b/docs/content/setup/index.md @@ -29,4 +29,12 @@ Choose the setup that best fits your use case: - Use **Helm Deployment** for production environments with standard Kubernetes - Use **KCP Integration** for advanced multi-tenant scenarios with workspace isolation +## Next Steps + +After completing your setup, explore these guides: + +- **[Usage Guide](../usage/index.md)**: Learn common workflows and the new Catalog API +- **[Migration Guide](../usage/migration.md)**: Upgrade from previous versions +- **[Developer Documentation](../developers/index.md)**: Understand the architecture and contribute + {% include "partials/section-overview.html" %} diff --git a/docs/content/setup/quickstart.md b/docs/content/setup/quickstart.md index 8a252db91..8cfd68cf4 100644 --- a/docs/content/setup/quickstart.md +++ b/docs/content/setup/quickstart.md @@ -73,6 +73,9 @@ kubectl ws create provider --enter ```shell kubectl apply -f deploy/examples/crd-mangodb.yaml kubectl apply -f deploy/examples/crd-foo.yaml + kubectl apply -f deploy/examples/template-mangodb.yaml + kubectl apply -f deploy/examples/template-foo.yaml + kubectl apply -f deploy/examples/collection.yaml ``` * start the backend binary with the right flags: diff --git a/docs/content/usage/.pages b/docs/content/usage/.pages new file mode 100644 index 000000000..33aedbeff --- /dev/null +++ b/docs/content/usage/.pages @@ -0,0 +1,4 @@ +title: Usage +nav: + - index.md + - migration.md \ No newline at end of file diff --git a/docs/content/usage/index.md b/docs/content/usage/index.md new file mode 100644 index 000000000..3078d7aa2 --- /dev/null +++ b/docs/content/usage/index.md @@ -0,0 +1,16 @@ +# Usage Guide + +This guide covers common usage patterns and workflows for kube-bind, including the new Catalog API features introduced in recent versions. + +## Table of Contents + +- [Basic Service Binding](#basic-service-binding) +- [Using the Catalog API](#using-the-catalog-api) +- [Permission Claims](#permission-claims) +- [Provider-side Namespace Management](#provider-side-namespace-management) +- [Advanced Workflows](#advanced-workflows) + +## Basic Service Binding + + +TODO \ No newline at end of file diff --git a/docs/content/usage/migration.md b/docs/content/usage/migration.md new file mode 100644 index 000000000..e4cffa217 --- /dev/null +++ b/docs/content/usage/migration.md @@ -0,0 +1,24 @@ +# Migration Guide + +This guide helps you migrate from older versions of kube-bind to the latest version with new features and API changes. + +## Migration Timeline + +### From v0.5.x to v0.6.x+ + +The v0.6.x release introduces several significant improvements: + +- **Catalog API**: New `Collection` and `APIServiceExportTemplate` CRDs for better service organization +- **Enhanced Permission Claims**: Support for `NamedResources` alongside label selectors +- **Provider-side Namespace Management**: Automatic RBAC and namespace provisioning +- **Improved KCP Integration**: Better workspace and APIExport handling + +## API Changes + +TODO + +If you encounter issues during migration: + +1. **Check GitHub Issues**: [kube-bind issues](https://github.com/kube-bind/kube-bind/issues) +2. **Slack Channel**: [`#kube-bind` on Kubernetes Slack](https://kubernetes.slack.com/archives/C046PRXNJ4W) +3. **Mailing List**: [kube-bind-dev](https://groups.google.com/g/kube-bind-dev) \ No newline at end of file diff --git a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go index d49f483ff..b77fe3d09 100644 --- a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go +++ b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go @@ -374,7 +374,7 @@ func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj interface{} // We need to list all the object which might not got synced at consumer side too: var sel labels.Selector switch v := c.claim.Selector; { - case v.LabelSelector == nil && len(v.NamedResource) == 0: + case v.LabelSelector == nil && len(v.NamedResources) == 0: sel = labels.Everything() case v.LabelSelector != nil: var err error @@ -383,9 +383,9 @@ func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj interface{} runtime.HandleError(err) return } - case len(v.NamedResource) > 0: + case len(v.NamedResources) > 0: // namedResource-only: fetch specific objects from cache and enqueue - for _, nr := range v.NamedResource { + for _, nr := range v.NamedResources { // Build consumer cache key; empty namespace implies cluster-scoped key := nr.Name if nr.Namespace != "" { diff --git a/pkg/konnector/controllers/cluster/namespacelifecycle/namespacelifecycle_controller.go b/pkg/konnector/controllers/cluster/namespacelifecycle/namespacelifecycle_controller.go index 7edc6172b..f0b7090ab 100644 --- a/pkg/konnector/controllers/cluster/namespacelifecycle/namespacelifecycle_controller.go +++ b/pkg/konnector/controllers/cluster/namespacelifecycle/namespacelifecycle_controller.go @@ -291,7 +291,7 @@ func (c *controller) handleNamespaceLifecycle(ctx context.Context, current *kube } // isConsumerOwned checks if the APIServiceNamespace is owned by consumer. -// If the label is missing, it is considered consumer owned. +// to prevent the controller from creating namespaces unless explicitly requested). func isConsumerOwned(sns *kubebindv1alpha2.APIServiceNamespace) bool { return sns.Labels[kubebindv1alpha2.ObjectOwnerLabel] != kubebindv1alpha2.OwnerProvider.String() } diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go index d37e524c3..dfe8ca561 100644 --- a/pkg/resources/resources.go +++ b/pkg/resources/resources.go @@ -30,7 +30,7 @@ func IsClaimed(selector kubebindv1alpha2.Selector, obj *unstructured.Unstructure return false } // Empty selector selects everything - if selector.LabelSelector == nil && len(selector.NamedResource) == 0 { + if selector.LabelSelector == nil && len(selector.NamedResources) == 0 { return true } @@ -52,9 +52,9 @@ func IsClaimed(selector kubebindv1alpha2.Selector, obj *unstructured.Unstructure } // Check named resources if specified - if len(selector.NamedResource) > 0 { + if len(selector.NamedResources) > 0 { namedResourceMatches = false // Default to false, must match at least one - for _, nr := range selector.NamedResource { + for _, nr := range selector.NamedResources { if nr.Namespace != "" && nr.Namespace != obj.GetNamespace() { continue } diff --git a/pkg/resources/resources_test.go b/pkg/resources/resources_test.go index a5d0a465c..a1a2a6c8c 100644 --- a/pkg/resources/resources_test.go +++ b/pkg/resources/resources_test.go @@ -95,7 +95,7 @@ func TestSelector_IsClaimed(t *testing.T) { { name: "named resource selector should match exact name and namespace", selector: kubebindv1alpha2.Selector{ - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "test-obj", Namespace: "test-ns", @@ -115,7 +115,7 @@ func TestSelector_IsClaimed(t *testing.T) { { name: "named resource selector should match name when namespace is empty", selector: kubebindv1alpha2.Selector{ - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "test-obj", Namespace: "", @@ -135,7 +135,7 @@ func TestSelector_IsClaimed(t *testing.T) { { name: "named resource selector should not match different name", selector: kubebindv1alpha2.Selector{ - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "other-obj", Namespace: "test-ns", @@ -155,7 +155,7 @@ func TestSelector_IsClaimed(t *testing.T) { { name: "named resource selector should not match different namespace", selector: kubebindv1alpha2.Selector{ - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "test-obj", Namespace: "other-ns", @@ -175,7 +175,7 @@ func TestSelector_IsClaimed(t *testing.T) { { name: "named resource selector should match one of multiple resources", selector: kubebindv1alpha2.Selector{ - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "other-obj", Namespace: "test-ns", @@ -223,7 +223,7 @@ func TestSelector_IsClaimed(t *testing.T) { "app": "test", }, }, - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "test-obj", Namespace: "test-ns", @@ -251,7 +251,7 @@ func TestSelector_IsClaimed(t *testing.T) { "app": "different", }, }, - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "test-obj", Namespace: "test-ns", @@ -279,7 +279,7 @@ func TestSelector_IsClaimed(t *testing.T) { "app": "test", }, }, - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "other-obj", Namespace: "test-ns", @@ -307,7 +307,7 @@ func TestSelector_IsClaimed(t *testing.T) { "app": "secrets", }, }, - NamedResource: []kubebindv1alpha2.NamedResource{ + NamedResources: []kubebindv1alpha2.NamedResource{ { Name: "test-secret", Namespace: "default", diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go index d95e589e0..068134e07 100644 --- a/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go @@ -154,7 +154,7 @@ func (r APIServiceExportRequestResource) ResourceGroupName() string { type Selector struct { // NamedResource is a shorthand for selecting a single resource by name and namespace. // +optional - NamedResource []NamedResource `json:"namedResource,omitempty"` + NamedResources []NamedResource `json:"namedResources,omitempty"` // LabelSelector is a label selector that selects objects of a GVR. // +optional diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexporttemplate_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexporttemplate_types.go new file mode 100644 index 000000000..94623c66e --- /dev/null +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexporttemplate_types.go @@ -0,0 +1,98 @@ +/* +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 v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" +) + +// APIServiceExportTemplate groups multiple CRDs with related resources (permissionClaims) as a Service definition. +// +// +crd +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,categories=kube-bind +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Resources",type="string",JSONPath=`.spec.resources[*].group` +// +kubebuilder:printcolumn:name="PermissionClaims",type="string",JSONPath=`.spec.permissionClaims[*].resource` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp`,priority=0 +type APIServiceExportTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec specifies the template. + // +required + // +kubebuilder:validation:Required + Spec APIServiceExportTemplateSpec `json:"spec"` + + // status contains reconciliation information for the template. + Status APIServiceExportTemplateStatus `json:"status,omitempty"` +} + +func (in *APIServiceExportTemplate) GetConditions() conditionsapi.Conditions { + return in.Status.Conditions +} + +func (in *APIServiceExportTemplate) SetConditions(conditions conditionsapi.Conditions) { + in.Status.Conditions = conditions +} + +// APIServiceExportTemplateSpec defines the desired state of APIServiceExportTemplate. +type APIServiceExportTemplateSpec struct { + // scope defines the scope of the resources in this template. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=Namespaced;Cluster + Scope InformerScope `json:"scope,omitempty"` + // resources defines the CRDs that are part of this template. + // + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Resources []APIServiceExportResource `json:"resources"` + + // permissionClaims defines the permission claims required by this template. + // + // +optional + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` + + // namespaces specifies the namespaces that should be bootstrapped as part of this template. + // When objects originate from provider side, consumer does not always know the necessary details + // This field allows provider to pre-heat the necessary namespaces on provider side by creating + // APIServiceNamespace objects attached to the APIServiceExport. More namespaces can be created later by the consumer. + // + // +optional + Namespaces []Namespaces `json:"namespaces,omitempty"` +} + +// APIServiceExportTemplateStatus stores status information about an APIServiceExportTemplate. +type APIServiceExportTemplateStatus struct { + // conditions is a list of conditions that apply to the APIServiceExportTemplate. + Conditions conditionsapi.Conditions `json:"conditions,omitempty"` +} + +// APIServiceExportTemplateList is the list of APIServiceExportTemplate. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type APIServiceExportTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []APIServiceExportTemplate `json:"items"` +} diff --git a/sdk/apis/kubebind/v1alpha2/collection_types.go b/sdk/apis/kubebind/v1alpha2/collection_types.go new file mode 100644 index 000000000..060f16da1 --- /dev/null +++ b/sdk/apis/kubebind/v1alpha2/collection_types.go @@ -0,0 +1,94 @@ +/* +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 v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" +) + +// Collection groups multiple APIServiceExportTemplates into a logical group. This functions as a folder in the UI. +// +// +crd +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,categories=kube-bind +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Description",type="string",JSONPath=`.spec.description` +// +kubebuilder:printcolumn:name="Templates",type="string",JSONPath=`.spec.templates[*].name` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp`,priority=0 +type Collection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec specifies the collection. + // +required + // +kubebuilder:validation:Required + Spec CollectionSpec `json:"spec"` + + // status contains reconciliation information for the collection. + Status CollectionStatus `json:"status,omitempty"` +} + +func (in *Collection) GetConditions() conditionsapi.Conditions { + return in.Status.Conditions +} + +func (in *Collection) SetConditions(conditions conditionsapi.Conditions) { + in.Status.Conditions = conditions +} + +// CollectionSpec defines the desired state of Collection. +type CollectionSpec struct { + // description is a human readable description of this collection. + // + // +optional + Description string `json:"description,omitempty"` + + // templates is a list of template references that are part of this collection. + // + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Templates []APIServiceExportTemplateReference `json:"templates"` +} + +// APIServiceExportTemplateReference references an APIServiceExportTemplate by name. +type APIServiceExportTemplateReference struct { + // name is the name of the template. + // + // +required + // +kubebuilder:validation:Required + Name string `json:"name"` +} + +// CollectionStatus stores status information about a Collection. +type CollectionStatus struct { + // conditions is a list of conditions that apply to the Collection. + Conditions conditionsapi.Conditions `json:"conditions,omitempty"` +} + +// CollectionList is the list of Collection. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type CollectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []Collection `json:"items"` +} diff --git a/sdk/apis/kubebind/v1alpha2/register.go b/sdk/apis/kubebind/v1alpha2/register.go index 90f0e283e..a23947385 100644 --- a/sdk/apis/kubebind/v1alpha2/register.go +++ b/sdk/apis/kubebind/v1alpha2/register.go @@ -60,6 +60,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ClusterBindingList{}, &BindingProvider{}, &BindingResponse{}, + &APIServiceExportTemplate{}, + &APIServiceExportTemplateList{}, + &Collection{}, + &CollectionList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go index eee7f0729..7ac1c67a0 100644 --- a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go +++ b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go @@ -486,6 +486,141 @@ func (in *APIServiceExportStatus) DeepCopy() *APIServiceExportStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceExportTemplate) DeepCopyInto(out *APIServiceExportTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceExportTemplate. +func (in *APIServiceExportTemplate) DeepCopy() *APIServiceExportTemplate { + if in == nil { + return nil + } + out := new(APIServiceExportTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIServiceExportTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceExportTemplateList) DeepCopyInto(out *APIServiceExportTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIServiceExportTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceExportTemplateList. +func (in *APIServiceExportTemplateList) DeepCopy() *APIServiceExportTemplateList { + if in == nil { + return nil + } + out := new(APIServiceExportTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIServiceExportTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceExportTemplateReference) DeepCopyInto(out *APIServiceExportTemplateReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceExportTemplateReference. +func (in *APIServiceExportTemplateReference) DeepCopy() *APIServiceExportTemplateReference { + if in == nil { + return nil + } + out := new(APIServiceExportTemplateReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceExportTemplateSpec) DeepCopyInto(out *APIServiceExportTemplateSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]APIServiceExportRequestResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]Namespaces, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceExportTemplateSpec. +func (in *APIServiceExportTemplateSpec) DeepCopy() *APIServiceExportTemplateSpec { + if in == nil { + return nil + } + out := new(APIServiceExportTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceExportTemplateStatus) DeepCopyInto(out *APIServiceExportTemplateStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1alpha1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceExportTemplateStatus. +func (in *APIServiceExportTemplateStatus) DeepCopy() *APIServiceExportTemplateStatus { + if in == nil { + return nil + } + out := new(APIServiceExportTemplateStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *APIServiceNamespace) DeepCopyInto(out *APIServiceNamespace) { *out = *in @@ -952,6 +1087,111 @@ func (in *ClusterSecretKeyRef) DeepCopy() *ClusterSecretKeyRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Collection) DeepCopyInto(out *Collection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collection. +func (in *Collection) DeepCopy() *Collection { + if in == nil { + return nil + } + out := new(Collection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Collection) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CollectionList) DeepCopyInto(out *CollectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Collection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectionList. +func (in *CollectionList) DeepCopy() *CollectionList { + if in == nil { + return nil + } + out := new(CollectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CollectionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CollectionSpec) DeepCopyInto(out *CollectionSpec) { + *out = *in + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]APIServiceExportTemplateReference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectionSpec. +func (in *CollectionSpec) DeepCopy() *CollectionSpec { + if in == nil { + return nil + } + out := new(CollectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CollectionStatus) DeepCopyInto(out *CollectionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1alpha1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectionStatus. +func (in *CollectionStatus) DeepCopy() *CollectionStatus { + if in == nil { + return nil + } + out := new(CollectionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomResourceConversion) DeepCopyInto(out *CustomResourceConversion) { *out = *in @@ -1141,8 +1381,8 @@ func (in *PermissionClaim) DeepCopy() *PermissionClaim { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Selector) DeepCopyInto(out *Selector) { *out = *in - if in.NamedResource != nil { - in, out := &in.NamedResource, &out.NamedResource + if in.NamedResources != nil { + in, out := &in.NamedResources, &out.NamedResources *out = make([]NamedResource, len(*in)) copy(*out, *in) } diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/collection.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/collection.go new file mode 100644 index 000000000..4da65a827 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/collection.go @@ -0,0 +1,70 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + context "context" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + scheme "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// CollectionsGetter has a method to return a CollectionInterface. +// A group's client should implement this interface. +type CollectionsGetter interface { + Collections(namespace string) CollectionInterface +} + +// CollectionInterface has methods to work with Collection resources. +type CollectionInterface interface { + Create(ctx context.Context, collection *kubebindv1alpha2.Collection, opts v1.CreateOptions) (*kubebindv1alpha2.Collection, error) + Update(ctx context.Context, collection *kubebindv1alpha2.Collection, opts v1.UpdateOptions) (*kubebindv1alpha2.Collection, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, collection *kubebindv1alpha2.Collection, opts v1.UpdateOptions) (*kubebindv1alpha2.Collection, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*kubebindv1alpha2.Collection, error) + List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.CollectionList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *kubebindv1alpha2.Collection, err error) + CollectionExpansion +} + +// collections implements CollectionInterface +type collections struct { + *gentype.ClientWithList[*kubebindv1alpha2.Collection, *kubebindv1alpha2.CollectionList] +} + +// newCollections returns a Collections +func newCollections(c *KubeBindV1alpha2Client, namespace string) *collections { + return &collections{ + gentype.NewClientWithList[*kubebindv1alpha2.Collection, *kubebindv1alpha2.CollectionList]( + "collections", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *kubebindv1alpha2.Collection { return &kubebindv1alpha2.Collection{} }, + func() *kubebindv1alpha2.CollectionList { return &kubebindv1alpha2.CollectionList{} }, + ), + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_collection.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_collection.go new file mode 100644 index 000000000..eb75906c7 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_collection.go @@ -0,0 +1,50 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/typed/kubebind/v1alpha2" + gentype "k8s.io/client-go/gentype" +) + +// fakeCollections implements CollectionInterface +type fakeCollections struct { + *gentype.FakeClientWithList[*v1alpha2.Collection, *v1alpha2.CollectionList] + Fake *FakeKubeBindV1alpha2 +} + +func newFakeCollections(fake *FakeKubeBindV1alpha2, namespace string) kubebindv1alpha2.CollectionInterface { + return &fakeCollections{ + gentype.NewFakeClientWithList[*v1alpha2.Collection, *v1alpha2.CollectionList]( + fake.Fake, + namespace, + v1alpha2.SchemeGroupVersion.WithResource("collections"), + v1alpha2.SchemeGroupVersion.WithKind("Collection"), + func() *v1alpha2.Collection { return &v1alpha2.Collection{} }, + func() *v1alpha2.CollectionList { return &v1alpha2.CollectionList{} }, + func(dst, src *v1alpha2.CollectionList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha2.CollectionList) []*v1alpha2.Collection { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha2.CollectionList, items []*v1alpha2.Collection) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go index d1661b376..17d65407f 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go @@ -52,6 +52,10 @@ func (c *FakeKubeBindV1alpha2) ClusterBindings(namespace string) v1alpha2.Cluste return newFakeClusterBindings(c, namespace) } +func (c *FakeKubeBindV1alpha2) Collections(namespace string) v1alpha2.CollectionInterface { + return newFakeCollections(c, namespace) +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeKubeBindV1alpha2) RESTClient() rest.Interface { diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go index d0968607d..3be2b1782 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go @@ -29,3 +29,5 @@ type APIServiceNamespaceExpansion interface{} type BoundSchemaExpansion interface{} type ClusterBindingExpansion interface{} + +type CollectionExpansion interface{} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go index 6d26561d6..d31e864bb 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go @@ -34,6 +34,7 @@ type KubeBindV1alpha2Interface interface { APIServiceNamespacesGetter BoundSchemasGetter ClusterBindingsGetter + CollectionsGetter } // KubeBindV1alpha2Client is used to interact with features provided by the kube-bind.io group. @@ -65,6 +66,10 @@ func (c *KubeBindV1alpha2Client) ClusterBindings(namespace string) ClusterBindin return newClusterBindings(c, namespace) } +func (c *KubeBindV1alpha2Client) Collections(namespace string) CollectionInterface { + return newCollections(c, namespace) +} + // NewForConfig creates a new KubeBindV1alpha2Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/sdk/client/informers/externalversions/generic.go b/sdk/client/informers/externalversions/generic.go index 27b112efb..fa39a9633 100644 --- a/sdk/client/informers/externalversions/generic.go +++ b/sdk/client/informers/externalversions/generic.go @@ -78,6 +78,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().BoundSchemas().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusterbindings"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().ClusterBindings().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("collections"): + return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().Collections().Informer()}, nil } diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/collection.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/collection.go new file mode 100644 index 000000000..3f5932ac8 --- /dev/null +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/collection.go @@ -0,0 +1,102 @@ +/* +Copyright 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + context "context" + time "time" + + apiskubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + versioned "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" + internalinterfaces "github.com/kube-bind/kube-bind/sdk/client/informers/externalversions/internalinterfaces" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CollectionInformer provides access to a shared informer and lister for +// Collections. +type CollectionInformer interface { + Informer() cache.SharedIndexInformer + Lister() kubebindv1alpha2.CollectionLister +} + +type collectionInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCollectionInformer constructs a new informer for Collection type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCollectionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCollectionInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCollectionInformer constructs a new informer for Collection type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCollectionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Collections(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Collections(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Collections(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Collections(namespace).Watch(ctx, options) + }, + }, + &apiskubebindv1alpha2.Collection{}, + resyncPeriod, + indexers, + ) +} + +func (f *collectionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCollectionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *collectionInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiskubebindv1alpha2.Collection{}, f.defaultInformer) +} + +func (f *collectionInformer) Lister() kubebindv1alpha2.CollectionLister { + return kubebindv1alpha2.NewCollectionLister(f.Informer().GetIndexer()) +} diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go index e4abc8bb5..1e7187ba9 100644 --- a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go @@ -36,6 +36,8 @@ type Interface interface { BoundSchemas() BoundSchemaInformer // ClusterBindings returns a ClusterBindingInformer. ClusterBindings() ClusterBindingInformer + // Collections returns a CollectionInformer. + Collections() CollectionInformer } type version struct { @@ -78,3 +80,8 @@ func (v *version) BoundSchemas() BoundSchemaInformer { func (v *version) ClusterBindings() ClusterBindingInformer { return &clusterBindingInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// Collections returns a CollectionInformer. +func (v *version) Collections() CollectionInformer { + return &collectionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/sdk/client/listers/kubebind/v1alpha2/collection.go b/sdk/client/listers/kubebind/v1alpha2/collection.go new file mode 100644 index 000000000..6ff8440be --- /dev/null +++ b/sdk/client/listers/kubebind/v1alpha2/collection.go @@ -0,0 +1,70 @@ +/* +Copyright 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// CollectionLister helps list Collections. +// All objects returned here must be treated as read-only. +type CollectionLister interface { + // List lists all Collections in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.Collection, err error) + // Collections returns an object that can list and get Collections. + Collections(namespace string) CollectionNamespaceLister + CollectionListerExpansion +} + +// collectionLister implements the CollectionLister interface. +type collectionLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.Collection] +} + +// NewCollectionLister returns a new CollectionLister. +func NewCollectionLister(indexer cache.Indexer) CollectionLister { + return &collectionLister{listers.New[*kubebindv1alpha2.Collection](indexer, kubebindv1alpha2.Resource("collection"))} +} + +// Collections returns an object that can list and get Collections. +func (s *collectionLister) Collections(namespace string) CollectionNamespaceLister { + return collectionNamespaceLister{listers.NewNamespaced[*kubebindv1alpha2.Collection](s.ResourceIndexer, namespace)} +} + +// CollectionNamespaceLister helps list and get Collections. +// All objects returned here must be treated as read-only. +type CollectionNamespaceLister interface { + // List lists all Collections in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.Collection, err error) + // Get retrieves the Collection from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*kubebindv1alpha2.Collection, error) + CollectionNamespaceListerExpansion +} + +// collectionNamespaceLister implements the CollectionNamespaceLister +// interface. +type collectionNamespaceLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.Collection] +} diff --git a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go index 3aaa58aa1..fc8aa4060 100644 --- a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go +++ b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go @@ -61,3 +61,11 @@ type ClusterBindingListerExpansion interface{} // ClusterBindingNamespaceListerExpansion allows custom methods to be added to // ClusterBindingNamespaceLister. type ClusterBindingNamespaceListerExpansion interface{} + +// CollectionListerExpansion allows custom methods to be added to +// CollectionLister. +type CollectionListerExpansion interface{} + +// CollectionNamespaceListerExpansion allows custom methods to be added to +// CollectionNamespaceLister. +type CollectionNamespaceListerExpansion interface{} diff --git a/test/e2e/bind/fixtures/provider/collection.yaml b/test/e2e/bind/fixtures/provider/collection.yaml new file mode 100644 index 000000000..a456e1a06 --- /dev/null +++ b/test/e2e/bind/fixtures/provider/collection.yaml @@ -0,0 +1,8 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: Collection +metadata: + name: all +spec: + templates: + - name: mangodb + - name: foo \ No newline at end of file diff --git a/test/e2e/bind/fixtures/provider/template-foo.yaml b/test/e2e/bind/fixtures/provider/template-foo.yaml new file mode 100644 index 000000000..bc2d87393 --- /dev/null +++ b/test/e2e/bind/fixtures/provider/template-foo.yaml @@ -0,0 +1,33 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportTemplate +metadata: + name: foo +spec: + scope: Cluster + resources: + - group: bar.io + versions: + - v1alpha1 + resource: foos + permissionClaims: + - group: "" + resource: configmaps + selector: + namedResources: + - name: named-configmap-only + namespace: default + - group: "" + resource: secrets + selector: + labelSelector: + matchLabels: + app: secrets + namedResources: + - name: test-secret + namespace: default + - name: named-secret-1 + namespace: default + - name: named-secret-2 + namespace: default + namespaces: + - name: consumer-configuration-ns \ No newline at end of file diff --git a/test/e2e/bind/fixtures/provider/template-mangodb.yaml b/test/e2e/bind/fixtures/provider/template-mangodb.yaml new file mode 100644 index 000000000..888d8ffdc --- /dev/null +++ b/test/e2e/bind/fixtures/provider/template-mangodb.yaml @@ -0,0 +1,33 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: APIServiceExportTemplate +metadata: + name: mangodb +spec: + scope: Namespaced + resources: + - group: mangodb.com + versions: + - v1alpha1 + resource: mangodbs + permissionClaims: + - group: "" + resource: configmaps + selector: + namedResources: + - name: named-configmap-only + namespace: default + - group: "" + resource: secrets + selector: + labelSelector: + matchLabels: + app: secrets + namedResources: + - name: test-secret + namespace: default + - name: named-secret-1 + namespace: default + - name: named-secret-2 + namespace: default + namespaces: + - name: consumer-configuration-ns \ No newline at end of file diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index 780937e55..06ded2270 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -18,7 +18,6 @@ package bind import ( "context" - "encoding/json" "fmt" "strings" "testing" @@ -144,65 +143,6 @@ spec: inv := <-invocations requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "-", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) - var request kubebindv1alpha2.APIServiceExportRequest - err := json.Unmarshal(inv.Stdin, &request) - require.NoError(t, err) - // Pre-seed provider side namespaces for secret management - request.Spec.Namespaces = []kubebindv1alpha2.Namespaces{ - { - Name: "consumer-secrets-ns", - }, - } - - // If we are in permissions claims mode - add configmaps & secrets - request.Spec.PermissionClaims = []kubebindv1alpha2.PermissionClaim{ - { - GroupResource: kubebindv1alpha2.GroupResource{ - Group: "", - Resource: "configmaps", - }, - Selector: kubebindv1alpha2.Selector{ - NamedResource: []kubebindv1alpha2.NamedResource{ - { - Name: "named-configmap-only", - Namespace: consumerNS, - }, - }, - }, - }, - { - GroupResource: kubebindv1alpha2.GroupResource{ - Group: "", - Resource: "secrets", - }, - Selector: kubebindv1alpha2.Selector{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "secrets", - }, - }, - NamedResource: []kubebindv1alpha2.NamedResource{ - { - Name: "test-secret", - Namespace: consumerNS, - }, - { - Name: "named-secret-1", - Namespace: consumerNS, - }, - { - Name: "named-secret-2", - Namespace: consumerNS, - }, - }, - }, - }, - } - - payload, err := json.Marshal(request) - require.NoError(t, err) - inv.Stdin = payload - framework.BindAPIService(t, inv.Stdin, "", inv.Args...) t.Logf("Waiting for %s CRD to be created on consumer side", serviceGVR.Resource) @@ -262,7 +202,7 @@ spec: } for _, ns := range namespaces.Items { - if ns.Name == "consumer-secrets-ns" && ns.Status.Namespace != "" { + if ns.Name == "consumer-configuration-ns" && ns.Status.Namespace != "" { actualProviderNamespace = ns.Status.Namespace foundPreSeededNamespace = true return true @@ -270,7 +210,7 @@ spec: } return false }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for pre-seeded APIServiceNamespace to be created on provider side") - require.True(t, foundPreSeededNamespace, "Pre-seeded namespace 'consumer-secrets-ns' should be created via APIServiceExportRequest.Spec.Namespaces") + require.True(t, foundPreSeededNamespace, "Pre-seeded namespace 'consumer-configuration-ns' should be created via APIServiceExportRequest.Spec.Namespaces") providerCoreClient := framework.KubeClient(t, providerConfig).CoreV1() _, err := providerCoreClient.Namespaces().Get(ctx, actualProviderNamespace, metav1.GetOptions{}) @@ -286,7 +226,7 @@ spec: var foundSecretClusterRole bool for _, cr := range clusterRoles.Items { - if strings.Contains(cr.Name, "kube-binder-") && strings.Contains(cr.Name, "-export-") { + if strings.Contains(cr.Name, "kube-binder-export") { for _, rule := range cr.Rules { for _, resource := range rule.Resources { if resource == "secrets" { @@ -808,8 +748,17 @@ func simulateBrowser(t *testing.T, authURLCh chan string, resource string) { t.Logf("Waiting for browser to be at /resources") framework.BrowserEventuallyAtPath(t, browser, "/resources") - t.Logf("Clicking %s", resource) - err = browser.Click("a." + resource) + // Convert resource name to template name + var templateName string + switch resource { + case "mangodbs": + templateName = "mangodb" + case "foos": + templateName = "foo" + } + + t.Logf("Clicking template %s", templateName) + err = browser.Click("a." + templateName) require.NoError(t, err) t.Logf("Waiting for browser to be forwarded to client") diff --git a/test/e2e/framework/backend.go b/test/e2e/framework/backend.go index a29d26cce..2e0f4fc6b 100644 --- a/test/e2e/framework/backend.go +++ b/test/e2e/framework/backend.go @@ -45,6 +45,8 @@ func InstallKubebindCRDs(t testing.TB, clientConfig *rest.Config) { metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiservicenamespaces"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiserviceexportrequests"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "boundschemas"}, + metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiserviceexporttemplates"}, + metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "collections"}, ) require.NoError(t, err) } From d59fde359a822253ded90a1bf36acb2a0ec1fc08 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Thu, 23 Oct 2025 17:19:19 +0300 Subject: [PATCH 2/3] Update README.md Co-authored-by: Nelo-T. Wallus <10514301+ntnn@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1004416f8..2af9f9c54 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ To get familiar with setting up the environment, please check out docs at [kube- ### Catalog API Introduction of new `Collection` and `APIServiceExportTemplate` CRDs for better service organization: - **Collections**: Function as folders in the UI, grouping related modules -- **APIServiceExportTemplate**: Group multiple CRDs with their related resources and permission claims and creates `APIServiceExportRequest` +- **APIServiceExportTemplate**: Group multiple CRDs with their related resources and permission claims that can be used to create a `APIServiceExportRequest` ### Enhanced Permission Claims Major improvements to `PermissionClaims` in APIServiceExportSpec: From 74815d4cea0e0ee92a56a79103cb32211cc78308 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Thu, 23 Oct 2025 17:47:25 +0300 Subject: [PATCH 3/3] nit updates --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++++++ README.md | 40 ------------------------------------ docs/content/index.md | 7 +++---- 3 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..cebd980d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# CHANGELOG + +Change log is high level summary of notable changes for each release. + +## API Changes in v0.6.0 release + +### Helm Chart Updates + +Introduction of helm chart, `kube-bind/backend`, to deploy the backend components with configurable options. + +### Catalog API +Introduction of new `Collection` and `APIServiceExportTemplate` CRDs for better service organization: +- **Collections**: Function as folders in the UI, grouping bindable resources such as templates +- **APIServiceExportTemplate**: Group multiple CRDs with their related resources and permission claims that can be used to create a `APIServiceExportRequest` + +### Enhanced Permission Claims +Major improvements to `PermissionClaims` in APIServiceExportSpec: +- **NamedResources**: Specify exact resources by name and namespace +- **Combined Selectors**: Use both label selectors AND named resources (both must match) +- **Granular Control**: More precise access control for service resources + +### Provider-side Namespace Management +Enhanced namespace management on the provider side: +- **APIServiceNamespace Controller**: Automatically creates Roles and RoleBindings +- **Namespace Isolation**: Each consumer gets isolated provider-side namespaces +- **RBAC Automation**: Proper permissions created based on scope (namespaced vs cluster-scoped) +- **Namespace Pre-provisioning**: Providers can pre-create namespaces for better UX + +**Important**: When `ClusterScope` mode is used, cluster-wide permissions are created instead of namespaced ones. + +## API Changes in v0.5.0 release + +Version v0.5.0 includes significant architectural improvements to the API structure: + +### Major Changes + +- **API Version Upgrade**: Introduced `v1alpha2` API version alongside existing `v1alpha1` +- **Service Exposure Refactoring**: Refactored the service exposure mechanism from embedded CRD specifications to a resource-based model: + - `APIServiceExportSpec` now uses `Resources []APIServiceExportResource` instead of embedded CRD specs + - `BoundSchema`: New resource type in `v1alpha2` that represents bound schemas in consumer clusters and tracks the status of synced resources + - This allows one APIServiceExport to reference multiple CRDs more efficiently + +### Backend Architecture Improvements + +- **MultiCluster Runtime Integration**: The backend now leverages `sigs.k8s.io/multicluster-runtime` for enhanced cluster management capabilities +- **Provider Support**: Built-in support for multiple backend providers including KCP through `github.com/kcp-dev/multicluster-provider` +- **Enhanced Cluster Operations**: Improved cluster-aware resource management with dedicated manager architecture for handling multi-cluster scenarios diff --git a/README.md b/README.md index 2af9f9c54..5452d6b15 100644 --- a/README.md +++ b/README.md @@ -62,46 +62,6 @@ All the actions shown between the clusters are done by the konnector, except: th To get familiar with setting up the environment, please check out docs at [kube-bind.io](https://docs.kube-bind.io/main/setup). -## API Changes in v0.6.0 release - -### Catalog API -Introduction of new `Collection` and `APIServiceExportTemplate` CRDs for better service organization: -- **Collections**: Function as folders in the UI, grouping related modules -- **APIServiceExportTemplate**: Group multiple CRDs with their related resources and permission claims that can be used to create a `APIServiceExportRequest` - -### Enhanced Permission Claims -Major improvements to `PermissionClaims` in APIServiceExportSpec: -- **NamedResources**: Specify exact resources by name and namespace -- **Combined Selectors**: Use both label selectors AND named resources (both must match) -- **Granular Control**: More precise access control for service resources - -### Provider-side Namespace Management -Enhanced namespace management on the provider side: -- **APIServiceNamespace Controller**: Automatically creates Roles and RoleBindings -- **Namespace Isolation**: Each consumer gets isolated provider-side namespaces -- **RBAC Automation**: Proper permissions created based on scope (namespaced vs cluster-scoped) -- **Namespace Pre-provisioning**: Providers can pre-create namespaces for better UX - -**Important**: When `ClusterScope` mode is used, cluster-wide permissions are created instead of namespaced ones. - -## API Changes in v0.5.0 release - -Version v0.5.0 includes significant architectural improvements to the API structure: - -### Major Changes - -- **API Version Upgrade**: Introduced `v1alpha2` API version alongside existing `v1alpha1` -- **Service Exposure Refactoring**: Refactored the service exposure mechanism from embedded CRD specifications to a resource-based model: - - `APIServiceExportSpec` now uses `Resources []APIServiceExportResource` instead of embedded CRD specs - - `BoundSchema`: New resource type in `v1alpha2` that represents bound schemas in consumer clusters and tracks the status of synced resources - - This allows one APIServiceExport to reference multiple CRDs more efficiently - -### Backend Architecture Improvements - -- **MultiCluster Runtime Integration**: The backend now leverages `sigs.k8s.io/multicluster-runtime` for enhanced cluster management capabilities -- **Provider Support**: Built-in support for multiple backend providers including KCP through `github.com/kcp-dev/multicluster-provider` -- **Enhanced Cluster Operations**: Improved cluster-aware resource management with dedicated manager architecture for handling multi-cluster scenarios - ### Limitations These limitations are part of the roadmap and will be addressed in the future. diff --git a/docs/content/index.md b/docs/content/index.md index f57713b9e..8facfcb1a 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -12,11 +12,10 @@ kube-bind is a prototype project that aims to provide better support for service - A single vendor-neutral, OpenSource agent per consumer cluster connects it with the requested services. ## Key Features - -- **Catalog API**: Organize exported services using Collections (folders) and Modules (service definitions) -- **Provider-side Namespace Management**: Automatic namespace provisioning and RBAC setup on provider clusters - **Enhanced Permission Claims**: Granular resource access with both label selectors and named resource support -- **Multi-backend Support**: Works with standard Kubernetes and KCP backends through multicluster-runtime +- **Multi-backend Support**: Works with standard Kubernetes and kcp backends through multicluster-runtime +- **Secure Communication**: TLS-encrypted channels between service consumers and providers +- **Flexible UI**: Web-based dashboard for managing services and permissions when binding services ## Quickstart