diff --git a/contrib/deploy/crd/bootstrap.go b/contrib/deploy/crd/bootstrap.go new file mode 100644 index 000000000..8751ed705 --- /dev/null +++ b/contrib/deploy/crd/bootstrap.go @@ -0,0 +1,181 @@ +/* +Copyright 2023 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 crd + +import ( + "context" + "embed" + "fmt" + "sync" + "time" + + crdhelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" + apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + kerrors "k8s.io/apimachinery/pkg/util/errors" + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" +) + +//go:embed *.yaml +var raw embed.FS + +// CreateFromFS creates the given CRDs using the target client from the +// provided filesystem and waits for it to become established. This call is blocking. +func CreateFromFS(ctx context.Context, client apiextensionsv1client.CustomResourceDefinitionInterface, fs embed.FS, grs ...metav1.GroupResource) error { + wg := sync.WaitGroup{} + bootstrapErrChan := make(chan error, len(grs)) + for _, gk := range grs { + wg.Add(1) + go func(gr metav1.GroupResource) { + defer wg.Done() + err := retryRetryableErrors(func() error { + return createSingleFromFS(ctx, client, gr, fs) + }) + // wait.Poll functions return ErrWaitTimeout instead the context cancellation error, for backward compatibility reasons, see: + // https://github.com/kubernetes/kubernetes/blob/b5f8cca701575678819b5e9e6372df989ab6799f/staging/src/k8s.io/apimachinery/pkg/util/wait/wait.go + // however, retryOnError swallows that error and replaces it for the last one, that is nil if it is still retrying, see: + // https://github.com/kubernetes/kubernetes/blob/ee81e5ebfad1b3f3c1112e7b83b0a5113286a3d3/pkg/client/unversioned/util.go + // if the context is cancelled, we have to inform the upper layers about that, so context error takes precedence. + if ctx.Err() != nil { + err = ctx.Err() + } + bootstrapErrChan <- err + }(gk) + } + wg.Wait() + close(bootstrapErrChan) + var bootstrapErrors []error + for err := range bootstrapErrChan { + bootstrapErrors = append(bootstrapErrors, err) + } + if err := kerrors.NewAggregate(bootstrapErrors); err != nil { + return fmt.Errorf("could not bootstrap CRDs: %w", err) + } + return nil +} + +// Create creates the given CRDs using the target client and waits +// for all of them to become established in parallel. This call is blocking. +func Create(ctx context.Context, client apiextensionsv1client.CustomResourceDefinitionInterface, grs ...metav1.GroupResource) error { + return CreateFromFS(ctx, client, raw, grs...) +} + +// CreateFromFS creates the given CRD using the target client from the +// provided filesystem and waits for it to become established. This call is blocking. +func createSingleFromFS(ctx context.Context, client apiextensionsv1client.CustomResourceDefinitionInterface, gr metav1.GroupResource, fs embed.FS) error { + crd, err := CRD(fs, gr) + if err != nil { + return err + } + + return CreateSingle(ctx, client, crd) +} + +// CRD returns an *apiextensionsv1.CustomResourceDefinition for the GroupResource specified by gr from fs. The embedded +// file's name must have the format _.yaml. +func CRD(fs embed.FS, gr metav1.GroupResource) (*apiextensionsv1.CustomResourceDefinition, error) { + raw, err := fs.ReadFile(fmt.Sprintf("%s_%s.yaml", gr.Group, gr.Resource)) + if err != nil { + return nil, fmt.Errorf("could not read CRD %s: %w", gr.String(), err) + } + + expectedGvk := &schema.GroupVersionKind{Group: apiextensionsv1.GroupName, Version: "v1", Kind: "CustomResourceDefinition"} + + obj, gvk, err := extensionsapiserver.Codecs.UniversalDeserializer().Decode(raw, expectedGvk, &apiextensionsv1.CustomResourceDefinition{}) + if err != nil { + return nil, fmt.Errorf("could not decode raw CRD %s: %w", gr.String(), err) + } + + if !equality.Semantic.DeepEqual(gvk, expectedGvk) { + return nil, fmt.Errorf("decoded CRD %s into incorrect GroupVersionKind, got %#v, wanted %#v", gr.String(), gvk, expectedGvk) + } + + crd, ok := obj.(*apiextensionsv1.CustomResourceDefinition) + if !ok { + return nil, fmt.Errorf("decoded CRD %s into incorrect type, got %T, wanted %T", gr.String(), obj, &apiextensionsv1.CustomResourceDefinition{}) + } + + return crd, nil +} + +func CreateSingle(ctx context.Context, client apiextensionsv1client.CustomResourceDefinitionInterface, rawCRD *apiextensionsv1.CustomResourceDefinition) error { + start := time.Now() + klog.V(4).Infof("Bootstrapping %v", rawCRD.Name) + + updateNeeded := false + crd, err := client.Get(ctx, rawCRD.Name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + crd, err = client.Create(ctx, rawCRD, metav1.CreateOptions{}) + if err != nil { + // If multiple post-start hooks specify the same CRD, they could race with each other, so we need to + // handle the scenario where another hook created this CRD after our Get() call returned not found. + if apierrors.IsAlreadyExists(err) { + // Re-get so we have the correct resourceVersion + crd, err = client.Get(ctx, rawCRD.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("error getting CRD %s: %w", rawCRD.Name, err) + } + updateNeeded = true + } else { + return fmt.Errorf("error creating CRD %s: %w", rawCRD.Name, err) + } + } else { + klog.Infof("Bootstrapped CRD %v after %s", crd.Name, time.Since(start).String()) + } + } else { + return fmt.Errorf("error fetching CRD %s: %w", rawCRD.Name, err) + } + } else { + updateNeeded = true + } + + if updateNeeded { + rawCRD.ResourceVersion = crd.ResourceVersion + _, err := client.Update(ctx, rawCRD, metav1.UpdateOptions{}) + if err != nil { + return err + } + klog.Infof("Updated CRD %v after %s", rawCRD.Name, time.Since(start).String()) + } + + return wait.PollImmediateInfiniteWithContext(ctx, 100*time.Millisecond, func(ctx context.Context) (bool, error) { + crd, err := client.Get(ctx, rawCRD.Name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return false, fmt.Errorf("CRD %s was deleted before being established", rawCRD.Name) + } + return false, fmt.Errorf("error fetching CRD %s: %w", rawCRD.Name, err) + } + + return crdhelpers.IsCRDConditionTrue(crd, apiextensionsv1.Established), nil + }) +} + +func retryRetryableErrors(f func() error) error { + return retry.OnError(retry.DefaultBackoff, func(err error) bool { + return utilnet.IsConnectionRefused(err) || apierrors.IsTooManyRequests(err) || apierrors.IsConflict(err) + }, f) +} diff --git a/contrib/deploy/crd/example-backend.kube-bind.io_apiserviceexporttemplates.yaml b/contrib/deploy/crd/example-backend.kube-bind.io_apiserviceexporttemplates.yaml new file mode 100644 index 000000000..d2e71d7e9 --- /dev/null +++ b/contrib/deploy/crd/example-backend.kube-bind.io_apiserviceexporttemplates.yaml @@ -0,0 +1,352 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: apiserviceexporttemplates.example-backend.kube-bind.io +spec: + group: example-backend.kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceExportTemplate + listKind: APIServiceExportTemplateList + plural: apiserviceexporttemplates + singular: apiserviceexporttemplate + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Established")].status + name: Established + priority: 5 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: APIServiceExportTemplate specifies the resource to be exported. + It references the CRD to be exported along with additional resources that + are synchronized from and to the consumer cluster. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec specifies the resource. + properties: + APIServiceSelector: + description: apiServiceSelector describes the groupresource and versions + of the api that will be offered to bind to consumer clusters. + 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 + version: + minLength: 1 + type: string + required: + - resource + type: object + permissionClaims: + description: permissionClaims are a list of permission claims for + the provider to read or create/update additional resources on the + consumers cluster. Empty by default. + 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: + autoAdopt: + description: autoAdopt set to true means that objects created + by the consumer are adopted by the provider. i.e. the provider + will become the owner. Mutually exclusive with autoDonate. + type: boolean + autoDonate: + description: autoDonate set to true means that a newly created + object by the provider is immediately owned by the consumer. + If false, the object stays in ownership of the provider. Mutually + exclusive with autoDonate. + type: boolean + create: + description: create determines whether the kube-bind konnector + will sync matching objects from the provider cluster down + to the consumer cluster. only for owner Provider + properties: + replaceExisting: + description: "replaceExisting means that an existing object + owned by the consumer will be replaced by the provider + object. \n If not true, and a conflicting consumer object + exists, it is not touched." + type: boolean + type: object + 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 + onConflict: + description: onConflict determines how the conflicts between + objects on the consumer cluster will be resolved. + properties: + recreateWhenConsumerSideDeleted: + default: true + description: "recreateWhenConsumerSideDeleted set to true + (the default) means the provider will recreate the object + in case the object is missing on the consumer cluster, + but has been synchronized before. \n If set to false, + deleted provider-owned objects get deleted on the provider + cluster as well." + type: boolean + type: object + read: + description: read claims read access to matching objects for + the provider. Reading of the claimed object(s) is always claimed. + By default, no labels and annotations can be read by the provider. + Reading of labels and annotations can be claimed in addition + by specifying them explicitly. If labels on consumer owned + objects that are set by the consumer are read, labelsOnProviderOwnedObjects + and annotationsOnProviderOwnedObjects can be set. + properties: + annotations: + description: annotations is a list of claimed annotation + key wildcard patterns that are synchronized from the consumer + cluster to the provider on objects that are owned by the + consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labels: + description: labels is a list of claimed label key wildcard + patterns that are synchronized from the consumer cluster + to the provider on objects that are owned by the consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnProviderOwnedObjects: + description: labelsOnProviderOwnedObjects is a list of claimed + label key wildcard patterns that are synchronized from + the consumer cluster to the provider on objects owned + by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + overrideAnnotations: + description: overrideAnnotations is a list of claimed annotation + key wildcard patterns that are synchronized from the consumer + cluster to the provider on objects owned by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + type: object + required: + description: required indicates whether the APIServiceBinding + will work if this claim is not accepted. If a required claim + is denied, the binding is aborted. + type: boolean + 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 selects which resources are being claimed. + If unset, all resources across all namespaces are being claimed. + properties: + fieldSelectors: + description: fieldSelectors is a list of field selectors + matching selected resources, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/. + items: + type: string + type: array + labelSelectors: + description: labelSelectors is a list of label selectors + matching selected resources. label selectors follow the + same rules as kubernetes label selectors, see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. + items: + additionalProperties: + type: string + type: object + type: array + names: + default: + - '*' + description: "names is a list of specific resource names + to select. Names matches the metadata.name field of the + underlying object. An entry of \"*\" anywhere in the list + means all object names of the group/resource within the + \"namespaces\" field are claimed. Wildcard entries other + than \"*\" and regular expressions are currently unsupported. + If a resources name matches any value in names, the resource + name is considered matching. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names or + * are allowed\"" + items: + type: string + type: array + namespaces: + default: + - '*' + description: "namespaces represents namespaces where an + object of the given group/resource may be managed. Namespaces + matches against the metadata.namespace field. A value + of \"*\" matches namespaced objects across all namespaces. + If a resources namespace matches any value in namespaces, + the resource namespace is considered matching. If the + claim is for a cluster-scoped resource, namespaces has + to explicitly be set to an empty array to prevent defaulting + to \"*\". If the \"names\" field is unset, all objects + of the group/resource within the listed namespaces (or + cluster) will be claimed. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names or + * are allowed\"" + items: + type: string + type: array + owner: + description: owner matches the resource's owner. If an owner + selector is set, resources owned by other owners will + not be claimed. Resources without a present owner will + be considered, if configured owner could be the owner + of the object. For example, if the consumer creates a + resource that is claimed by the provider for reading. + In this case the resource will be marked as owned by the + consumer, and handled as such in further reconciliations. + An unset owner selector means objects from both sides + are considered. + enum: + - Provider + - Consumer + type: string + type: object + update: + description: update lists which updates to objects on the consumer + cluster are claimed. By default, the whole object is synced, + but metadata is not. + properties: + alwaysRecreate: + description: "alwaysRecreate, when true will delete the + old object and create new ones instead of updating. Useful + for immutable objects. \n This does not apply to metadata + field updates." + type: boolean + annotations: + description: "annotations is a list of claimed annotation + keys or annotation wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the provider. \n By default, no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + annotationsOnConsumerOwnedObjects: + description: "annotationsOnConsumerOwnedObjects is a list + of claimed annotation key wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the consumer. \n By default, no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + fields: + description: "fields are a list of JSON Paths describing + which parts of an object the provider wants to control. + \n This field is ignored if the owner in the claim selector + is set to \"Provider\"." + items: + type: string + type: array + labels: + description: "labels is a list of claimed label keys or + label wildcard patterns that are synchronized from the + provider to the consumer for objects owned by the provider. + \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnConsumerOwnedObjects: + description: "labelsOnConsumerOwnedObjects is a list of + claimed label key wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the consumer. \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + preserving: + description: "preserving is a list of JSON Paths describing + which parts of an object owned by the provider the consumer + keeps controlling. \n This field is ignored if the owner + in the claim selector is set to \"Consumer\"." + items: + type: string + type: array + type: object + version: + description: version is the version of the claimed resource. + minLength: 1 + type: string + required: + - resource + - version + type: object + x-kubernetes-validations: + - message: donate and adopt are mutually exclusive + rule: '!(has(self.autoDonate) && self.autoDonate && has(self.autoAdopt) + && self.autoAdopt)' + type: array + type: object + status: + description: status contains reconciliation information for the resource. + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/example-backend/apis/examplebackend/v1alpha1/apiserviceexporttemplate_types.go b/contrib/example-backend/apis/examplebackend/v1alpha1/apiserviceexporttemplate_types.go new file mode 100644 index 000000000..c378242f7 --- /dev/null +++ b/contrib/example-backend/apis/examplebackend/v1alpha1/apiserviceexporttemplate_types.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" +) + +// APIServiceExportTemplate specifies the resource to be exported. +// It references the CRD to be exported along with additional resources that +// are synchronized from and to the consumer cluster. +// +// +crd +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,categories=kube-bindings +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Established",type="string",JSONPath=`.status.conditions[?(@.type=="Established")].status`,priority=5 +// +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 resource. + // +required + // +kubebuilder:validation:Required + Spec APIServiceExportTemplateSpec `json:"spec"` + + // status contains reconciliation information for the resource. + Status APIServiceExportTemplateStatus `json:"status,omitempty"` +} + +type APIServiceExportTemplateSpec struct { + // apiServiceSelector describes the groupresource and versions of the api that will be offered to bind to consumer clusters. + // + // +required + APIServiceSelector APIServiceSelector `json:"APIServiceSelector"` + + // permissionClaims are a list of permission claims for the provider to read or create/update additional resources on the + // consumers cluster. Empty by default. + // + // +optional + PermissionClaims []v1alpha1.PermissionClaim `json:"permissionClaims,omitempty"` +} + +type APIServiceExportTemplateStatus struct{} + +type APIServiceSelector struct { + v1alpha1.GroupResource `json:","` + + // +required + // +kubebuilder:validation:MinLength:=1 + Version string `json:"version"` +} + +// APIServiceExportRequestList is the list of APIServiceExportRequest. +// +// +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/contrib/example-backend/apis/examplebackend/v1alpha1/doc.go b/contrib/example-backend/apis/examplebackend/v1alpha1/doc.go new file mode 100644 index 000000000..8fab39db3 --- /dev/null +++ b/contrib/example-backend/apis/examplebackend/v1alpha1/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2023 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 v1alpha1 defines the v1alpha1 version of the Example Backend API +// +// +groupName=example-backend.kube-bind.io +// +groupGoName=ExampleBackend +// +k8s:deepcopy-gen=package,register +// +kubebuilder:validation:Optional +package v1alpha1 diff --git a/contrib/example-backend/apis/examplebackend/v1alpha1/register.go b/contrib/example-backend/apis/examplebackend/v1alpha1/register.go new file mode 100644 index 000000000..5d9f73ccc --- /dev/null +++ b/contrib/example-backend/apis/examplebackend/v1alpha1/register.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +const ( + // GroupName is the group name used in this package + GroupName = "example-backend.kube-bind.io" + + // GroupVersion is the group version used in this package + GroupVersion = "v1alpha1" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &APIServiceExportTemplate{}, + &APIServiceExportTemplateList{}, + ) + + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/contrib/example-backend/apis/examplebackend/v1alpha1/zz_generated.deepcopy.go b/contrib/example-backend/apis/examplebackend/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..718cb9e13 --- /dev/null +++ b/contrib/example-backend/apis/examplebackend/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,146 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +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 deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" +) + +// 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) + out.Status = in.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 *APIServiceExportTemplateSpec) DeepCopyInto(out *APIServiceExportTemplateSpec) { + *out = *in + out.APIServiceSelector = in.APIServiceSelector + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]kubebindv1alpha1.PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + 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 + 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 *APIServiceSelector) DeepCopyInto(out *APIServiceSelector) { + *out = *in + out.GroupResource = in.GroupResource + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceSelector. +func (in *APIServiceSelector) DeepCopy() *APIServiceSelector { + if in == nil { + return nil + } + out := new(APIServiceSelector) + in.DeepCopyInto(out) + return out +} diff --git a/contrib/example-backend/client/clientset/versioned/clientset.go b/contrib/example-backend/client/clientset/versioned/clientset.go new file mode 100644 index 000000000..85fdb4669 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/clientset.go @@ -0,0 +1,122 @@ +/* +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 versioned + +import ( + "fmt" + "net/http" + + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" + + examplebackendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + ExampleBackendV1alpha1() examplebackendv1alpha1.ExampleBackendV1alpha1Interface +} + +// Clientset contains the clients for groups. Each group has exactly one +// version included in a Clientset. +type Clientset struct { + *discovery.DiscoveryClient + exampleBackendV1alpha1 *examplebackendv1alpha1.ExampleBackendV1alpha1Client +} + +// ExampleBackendV1alpha1 retrieves the ExampleBackendV1alpha1Client +func (c *Clientset) ExampleBackendV1alpha1() examplebackendv1alpha1.ExampleBackendV1alpha1Interface { + return c.exampleBackendV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.exampleBackendV1alpha1, err = examplebackendv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.exampleBackendV1alpha1 = examplebackendv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/contrib/example-backend/client/clientset/versioned/doc.go b/contrib/example-backend/client/clientset/versioned/doc.go new file mode 100644 index 000000000..10f8b80ef --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/doc.go @@ -0,0 +1,20 @@ +/* +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. + +// This package has the automatically generated clientset. +package versioned diff --git a/contrib/example-backend/client/clientset/versioned/fake/clientset_generated.go b/contrib/example-backend/client/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 000000000..e7e88c819 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,86 @@ +/* +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 ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" + + clientset "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned" + examplebackendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1" + fakeexamplebackendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// ExampleBackendV1alpha1 retrieves the ExampleBackendV1alpha1Client +func (c *Clientset) ExampleBackendV1alpha1() examplebackendv1alpha1.ExampleBackendV1alpha1Interface { + return &fakeexamplebackendv1alpha1.FakeExampleBackendV1alpha1{Fake: &c.Fake} +} diff --git a/contrib/example-backend/client/clientset/versioned/fake/doc.go b/contrib/example-backend/client/clientset/versioned/fake/doc.go new file mode 100644 index 000000000..ca4191ad7 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/fake/doc.go @@ -0,0 +1,20 @@ +/* +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. + +// This package has the automatically generated fake clientset. +package fake diff --git a/contrib/example-backend/client/clientset/versioned/fake/register.go b/contrib/example-backend/client/clientset/versioned/fake/register.go new file mode 100644 index 000000000..67292a373 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/fake/register.go @@ -0,0 +1,57 @@ +/* +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 ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + examplebackendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + examplebackendv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/contrib/example-backend/client/clientset/versioned/scheme/doc.go b/contrib/example-backend/client/clientset/versioned/scheme/doc.go new file mode 100644 index 000000000..b5ec927c6 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/scheme/doc.go @@ -0,0 +1,20 @@ +/* +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. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/contrib/example-backend/client/clientset/versioned/scheme/register.go b/contrib/example-backend/client/clientset/versioned/scheme/register.go new file mode 100644 index 000000000..5629c7885 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/scheme/register.go @@ -0,0 +1,57 @@ +/* +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 scheme + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + examplebackendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + examplebackendv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/apiserviceexporttemplate.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/apiserviceexporttemplate.go new file mode 100644 index 000000000..9465b30b8 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/apiserviceexporttemplate.go @@ -0,0 +1,196 @@ +/* +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 v1alpha1 + +import ( + "context" + "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" + + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" + scheme "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/scheme" +) + +// APIServiceExportTemplatesGetter has a method to return a APIServiceExportTemplateInterface. +// A group's client should implement this interface. +type APIServiceExportTemplatesGetter interface { + APIServiceExportTemplates(namespace string) APIServiceExportTemplateInterface +} + +// APIServiceExportTemplateInterface has methods to work with APIServiceExportTemplate resources. +type APIServiceExportTemplateInterface interface { + Create(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.CreateOptions) (*v1alpha1.APIServiceExportTemplate, error) + Update(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.UpdateOptions) (*v1alpha1.APIServiceExportTemplate, error) + UpdateStatus(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.UpdateOptions) (*v1alpha1.APIServiceExportTemplate, 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) (*v1alpha1.APIServiceExportTemplate, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.APIServiceExportTemplateList, 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 *v1alpha1.APIServiceExportTemplate, err error) + APIServiceExportTemplateExpansion +} + +// aPIServiceExportTemplates implements APIServiceExportTemplateInterface +type aPIServiceExportTemplates struct { + client rest.Interface + ns string +} + +// newAPIServiceExportTemplates returns a APIServiceExportTemplates +func newAPIServiceExportTemplates(c *ExampleBackendV1alpha1Client, namespace string) *aPIServiceExportTemplates { + return &aPIServiceExportTemplates{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the aPIServiceExportTemplate, and returns the corresponding aPIServiceExportTemplate object, and an error if there is any. +func (c *aPIServiceExportTemplates) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + result = &v1alpha1.APIServiceExportTemplate{} + err = c.client.Get(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of APIServiceExportTemplates that match those selectors. +func (c *aPIServiceExportTemplates) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.APIServiceExportTemplateList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.APIServiceExportTemplateList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested aPIServiceExportTemplates. +func (c *aPIServiceExportTemplates) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a aPIServiceExportTemplate and creates it. Returns the server's representation of the aPIServiceExportTemplate, and an error, if there is any. +func (c *aPIServiceExportTemplates) Create(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.CreateOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + result = &v1alpha1.APIServiceExportTemplate{} + err = c.client.Post(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(aPIServiceExportTemplate). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a aPIServiceExportTemplate and updates it. Returns the server's representation of the aPIServiceExportTemplate, and an error, if there is any. +func (c *aPIServiceExportTemplates) Update(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.UpdateOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + result = &v1alpha1.APIServiceExportTemplate{} + err = c.client.Put(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + Name(aPIServiceExportTemplate.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(aPIServiceExportTemplate). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *aPIServiceExportTemplates) UpdateStatus(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.UpdateOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + result = &v1alpha1.APIServiceExportTemplate{} + err = c.client.Put(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + Name(aPIServiceExportTemplate.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(aPIServiceExportTemplate). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the aPIServiceExportTemplate and deletes it. Returns an error if one occurs. +func (c *aPIServiceExportTemplates) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *aPIServiceExportTemplates) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched aPIServiceExportTemplate. +func (c *aPIServiceExportTemplates) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.APIServiceExportTemplate, err error) { + result = &v1alpha1.APIServiceExportTemplate{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("apiserviceexporttemplates"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/doc.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/doc.go new file mode 100644 index 000000000..05ebc8af6 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +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. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/examplebackend_client.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/examplebackend_client.go new file mode 100644 index 000000000..0f670149e --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/examplebackend_client.go @@ -0,0 +1,108 @@ +/* +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 v1alpha1 + +import ( + "net/http" + + rest "k8s.io/client-go/rest" + + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" + "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/scheme" +) + +type ExampleBackendV1alpha1Interface interface { + RESTClient() rest.Interface + APIServiceExportTemplatesGetter +} + +// ExampleBackendV1alpha1Client is used to interact with features provided by the example-backend.kube-bind.io group. +type ExampleBackendV1alpha1Client struct { + restClient rest.Interface +} + +func (c *ExampleBackendV1alpha1Client) APIServiceExportTemplates(namespace string) APIServiceExportTemplateInterface { + return newAPIServiceExportTemplates(c, namespace) +} + +// NewForConfig creates a new ExampleBackendV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*ExampleBackendV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new ExampleBackendV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ExampleBackendV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &ExampleBackendV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new ExampleBackendV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *ExampleBackendV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new ExampleBackendV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *ExampleBackendV1alpha1Client { + return &ExampleBackendV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *ExampleBackendV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/doc.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/doc.go new file mode 100644 index 000000000..a0ee34800 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +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 has the automatically generated clients. +package fake diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/fake_apiserviceexporttemplate.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/fake_apiserviceexporttemplate.go new file mode 100644 index 000000000..f87a07e0e --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/fake_apiserviceexporttemplate.go @@ -0,0 +1,143 @@ +/* +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 ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" + + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" +) + +// FakeAPIServiceExportTemplates implements APIServiceExportTemplateInterface +type FakeAPIServiceExportTemplates struct { + Fake *FakeExampleBackendV1alpha1 + ns string +} + +var apiserviceexporttemplatesResource = schema.GroupVersionResource{Group: "example-backend.kube-bind.io", Version: "v1alpha1", Resource: "apiserviceexporttemplates"} + +var apiserviceexporttemplatesKind = schema.GroupVersionKind{Group: "example-backend.kube-bind.io", Version: "v1alpha1", Kind: "APIServiceExportTemplate"} + +// Get takes name of the aPIServiceExportTemplate, and returns the corresponding aPIServiceExportTemplate object, and an error if there is any. +func (c *FakeAPIServiceExportTemplates) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(apiserviceexporttemplatesResource, c.ns, name), &v1alpha1.APIServiceExportTemplate{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIServiceExportTemplate), err +} + +// List takes label and field selectors, and returns the list of APIServiceExportTemplates that match those selectors. +func (c *FakeAPIServiceExportTemplates) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.APIServiceExportTemplateList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(apiserviceexporttemplatesResource, apiserviceexporttemplatesKind, c.ns, opts), &v1alpha1.APIServiceExportTemplateList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.APIServiceExportTemplateList{ListMeta: obj.(*v1alpha1.APIServiceExportTemplateList).ListMeta} + for _, item := range obj.(*v1alpha1.APIServiceExportTemplateList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested aPIServiceExportTemplates. +func (c *FakeAPIServiceExportTemplates) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(apiserviceexporttemplatesResource, c.ns, opts)) + +} + +// Create takes the representation of a aPIServiceExportTemplate and creates it. Returns the server's representation of the aPIServiceExportTemplate, and an error, if there is any. +func (c *FakeAPIServiceExportTemplates) Create(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.CreateOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(apiserviceexporttemplatesResource, c.ns, aPIServiceExportTemplate), &v1alpha1.APIServiceExportTemplate{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIServiceExportTemplate), err +} + +// Update takes the representation of a aPIServiceExportTemplate and updates it. Returns the server's representation of the aPIServiceExportTemplate, and an error, if there is any. +func (c *FakeAPIServiceExportTemplates) Update(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.UpdateOptions) (result *v1alpha1.APIServiceExportTemplate, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(apiserviceexporttemplatesResource, c.ns, aPIServiceExportTemplate), &v1alpha1.APIServiceExportTemplate{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIServiceExportTemplate), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeAPIServiceExportTemplates) UpdateStatus(ctx context.Context, aPIServiceExportTemplate *v1alpha1.APIServiceExportTemplate, opts v1.UpdateOptions) (*v1alpha1.APIServiceExportTemplate, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(apiserviceexporttemplatesResource, "status", c.ns, aPIServiceExportTemplate), &v1alpha1.APIServiceExportTemplate{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIServiceExportTemplate), err +} + +// Delete takes name of the aPIServiceExportTemplate and deletes it. Returns an error if one occurs. +func (c *FakeAPIServiceExportTemplates) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(apiserviceexporttemplatesResource, c.ns, name, opts), &v1alpha1.APIServiceExportTemplate{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeAPIServiceExportTemplates) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(apiserviceexporttemplatesResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.APIServiceExportTemplateList{}) + return err +} + +// Patch applies the patch and returns the patched aPIServiceExportTemplate. +func (c *FakeAPIServiceExportTemplates) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.APIServiceExportTemplate, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(apiserviceexporttemplatesResource, c.ns, name, pt, data, subresources...), &v1alpha1.APIServiceExportTemplate{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIServiceExportTemplate), err +} diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/fake_examplebackend_client.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/fake_examplebackend_client.go new file mode 100644 index 000000000..4b1d95374 --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/fake/fake_examplebackend_client.go @@ -0,0 +1,41 @@ +/* +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 ( + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" + + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1" +) + +type FakeExampleBackendV1alpha1 struct { + *testing.Fake +} + +func (c *FakeExampleBackendV1alpha1) APIServiceExportTemplates(namespace string) v1alpha1.APIServiceExportTemplateInterface { + return &FakeAPIServiceExportTemplates{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeExampleBackendV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/generated_expansion.go b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/generated_expansion.go new file mode 100644 index 000000000..e9426f91c --- /dev/null +++ b/contrib/example-backend/client/clientset/versioned/typed/examplebackend/v1alpha1/generated_expansion.go @@ -0,0 +1,21 @@ +/* +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 v1alpha1 + +type APIServiceExportTemplateExpansion interface{} diff --git a/contrib/example-backend/client/informers/externalversions/examplebackend/interface.go b/contrib/example-backend/client/informers/externalversions/examplebackend/interface.go new file mode 100644 index 000000000..5c6fb8ecb --- /dev/null +++ b/contrib/example-backend/client/informers/externalversions/examplebackend/interface.go @@ -0,0 +1,46 @@ +/* +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 examplebackend + +import ( + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1" + internalinterfaces "github.com/kube-bind/kube-bind/contrib/example-backend/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1/apiserviceexporttemplate.go b/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1/apiserviceexporttemplate.go new file mode 100644 index 000000000..929074642 --- /dev/null +++ b/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1/apiserviceexporttemplate.go @@ -0,0 +1,91 @@ +/* +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 v1alpha1 + +import ( + "context" + time "time" + + 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" + + examplebackendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" + versioned "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned" + internalinterfaces "github.com/kube-bind/kube-bind/contrib/example-backend/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/client/listers/examplebackend/v1alpha1" +) + +// APIServiceExportTemplateInformer provides access to a shared informer and lister for +// APIServiceExportTemplates. +type APIServiceExportTemplateInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.APIServiceExportTemplateLister +} + +type aPIServiceExportTemplateInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewAPIServiceExportTemplateInformer constructs a new informer for APIServiceExportTemplate 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 NewAPIServiceExportTemplateInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredAPIServiceExportTemplateInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredAPIServiceExportTemplateInformer constructs a new informer for APIServiceExportTemplate 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 NewFilteredAPIServiceExportTemplateInformer(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.ExampleBackendV1alpha1().APIServiceExportTemplates(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ExampleBackendV1alpha1().APIServiceExportTemplates(namespace).Watch(context.TODO(), options) + }, + }, + &examplebackendv1alpha1.APIServiceExportTemplate{}, + resyncPeriod, + indexers, + ) +} + +func (f *aPIServiceExportTemplateInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredAPIServiceExportTemplateInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *aPIServiceExportTemplateInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&examplebackendv1alpha1.APIServiceExportTemplate{}, f.defaultInformer) +} + +func (f *aPIServiceExportTemplateInformer) Lister() v1alpha1.APIServiceExportTemplateLister { + return v1alpha1.NewAPIServiceExportTemplateLister(f.Informer().GetIndexer()) +} diff --git a/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1/interface.go b/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1/interface.go new file mode 100644 index 000000000..90b89d67a --- /dev/null +++ b/contrib/example-backend/client/informers/externalversions/examplebackend/v1alpha1/interface.go @@ -0,0 +1,45 @@ +/* +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 v1alpha1 + +import ( + internalinterfaces "github.com/kube-bind/kube-bind/contrib/example-backend/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // APIServiceExportTemplates returns a APIServiceExportTemplateInformer. + APIServiceExportTemplates() APIServiceExportTemplateInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// APIServiceExportTemplates returns a APIServiceExportTemplateInformer. +func (v *version) APIServiceExportTemplates() APIServiceExportTemplateInformer { + return &aPIServiceExportTemplateInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/contrib/example-backend/client/informers/externalversions/factory.go b/contrib/example-backend/client/informers/externalversions/factory.go new file mode 100644 index 000000000..6f8288421 --- /dev/null +++ b/contrib/example-backend/client/informers/externalversions/factory.go @@ -0,0 +1,181 @@ +/* +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 externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" + + versioned "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned" + examplebackend "github.com/kube-bind/kube-bind/contrib/example-backend/client/informers/externalversions/examplebackend" + internalinterfaces "github.com/kube-bind/kube-bind/contrib/example-backend/client/informers/externalversions/internalinterfaces" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +// Start initializes all requested informers. +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InternalInformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + ExampleBackend() examplebackend.Interface +} + +func (f *sharedInformerFactory) ExampleBackend() examplebackend.Interface { + return examplebackend.New(f, f.namespace, f.tweakListOptions) +} diff --git a/contrib/example-backend/client/informers/externalversions/generic.go b/contrib/example-backend/client/informers/externalversions/generic.go new file mode 100644 index 000000000..a1e5634be --- /dev/null +++ b/contrib/example-backend/client/informers/externalversions/generic.go @@ -0,0 +1,63 @@ +/* +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 externalversions + +import ( + "fmt" + + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" + + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=example-backend.kube-bind.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("apiserviceexporttemplates"): + return &genericInformer{resource: resource.GroupResource(), informer: f.ExampleBackend().V1alpha1().APIServiceExportTemplates().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/contrib/example-backend/client/informers/externalversions/internalinterfaces/factory_interfaces.go b/contrib/example-backend/client/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 000000000..7d649b9ec --- /dev/null +++ b/contrib/example-backend/client/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,41 @@ +/* +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 internalinterfaces + +import ( + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" + + versioned "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/contrib/example-backend/client/listers/examplebackend/v1alpha1/apiserviceexporttemplate.go b/contrib/example-backend/client/listers/examplebackend/v1alpha1/apiserviceexporttemplate.go new file mode 100644 index 000000000..52c9351df --- /dev/null +++ b/contrib/example-backend/client/listers/examplebackend/v1alpha1/apiserviceexporttemplate.go @@ -0,0 +1,100 @@ +/* +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 v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + + v1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" +) + +// APIServiceExportTemplateLister helps list APIServiceExportTemplates. +// All objects returned here must be treated as read-only. +type APIServiceExportTemplateLister interface { + // List lists all APIServiceExportTemplates in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.APIServiceExportTemplate, err error) + // APIServiceExportTemplates returns an object that can list and get APIServiceExportTemplates. + APIServiceExportTemplates(namespace string) APIServiceExportTemplateNamespaceLister + APIServiceExportTemplateListerExpansion +} + +// aPIServiceExportTemplateLister implements the APIServiceExportTemplateLister interface. +type aPIServiceExportTemplateLister struct { + indexer cache.Indexer +} + +// NewAPIServiceExportTemplateLister returns a new APIServiceExportTemplateLister. +func NewAPIServiceExportTemplateLister(indexer cache.Indexer) APIServiceExportTemplateLister { + return &aPIServiceExportTemplateLister{indexer: indexer} +} + +// List lists all APIServiceExportTemplates in the indexer. +func (s *aPIServiceExportTemplateLister) List(selector labels.Selector) (ret []*v1alpha1.APIServiceExportTemplate, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.APIServiceExportTemplate)) + }) + return ret, err +} + +// APIServiceExportTemplates returns an object that can list and get APIServiceExportTemplates. +func (s *aPIServiceExportTemplateLister) APIServiceExportTemplates(namespace string) APIServiceExportTemplateNamespaceLister { + return aPIServiceExportTemplateNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// APIServiceExportTemplateNamespaceLister helps list and get APIServiceExportTemplates. +// All objects returned here must be treated as read-only. +type APIServiceExportTemplateNamespaceLister interface { + // List lists all APIServiceExportTemplates in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.APIServiceExportTemplate, err error) + // Get retrieves the APIServiceExportTemplate from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.APIServiceExportTemplate, error) + APIServiceExportTemplateNamespaceListerExpansion +} + +// aPIServiceExportTemplateNamespaceLister implements the APIServiceExportTemplateNamespaceLister +// interface. +type aPIServiceExportTemplateNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all APIServiceExportTemplates in the indexer for a given namespace. +func (s aPIServiceExportTemplateNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.APIServiceExportTemplate, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.APIServiceExportTemplate)) + }) + return ret, err +} + +// Get retrieves the APIServiceExportTemplate from the indexer for a given namespace and name. +func (s aPIServiceExportTemplateNamespaceLister) Get(name string) (*v1alpha1.APIServiceExportTemplate, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("apiserviceexporttemplate"), name) + } + return obj.(*v1alpha1.APIServiceExportTemplate), nil +} diff --git a/contrib/example-backend/client/listers/examplebackend/v1alpha1/expansion_generated.go b/contrib/example-backend/client/listers/examplebackend/v1alpha1/expansion_generated.go new file mode 100644 index 000000000..0fbccdc2a --- /dev/null +++ b/contrib/example-backend/client/listers/examplebackend/v1alpha1/expansion_generated.go @@ -0,0 +1,27 @@ +/* +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 v1alpha1 + +// APIServiceExportTemplateListerExpansion allows custom methods to be added to +// APIServiceExportTemplateLister. +type APIServiceExportTemplateListerExpansion interface{} + +// APIServiceExportTemplateNamespaceListerExpansion allows custom methods to be added to +// APIServiceExportTemplateNamespaceLister. +type APIServiceExportTemplateNamespaceListerExpansion interface{} diff --git a/contrib/example-backend/controllers/clusterbinding/clusterbinding_reconcile.go b/contrib/example-backend/controllers/clusterbinding/clusterbinding_reconcile.go index 834487a1f..65fc8334a 100644 --- a/contrib/example-backend/controllers/clusterbinding/clusterbinding_reconcile.go +++ b/contrib/example-backend/controllers/clusterbinding/clusterbinding_reconcile.go @@ -148,11 +148,22 @@ func (r *reconciler) ensureRBACClusterRole(ctx context.Context, clusterBinding * }, } for _, export := range exports { + export := export expected.Rules = append(expected.Rules, rbacv1.PolicyRule{ APIGroups: []string{export.Spec.Group}, Resources: []string{export.Spec.Names.Plural}, Verbs: []string{"get", "list", "watch", "update", "patch", "delete", "create"}, }) + for _, e := range export.Spec.PermissionClaims { + e := e + // TODO more fine grained filtering + + expected.Rules = append(expected.Rules, rbacv1.PolicyRule{ + APIGroups: []string{e.Group}, + Resources: []string{e.Resource}, + Verbs: []string{"get", "list", "watch", "update", "patch", "delete", "create"}, + }) + } } if role == nil { diff --git a/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_controller.go b/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_controller.go index 852b00f8b..083060c92 100644 --- a/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_controller.go +++ b/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_controller.go @@ -35,6 +35,7 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" + "github.com/kube-bind/kube-bind/contrib/example-backend/exporttemplate" kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" bindclient "github.com/kube-bind/kube-bind/pkg/client/clientset/versioned" bindinformers "github.com/kube-bind/kube-bind/pkg/client/informers/externalversions/kubebind/v1alpha1" @@ -101,6 +102,7 @@ func NewController( deleteServiceExportRequest: func(ctx context.Context, ns, name string) error { return bindClient.KubeBindV1alpha1().APIServiceExportRequests(ns).Delete(ctx, name, metav1.DeleteOptions{}) }, + crds: exporttemplate.NewCatalog(config), }, commit: committer.NewCommitter[*kubebindv1alpha1.APIServiceExportRequest, *kubebindv1alpha1.APIServiceExportRequestSpec, *kubebindv1alpha1.APIServiceExportRequestStatus]( diff --git a/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go b/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go index 46c2be164..482f38b5d 100644 --- a/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go +++ b/contrib/example-backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go @@ -26,6 +26,7 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/klog/v2" + "github.com/kube-bind/kube-bind/contrib/example-backend/exporttemplate" kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1/helpers" conditionsapi "github.com/kube-bind/kube-bind/pkg/apis/third_party/conditions/apis/conditions/v1alpha1" @@ -40,6 +41,7 @@ type reconciler struct { createServiceExport func(ctx context.Context, resource *kubebindv1alpha1.APIServiceExport) (*kubebindv1alpha1.APIServiceExport, error) deleteServiceExportRequest func(ctx context.Context, namespace, name string) error + crds exporttemplate.Index } func (r *reconciler) reconcile(ctx context.Context, req *kubebindv1alpha1.APIServiceExportRequest) error { @@ -84,6 +86,11 @@ func (r *reconciler) ensureExports(ctx context.Context, req *kubebindv1alpha1.AP continue } + template, err := r.crds.TemplateFor(ctx, res.Group, res.Resource) + if err != nil { + return err + } + exportSpec, err := helpers.CRDToServiceExport(crd) if err != nil { conditions.MarkFalse( @@ -110,6 +117,7 @@ func (r *reconciler) ensureExports(ctx context.Context, req *kubebindv1alpha1.AP Spec: kubebindv1alpha1.APIServiceExportSpec{ APIServiceExportCRDSpec: *exportSpec, InformerScope: r.informerScope, + PermissionClaims: template.Spec.PermissionClaims, }, } diff --git a/contrib/example-backend/exporttemplate/index.go b/contrib/example-backend/exporttemplate/index.go new file mode 100644 index 000000000..a1e29a7dc --- /dev/null +++ b/contrib/example-backend/exporttemplate/index.go @@ -0,0 +1,93 @@ +/* +Copyright 2023 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 exporttemplate + +// TODO by namespace +// TODO cached client +// TODO find a better name or reorganize into different packages +import ( + "context" + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + crd "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + + "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" + templates "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned" +) + +type Index struct { + templates templates.Interface + crds crd.Interface + clusterNs string +} + +func NewCatalog(r *rest.Config) Index { + crdClient := crd.NewForConfigOrDie(r) + + templateClient := templates.NewForConfigOrDie(r) + + return Index{ + templates: templateClient, + crds: crdClient, + } +} + +func (i Index) GetExported(ctx context.Context) ([]apiextensionsv1.CustomResourceDefinition, error) { + list, err := i.crds.ApiextensionsV1().CustomResourceDefinitions().List(ctx, v1.ListOptions{}) + if err != nil { + return nil, err + } + exports, err := i.templates.ExampleBackendV1alpha1().APIServiceExportTemplates(i.clusterNs).List(ctx, v1.ListOptions{}) + if err != nil { + return nil, err + } + + exported := []apiextensionsv1.CustomResourceDefinition{} + + for _, ex := range exports.Items { + for _, c := range list.Items { + s := ex.Spec.APIServiceSelector + if s.Group == c.Spec.Group && s.Resource == c.Spec.Names.Plural { + exported = append(exported, c) + } + } + } + + if exported == nil { + return nil, fmt.Errorf("no exported resources") + } + + return exported, nil +} + +func (i Index) TemplateFor(ctx context.Context, group, resource string) (v1alpha1.APIServiceExportTemplate, error) { + exports, err := i.templates.ExampleBackendV1alpha1().APIServiceExportTemplates(i.clusterNs).List(ctx, v1.ListOptions{}) + if err != nil { + return v1alpha1.APIServiceExportTemplate{}, nil + } + + for _, e := range exports.Items { + if e.Spec.APIServiceSelector.Resource == resource && e.Spec.APIServiceSelector.Group == group { + return e, nil + } + } + + return v1alpha1.APIServiceExportTemplate{}, fmt.Errorf("not found: %s/%s", group, resource) +} diff --git a/contrib/example-backend/exporttemplate/index_test.go b/contrib/example-backend/exporttemplate/index_test.go new file mode 100644 index 000000000..7e100e351 --- /dev/null +++ b/contrib/example-backend/exporttemplate/index_test.go @@ -0,0 +1,113 @@ +/* +Copyright 2023 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 exporttemplate + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + crd "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" + templates "github.com/kube-bind/kube-bind/contrib/example-backend/client/clientset/versioned/fake" + kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" +) + +var mangodb = apiextensions.CustomResourceDefinition{ + ObjectMeta: v1.ObjectMeta{ + Name: "mangodbs.mangodb.com", + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "mangodb.com", + Scope: apiextensions.NamespaceScoped, + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "mangodbs", + Kind: "MangoDB", + }, + }, +} + +var dummy = apiextensions.CustomResourceDefinition{ + ObjectMeta: v1.ObjectMeta{ + Name: "dummies.example.com", + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "example.com", + Scope: apiextensions.NamespaceScoped, + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "dummies", + Kind: "dummies", + }, + }, +} + +var export = v1alpha1.APIServiceExportTemplate{ + Spec: v1alpha1.APIServiceExportTemplateSpec{ + APIServiceSelector: v1alpha1.APIServiceSelector{ + GroupResource: kubebindv1alpha1.GroupResource{ + Resource: "mangodbs", + Group: "mangodb.com", + }, + }, + }, + ObjectMeta: v1.ObjectMeta{ + Name: "mangodb.com", + Namespace: "cluster-x", + }, +} + +func TestListCRDsForAPIServiceExport(t *testing.T) { + t.Parallel() + + c := crd.NewSimpleClientset(&mangodb, &dummy) + templatesClient := templates.NewSimpleClientset(&export) + + ix := Index{ + templates: templatesClient, + crds: c, + } + + crdList, err := ix.GetExported(context.TODO()) + if err != nil { + t.Fatal(err) + } + + require.Equal(t, []apiextensions.CustomResourceDefinition{mangodb}, crdList) +} + +func TestGetAPIServiceExportTemplates(t *testing.T) { + t.Parallel() + + c := crd.NewSimpleClientset(&mangodb, &dummy) + templatesClient := templates.NewSimpleClientset(&export) + + ix := Index{ + templates: templatesClient, + crds: c, + } + + exported, err := ix.TemplateFor(context.TODO(), mangodb.Spec.Group, mangodb.Spec.Names.Plural) + if err != nil { + t.Fatal(err) + } + + require.Equal(t, export, exported) +} diff --git a/contrib/example-backend/http/handler.go b/contrib/example-backend/http/handler.go index dbbb4c69e..7a9285b98 100644 --- a/contrib/example-backend/http/handler.go +++ b/contrib/example-backend/http/handler.go @@ -18,6 +18,7 @@ package http import ( "bytes" + "context" "encoding/base64" "encoding/json" "errors" @@ -33,16 +34,14 @@ import ( "github.com/gorilla/securecookie" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionslisters "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" componentbaseversion "k8s.io/component-base/version" "k8s.io/klog/v2" "github.com/kube-bind/kube-bind/contrib/example-backend/cookie" + "github.com/kube-bind/kube-bind/contrib/example-backend/exporttemplate" "github.com/kube-bind/kube-bind/contrib/example-backend/kubernetes" - "github.com/kube-bind/kube-bind/contrib/example-backend/kubernetes/resources" "github.com/kube-bind/kube-bind/contrib/example-backend/template" kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" bindversion "github.com/kube-bind/kube-bind/pkg/version" @@ -71,9 +70,9 @@ type handler struct { cookieEncryptionKey []byte cookieSigningKey []byte - client *http.Client - apiextensionsLister apiextensionslisters.CustomResourceDefinitionLister - kubeManager *kubernetes.Manager + client *http.Client + templateIndex exporttemplate.Index + kubeManager *kubernetes.Manager } func NewHandler( @@ -82,7 +81,7 @@ func NewHandler( cookieSigningKey, cookieEncryptionKey []byte, scope kubebindv1alpha1.Scope, mgr *kubernetes.Manager, - apiextensionsLister apiextensionslisters.CustomResourceDefinitionLister, + apiextensionsLister exporttemplate.Index, ) (*handler, error) { return &handler{ oidc: provider, @@ -93,7 +92,7 @@ func NewHandler( scope: scope, client: http.DefaultClient, kubeManager: mgr, - apiextensionsLister: apiextensionsLister, + templateIndex: apiextensionsLister, cookieSigningKey: cookieSigningKey, cookieEncryptionKey: cookieEncryptionKey, }, nil @@ -292,10 +291,7 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { return } - labelSelector := labels.Set{ - resources.ExportedCRDsLabel: "true", - } - crds, err := h.apiextensionsLister.List(labelSelector.AsSelector()) + crds, err := h.templateIndex.GetExported(r.Context()) if err != nil { logger.Error(err, "failed to list crds") http.Error(w, "internal error", http.StatusInternalServerError) @@ -306,8 +302,9 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { }) rightScopedCRDs := []*apiextensionsv1.CustomResourceDefinition{} for _, crd := range crds { + crd := crd if h.scope == kubebindv1alpha1.ClusterScope || crd.Spec.Scope == apiextensionsv1.NamespaceScoped { - rightScopedCRDs = append(rightScopedCRDs, crd) + rightScopedCRDs = append(rightScopedCRDs, &crd) } } @@ -368,6 +365,13 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { return } + exportTemplate, err := h.templateIndex.TemplateFor(context.Background(), group, resource) + if err != nil { + logger.Error(err, "failed to get export template", "group", group, "resource", resource) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + request := kubebindv1alpha1.APIServiceExportRequestResponse{ TypeMeta: metav1.TypeMeta{ APIVersion: kubebindv1alpha1.SchemeGroupVersion.String(), @@ -381,7 +385,10 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { }, Spec: kubebindv1alpha1.APIServiceExportRequestSpec{ Resources: []kubebindv1alpha1.APIServiceExportRequestResource{ - {GroupResource: kubebindv1alpha1.GroupResource{Group: group, Resource: resource}}, + { + GroupResource: kubebindv1alpha1.GroupResource{Group: group, Resource: resource}, + PermissionClaims: exportTemplate.Spec.PermissionClaims, + }, }, }, } diff --git a/contrib/example-backend/server.go b/contrib/example-backend/server.go index 5d7fe481f..1ef1a3be1 100644 --- a/contrib/example-backend/server.go +++ b/contrib/example-backend/server.go @@ -31,6 +31,7 @@ import ( "github.com/kube-bind/kube-bind/contrib/example-backend/controllers/serviceexportrequest" "github.com/kube-bind/kube-bind/contrib/example-backend/controllers/servicenamespace" "github.com/kube-bind/kube-bind/contrib/example-backend/deploy" + "github.com/kube-bind/kube-bind/contrib/example-backend/exporttemplate" examplehttp "github.com/kube-bind/kube-bind/contrib/example-backend/http" examplekube "github.com/kube-bind/kube-bind/contrib/example-backend/kubernetes" kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" @@ -106,6 +107,7 @@ func NewServer(config *Config) (*Server, error) { } } + ind := exporttemplate.NewCatalog(config.ClientConfig) handler, err := examplehttp.NewHandler( s.OIDC, config.Options.OIDC.AuthorizeURL, @@ -116,7 +118,7 @@ func NewServer(config *Config) (*Server, error) { encryptionKey, kubebindv1alpha1.Scope(config.Options.ConsumerScope), s.Kubernetes, - config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions().Lister(), + ind, ) if err != nil { return nil, fmt.Errorf("error setting up HTTP Handler: %w", err) diff --git a/deploy/crd/kube-bind.io_apiservicebindings.yaml b/deploy/crd/kube-bind.io_apiservicebindings.yaml index 132432963..d1a27c04e 100644 --- a/deploy/crd/kube-bind.io_apiservicebindings.yaml +++ b/deploy/crd/kube-bind.io_apiservicebindings.yaml @@ -84,6 +84,285 @@ spec: x-kubernetes-validations: - message: kubeconfigSecretRef is immutable rule: self == oldSelf + permissionClaims: + description: permissionClaims records decisions about permission claims + requested by the API service provider. Individual claims can be + accepted or rejected. If accepted, the API service provider gets + the requested access to the specified resources in this workspace. + Access is granted per GroupResource and other properties like selectors. + items: + description: acceptablePermissionClaim is a permission claim that + stores the users acceptance in the field state. Only accepted + permission claims are reconciled. + properties: + autoAdopt: + description: autoAdopt set to true means that objects created + by the consumer are adopted by the provider. i.e. the provider + will become the owner. Mutually exclusive with autoDonate. + type: boolean + autoDonate: + description: autoDonate set to true means that a newly created + object by the provider is immediately owned by the consumer. + If false, the object stays in ownership of the provider. Mutually + exclusive with autoDonate. + type: boolean + create: + description: create determines whether the kube-bind konnector + will sync matching objects from the provider cluster down + to the consumer cluster. only for owner Provider + properties: + replaceExisting: + description: "replaceExisting means that an existing object + owned by the consumer will be replaced by the provider + object. \n If not true, and a conflicting consumer object + exists, it is not touched." + type: boolean + type: object + 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 + onConflict: + description: onConflict determines how the conflicts between + objects on the consumer cluster will be resolved. + properties: + recreateWhenConsumerSideDeleted: + default: true + description: "recreateWhenConsumerSideDeleted set to true + (the default) means the provider will recreate the object + in case the object is missing on the consumer cluster, + but has been synchronized before. \n If set to false, + deleted provider-owned objects get deleted on the provider + cluster as well." + type: boolean + type: object + read: + description: read claims read access to matching objects for + the provider. Reading of the claimed object(s) is always claimed. + By default, no labels and annotations can be read by the provider. + Reading of labels and annotations can be claimed in addition + by specifying them explicitly. If labels on consumer owned + objects that are set by the consumer are read, labelsOnProviderOwnedObjects + and annotationsOnProviderOwnedObjects can be set. + properties: + annotations: + description: annotations is a list of claimed annotation + key wildcard patterns that are synchronized from the consumer + cluster to the provider on objects that are owned by the + consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labels: + description: labels is a list of claimed label key wildcard + patterns that are synchronized from the consumer cluster + to the provider on objects that are owned by the consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnProviderOwnedObjects: + description: labelsOnProviderOwnedObjects is a list of claimed + label key wildcard patterns that are synchronized from + the consumer cluster to the provider on objects owned + by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + overrideAnnotations: + description: overrideAnnotations is a list of claimed annotation + key wildcard patterns that are synchronized from the consumer + cluster to the provider on objects owned by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + type: object + required: + description: required indicates whether the APIServiceBinding + will work if this claim is not accepted. If a required claim + is denied, the binding is aborted. + type: boolean + 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 selects which resources are being claimed. + If unset, all resources across all namespaces are being claimed. + properties: + fieldSelectors: + description: fieldSelectors is a list of field selectors + matching selected resources, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/. + items: + type: string + type: array + labelSelectors: + description: labelSelectors is a list of label selectors + matching selected resources. label selectors follow the + same rules as kubernetes label selectors, see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. + items: + additionalProperties: + type: string + type: object + type: array + names: + default: + - '*' + description: "names is a list of specific resource names + to select. Names matches the metadata.name field of the + underlying object. An entry of \"*\" anywhere in the list + means all object names of the group/resource within the + \"namespaces\" field are claimed. Wildcard entries other + than \"*\" and regular expressions are currently unsupported. + If a resources name matches any value in names, the resource + name is considered matching. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names or + * are allowed\"" + items: + type: string + type: array + namespaces: + default: + - '*' + description: "namespaces represents namespaces where an + object of the given group/resource may be managed. Namespaces + matches against the metadata.namespace field. A value + of \"*\" matches namespaced objects across all namespaces. + If a resources namespace matches any value in namespaces, + the resource namespace is considered matching. If the + claim is for a cluster-scoped resource, namespaces has + to explicitly be set to an empty array to prevent defaulting + to \"*\". If the \"names\" field is unset, all objects + of the group/resource within the listed namespaces (or + cluster) will be claimed. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names or + * are allowed\"" + items: + type: string + type: array + owner: + description: owner matches the resource's owner. If an owner + selector is set, resources owned by other owners will + not be claimed. Resources without a present owner will + be considered, if configured owner could be the owner + of the object. For example, if the consumer creates a + resource that is claimed by the provider for reading. + In this case the resource will be marked as owned by the + consumer, and handled as such in further reconciliations. + An unset owner selector means objects from both sides + are considered. + enum: + - Provider + - Consumer + type: string + type: object + state: + description: state indicates if the claim is accepted or rejected. + enum: + - Accepted + - Rejected + type: string + update: + description: update lists which updates to objects on the consumer + cluster are claimed. By default, the whole object is synced, + but metadata is not. + properties: + alwaysRecreate: + description: "alwaysRecreate, when true will delete the + old object and create new ones instead of updating. Useful + for immutable objects. \n This does not apply to metadata + field updates." + type: boolean + annotations: + description: "annotations is a list of claimed annotation + keys or annotation wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the provider. \n By default, no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + annotationsOnConsumerOwnedObjects: + description: "annotationsOnConsumerOwnedObjects is a list + of claimed annotation key wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the consumer. \n By default, no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + fields: + description: "fields are a list of JSON Paths describing + which parts of an object the provider wants to control. + \n This field is ignored if the owner in the claim selector + is set to \"Provider\"." + items: + type: string + type: array + labels: + description: "labels is a list of claimed label keys or + label wildcard patterns that are synchronized from the + provider to the consumer for objects owned by the provider. + \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnConsumerOwnedObjects: + description: "labelsOnConsumerOwnedObjects is a list of + claimed label key wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the consumer. \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + preserving: + description: "preserving is a list of JSON Paths describing + which parts of an object owned by the provider the consumer + keeps controlling. \n This field is ignored if the owner + in the claim selector is set to \"Consumer\"." + items: + type: string + type: array + type: object + version: + description: version is the version of the claimed resource. + minLength: 1 + type: string + required: + - resource + - state + - version + type: object + x-kubernetes-validations: + - message: donate and adopt are mutually exclusive + rule: '!(has(self.autoDonate) && self.autoDonate && has(self.autoAdopt) + && self.autoAdopt)' + type: array required: - kubeconfigSecretRef type: object diff --git a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml index 7a38c14f2..509cd1784 100644 --- a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml @@ -63,6 +63,291 @@ spec: 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 + permissionClaims: + description: permissionClaims records decisions about permission + claims requested by the service provider. Individual claims + can be accepted or rejected. If accepted, the API service + provider gets the requested access to the specified resources + in this workspace. Access is granted per GroupResource, identity, + and other properties. + 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: + autoAdopt: + description: autoAdopt set to true means that objects + created by the consumer are adopted by the provider. + i.e. the provider will become the owner. Mutually exclusive + with autoDonate. + type: boolean + autoDonate: + description: autoDonate set to true means that a newly + created object by the provider is immediately owned + by the consumer. If false, the object stays in ownership + of the provider. Mutually exclusive with autoDonate. + type: boolean + create: + description: create determines whether the kube-bind konnector + will sync matching objects from the provider cluster + down to the consumer cluster. only for owner Provider + properties: + replaceExisting: + description: "replaceExisting means that an existing + object owned by the consumer will be replaced by + the provider object. \n If not true, and a conflicting + consumer object exists, it is not touched." + type: boolean + type: object + 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 + onConflict: + description: onConflict determines how the conflicts between + objects on the consumer cluster will be resolved. + properties: + recreateWhenConsumerSideDeleted: + default: true + description: "recreateWhenConsumerSideDeleted set + to true (the default) means the provider will recreate + the object in case the object is missing on the + consumer cluster, but has been synchronized before. + \n If set to false, deleted provider-owned objects + get deleted on the provider cluster as well." + type: boolean + type: object + read: + description: read claims read access to matching objects + for the provider. Reading of the claimed object(s) is + always claimed. By default, no labels and annotations + can be read by the provider. Reading of labels and annotations + can be claimed in addition by specifying them explicitly. + If labels on consumer owned objects that are set by + the consumer are read, labelsOnProviderOwnedObjects + and annotationsOnProviderOwnedObjects can be set. + properties: + annotations: + description: annotations is a list of claimed annotation + key wildcard patterns that are synchronized from + the consumer cluster to the provider on objects + that are owned by the consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labels: + description: labels is a list of claimed label key + wildcard patterns that are synchronized from the + consumer cluster to the provider on objects that + are owned by the consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnProviderOwnedObjects: + description: labelsOnProviderOwnedObjects is a list + of claimed label key wildcard patterns that are + synchronized from the consumer cluster to the provider + on objects owned by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + overrideAnnotations: + description: overrideAnnotations is a list of claimed + annotation key wildcard patterns that are synchronized + from the consumer cluster to the provider on objects + owned by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + type: object + required: + description: required indicates whether the APIServiceBinding + will work if this claim is not accepted. If a required + claim is denied, the binding is aborted. + type: boolean + 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 selects which resources are being + claimed. If unset, all resources across all namespaces + are being claimed. + properties: + fieldSelectors: + description: fieldSelectors is a list of field selectors + matching selected resources, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/. + items: + type: string + type: array + labelSelectors: + description: labelSelectors is a list of label selectors + matching selected resources. label selectors follow + the same rules as kubernetes label selectors, see + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. + items: + additionalProperties: + type: string + type: object + type: array + names: + default: + - '*' + description: "names is a list of specific resource + names to select. Names matches the metadata.name + field of the underlying object. An entry of \"*\" + anywhere in the list means all object names of the + group/resource within the \"namespaces\" field are + claimed. Wildcard entries other than \"*\" and regular + expressions are currently unsupported. If a resources + name matches any value in names, the resource name + is considered matching. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names + or * are allowed\"" + items: + type: string + type: array + namespaces: + default: + - '*' + description: "namespaces represents namespaces where + an object of the given group/resource may be managed. + Namespaces matches against the metadata.namespace + field. A value of \"*\" matches namespaced objects + across all namespaces. If a resources namespace + matches any value in namespaces, the resource namespace + is considered matching. If the claim is for a cluster-scoped + resource, namespaces has to explicitly be set to + an empty array to prevent defaulting to \"*\". If + the \"names\" field is unset, all objects of the + group/resource within the listed namespaces (or + cluster) will be claimed. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names + or * are allowed\"" + items: + type: string + type: array + owner: + description: owner matches the resource's owner. If + an owner selector is set, resources owned by other + owners will not be claimed. Resources without a + present owner will be considered, if configured + owner could be the owner of the object. For example, + if the consumer creates a resource that is claimed + by the provider for reading. In this case the resource + will be marked as owned by the consumer, and handled + as such in further reconciliations. An unset owner + selector means objects from both sides are considered. + enum: + - Provider + - Consumer + type: string + type: object + update: + description: update lists which updates to objects on + the consumer cluster are claimed. By default, the whole + object is synced, but metadata is not. + properties: + alwaysRecreate: + description: "alwaysRecreate, when true will delete + the old object and create new ones instead of updating. + Useful for immutable objects. \n This does not apply + to metadata field updates." + type: boolean + annotations: + description: "annotations is a list of claimed annotation + keys or annotation wildcard patterns that are synchronized + from the provider to the consumer for objects owned + by the provider. \n By default, no annotations are + synced." + items: + properties: + pattern: + type: string + type: object + type: array + annotationsOnConsumerOwnedObjects: + description: "annotationsOnConsumerOwnedObjects is + a list of claimed annotation key wildcard patterns + that are synchronized from the provider to the consumer + for objects owned by the consumer. \n By default, + no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + fields: + description: "fields are a list of JSON Paths describing + which parts of an object the provider wants to control. + \n This field is ignored if the owner in the claim + selector is set to \"Provider\"." + items: + type: string + type: array + labels: + description: "labels is a list of claimed label keys + or label wildcard patterns that are synchronized + from the provider to the consumer for objects owned + by the provider. \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnConsumerOwnedObjects: + description: "labelsOnConsumerOwnedObjects is a list + of claimed label key wildcard patterns that are + synchronized from the provider to the consumer for + objects owned by the consumer. \n By default, no + labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + preserving: + description: "preserving is a list of JSON Paths describing + which parts of an object owned by the provider the + consumer keeps controlling. \n This field is ignored + if the owner in the claim selector is set to \"Consumer\"." + items: + type: string + type: array + type: object + version: + description: version is the version of the claimed resource. + minLength: 1 + type: string + required: + - resource + - version + type: object + x-kubernetes-validations: + - message: donate and adopt are mutually exclusive + rule: '!(has(self.autoDonate) && self.autoDonate && has(self.autoAdopt) + && self.autoAdopt)' + type: array resource: description: 'resource is the name of the resource. Note: it is worth noting that you can not ask for permissions for resource diff --git a/deploy/crd/kube-bind.io_apiserviceexports.yaml b/deploy/crd/kube-bind.io_apiserviceexports.yaml index 6b5a75827..f1a8d6c33 100644 --- a/deploy/crd/kube-bind.io_apiserviceexports.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexports.yaml @@ -108,6 +108,273 @@ spec: - kind - plural type: object + permissionClaims: + 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: + autoAdopt: + description: autoAdopt set to true means that objects created + by the consumer are adopted by the provider. i.e. the provider + will become the owner. Mutually exclusive with autoDonate. + type: boolean + autoDonate: + description: autoDonate set to true means that a newly created + object by the provider is immediately owned by the consumer. + If false, the object stays in ownership of the provider. Mutually + exclusive with autoDonate. + type: boolean + create: + description: create determines whether the kube-bind konnector + will sync matching objects from the provider cluster down + to the consumer cluster. only for owner Provider + properties: + replaceExisting: + description: "replaceExisting means that an existing object + owned by the consumer will be replaced by the provider + object. \n If not true, and a conflicting consumer object + exists, it is not touched." + type: boolean + type: object + 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 + onConflict: + description: onConflict determines how the conflicts between + objects on the consumer cluster will be resolved. + properties: + recreateWhenConsumerSideDeleted: + default: true + description: "recreateWhenConsumerSideDeleted set to true + (the default) means the provider will recreate the object + in case the object is missing on the consumer cluster, + but has been synchronized before. \n If set to false, + deleted provider-owned objects get deleted on the provider + cluster as well." + type: boolean + type: object + read: + description: read claims read access to matching objects for + the provider. Reading of the claimed object(s) is always claimed. + By default, no labels and annotations can be read by the provider. + Reading of labels and annotations can be claimed in addition + by specifying them explicitly. If labels on consumer owned + objects that are set by the consumer are read, labelsOnProviderOwnedObjects + and annotationsOnProviderOwnedObjects can be set. + properties: + annotations: + description: annotations is a list of claimed annotation + key wildcard patterns that are synchronized from the consumer + cluster to the provider on objects that are owned by the + consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labels: + description: labels is a list of claimed label key wildcard + patterns that are synchronized from the consumer cluster + to the provider on objects that are owned by the consumer. + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnProviderOwnedObjects: + description: labelsOnProviderOwnedObjects is a list of claimed + label key wildcard patterns that are synchronized from + the consumer cluster to the provider on objects owned + by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + overrideAnnotations: + description: overrideAnnotations is a list of claimed annotation + key wildcard patterns that are synchronized from the consumer + cluster to the provider on objects owned by the provider. + items: + properties: + pattern: + type: string + type: object + type: array + type: object + required: + description: required indicates whether the APIServiceBinding + will work if this claim is not accepted. If a required claim + is denied, the binding is aborted. + type: boolean + 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 selects which resources are being claimed. + If unset, all resources across all namespaces are being claimed. + properties: + fieldSelectors: + description: fieldSelectors is a list of field selectors + matching selected resources, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/. + items: + type: string + type: array + labelSelectors: + description: labelSelectors is a list of label selectors + matching selected resources. label selectors follow the + same rules as kubernetes label selectors, see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. + items: + additionalProperties: + type: string + type: object + type: array + names: + default: + - '*' + description: "names is a list of specific resource names + to select. Names matches the metadata.name field of the + underlying object. An entry of \"*\" anywhere in the list + means all object names of the group/resource within the + \"namespaces\" field are claimed. Wildcard entries other + than \"*\" and regular expressions are currently unsupported. + If a resources name matches any value in names, the resource + name is considered matching. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names or + * are allowed\"" + items: + type: string + type: array + namespaces: + default: + - '*' + description: "namespaces represents namespaces where an + object of the given group/resource may be managed. Namespaces + matches against the metadata.namespace field. A value + of \"*\" matches namespaced objects across all namespaces. + If a resources namespace matches any value in namespaces, + the resource namespace is considered matching. If the + claim is for a cluster-scoped resource, namespaces has + to explicitly be set to an empty array to prevent defaulting + to \"*\". If the \"names\" field is unset, all objects + of the group/resource within the listed namespaces (or + cluster) will be claimed. \n // +kubebuilder:validation:XValidation:rule=\"self.all(n, + n.matches('^[A-z-]+|[*]$'))\",message=\"only names or + * are allowed\"" + items: + type: string + type: array + owner: + description: owner matches the resource's owner. If an owner + selector is set, resources owned by other owners will + not be claimed. Resources without a present owner will + be considered, if configured owner could be the owner + of the object. For example, if the consumer creates a + resource that is claimed by the provider for reading. + In this case the resource will be marked as owned by the + consumer, and handled as such in further reconciliations. + An unset owner selector means objects from both sides + are considered. + enum: + - Provider + - Consumer + type: string + type: object + update: + description: update lists which updates to objects on the consumer + cluster are claimed. By default, the whole object is synced, + but metadata is not. + properties: + alwaysRecreate: + description: "alwaysRecreate, when true will delete the + old object and create new ones instead of updating. Useful + for immutable objects. \n This does not apply to metadata + field updates." + type: boolean + annotations: + description: "annotations is a list of claimed annotation + keys or annotation wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the provider. \n By default, no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + annotationsOnConsumerOwnedObjects: + description: "annotationsOnConsumerOwnedObjects is a list + of claimed annotation key wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the consumer. \n By default, no annotations are synced." + items: + properties: + pattern: + type: string + type: object + type: array + fields: + description: "fields are a list of JSON Paths describing + which parts of an object the provider wants to control. + \n This field is ignored if the owner in the claim selector + is set to \"Provider\"." + items: + type: string + type: array + labels: + description: "labels is a list of claimed label keys or + label wildcard patterns that are synchronized from the + provider to the consumer for objects owned by the provider. + \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + labelsOnConsumerOwnedObjects: + description: "labelsOnConsumerOwnedObjects is a list of + claimed label key wildcard patterns that are synchronized + from the provider to the consumer for objects owned by + the consumer. \n By default, no labels are synced." + items: + properties: + pattern: + type: string + type: object + type: array + preserving: + description: "preserving is a list of JSON Paths describing + which parts of an object owned by the provider the consumer + keeps controlling. \n This field is ignored if the owner + in the claim selector is set to \"Consumer\"." + items: + type: string + type: array + type: object + version: + description: version is the version of the claimed resource. + minLength: 1 + type: string + required: + - resource + - version + type: object + x-kubernetes-validations: + - message: donate and adopt are mutually exclusive + rule: '!(has(self.autoDonate) && self.autoDonate && has(self.autoAdopt) + && self.autoAdopt)' + type: array scope: description: scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`. diff --git a/hack/update-codegen-clients.sh b/hack/update-codegen-clients.sh index ae33d3520..50f01a258 100755 --- a/hack/update-codegen-clients.sh +++ b/hack/update-codegen-clients.sh @@ -30,3 +30,10 @@ bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \ --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate/boilerplate.generatego.txt \ --output-base "${SCRIPT_ROOT}" \ --trim-path-prefix github.com/kube-bind/kube-bind + +bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \ + github.com/kube-bind/kube-bind/contrib/example-backend/client github.com/kube-bind/kube-bind/contrib/example-backend/apis \ + "examplebackend:v1alpha1" \ + --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate/boilerplate.generatego.txt \ + --output-base "${SCRIPT_ROOT}" \ + --trim-path-prefix github.com/kube-bind/kube-bind diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 783472d67..0a852adc3 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -36,6 +36,17 @@ cd pkg/apis output:crd:artifacts:config=../../deploy/crd cd - +# Update generated CRD YAML +cd contrib/example-backend/apis +../../../${CONTROLLER_GEN} \ + crd \ + rbac:roleName=manager-role \ + webhook \ + paths="./..." \ + output:crd:artifacts:config=../../../contrib/deploy/crd +cd - + + cd deploy/crd for CRD in *.yaml; do if [ -f "../patches/${CRD}-patch" ]; then diff --git a/pkg/apis/kubebind/v1alpha1/apiservicebinding_types.go b/pkg/apis/kubebind/v1alpha1/apiservicebinding_types.go index 4a664a7af..cc306946a 100644 --- a/pkg/apis/kubebind/v1alpha1/apiservicebinding_types.go +++ b/pkg/apis/kubebind/v1alpha1/apiservicebinding_types.go @@ -87,8 +87,261 @@ type APIServiceBindingSpec struct { // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="kubeconfigSecretRef is immutable" KubeconfigSecretRef ClusterSecretKeyRef `json:"kubeconfigSecretRef"` + + // permissionClaims records decisions about permission claims requested by the API service provider. + // Individual claims can be accepted or rejected. If accepted, the API service provider gets the + // requested access to the specified resources in this workspace. Access is granted per + // GroupResource and other properties like selectors. + // + // +optional + PermissionClaims []AcceptablePermissionClaim `json:"permissionClaims,omitempty"` +} + +// acceptablePermissionClaim is a permission claim that stores the users acceptance in the field state. Only accepted permission claims are reconciled. +type AcceptablePermissionClaim struct { + PermissionClaim `json:",inline"` + + // state indicates if the claim is accepted or rejected. + // + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=Accepted;Rejected + State AcceptablePermissionClaimState `json:"state"` +} + +type AcceptablePermissionClaimState string + +const ( + ClaimAccepted AcceptablePermissionClaimState = "Accepted" + ClaimRejected AcceptablePermissionClaimState = "Rejected" +) + +// 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. +// +// +kubebuilder:validation:XValidation:rule="!(has(self.autoDonate) && self.autoDonate && has(self.autoAdopt) && self.autoAdopt)",message="donate and adopt are mutually exclusive" +type PermissionClaim struct { + GroupResource `json:","` + + // version is the version of the claimed resource. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength:=1 + Version string `json:"version"` + + // selector selects which resources are being claimed. + // If unset, all resources across all namespaces are being claimed. + // + // +optional + // +kubebuilder:default:={} + Selector *ResourceSelector `json:"selector,omitempty"` + + // required indicates whether the APIServiceBinding will work if this claim is not accepted. If a required claim is denied, the binding is aborted. + Required bool `json:"required"` + + // read claims read access to matching objects for the provider. + // Reading of the claimed object(s) is always claimed. + // By default, no labels and annotations can be read by the provider. + // Reading of labels and annotations can be claimed in addition by specifying them explicitly. + // If labels on consumer owned objects that are set by the consumer are read, labelsOnProviderOwnedObjects and + // annotationsOnProviderOwnedObjects can be set. + // + // +optional + // +kubebuilder:default={} + Read *ReadOptions `json:"read,omitempty"` + + // create determines whether the kube-bind konnector will sync matching objects from the + // provider cluster down to the consumer cluster. + // only for owner Provider + // + // +optional + Create *CreateOptions `json:"create,omitempty"` + + // autoAdopt set to true means that objects created by the consumer are adopted by the provider. + // i.e. the provider will become the owner. + // Mutually exclusive with autoDonate. + // + // +optional + AutoAdopt bool `json:"autoAdopt,omitempty"` + + // autoDonate set to true means that a newly created object by the provider is immediately owned by the consumer. + // If false, the object stays in ownership of the provider. + // Mutually exclusive with autoDonate. + // + // +optional + AutoDonate bool `json:"autoDonate,omitempty"` + + // onConflict determines how the conflicts between objects on the consumer cluster will be resolved. + // + // +optional + // +kubebuilder:default:={} + OnConflict *OnConflictOptions `json:"onConflict,omitempty"` + + // update lists which updates to objects on the consumer cluster are claimed. + // By default, the whole object is synced, but metadata is not. + // + // +optional + Update *UpdateOptions `json:"update,omitempty"` +} + +type ReadOptions struct { + // labels is a list of claimed label key wildcard patterns + // that are synchronized from the consumer cluster to the provider on + // objects that are owned by the consumer. + // + // +optional + Labels []Matcher `json:"labels,omitempty"` + + // labelsOnProviderOwnedObjects is a list of claimed label key wildcard + // patterns that are synchronized from the consumer cluster + // to the provider on objects owned by the provider. + // + // +optional + LabelsOnProviderOwnedObjects []Matcher `json:"labelsOnProviderOwnedObjects,omitempty"` + + // annotations is a list of claimed annotation key wildcard patterns + // that are synchronized from the consumer cluster to the provider on + // objects that are owned by the consumer. + // + // +optional + Annotations []Matcher `json:"annotations,omitempty"` + + // overrideAnnotations is a list of claimed annotation key wildcard + // patterns that are synchronized from the consumer cluster + // to the provider on objects owned by the provider. + // + // +optional + OverrideAnnotations []Matcher `json:"overrideAnnotations,omitempty"` +} + +type Matcher struct { + // +optional + Pattern string `json:"pattern,omitempty"` } +type OnConflictOptions struct { + // recreateWhenConsumerSideDeleted set to true (the default) means the provider will recreate the object + // in case the object is missing on the consumer cluster, but has been synchronized before. + // + // If set to false, deleted provider-owned objects get deleted on the provider cluster as well. + // + // +kubebuilder:default:=true + RecreateWhenConsumerSideDeleted bool `json:"recreateWhenConsumerSideDeleted"` +} + +type CreateOptions struct { + // replaceExisting means that an existing object owned by the consumer will be replaced by the provider object. + // + // If not true, and a conflicting consumer object exists, it is not touched. + // + // +optional + ReplaceExisting bool `json:"replaceExisting,omitempty"` +} + +type UpdateOptions struct { + // fields are a list of JSON Paths describing which parts of an object the provider wants to control. + // + // This field is ignored if the owner in the claim selector is set to "Provider". + // + // +optional + Fields []string `json:"fields,omitempty"` + + // preserving is a list of JSON Paths describing which parts of an object owned by the provider the consumer keeps controlling. + // + // This field is ignored if the owner in the claim selector is set to "Consumer". + // + // +optional + Preserving []string `json:"preserving,omitempty"` + + // alwaysRecreate, when true will delete the old object and create new ones + // instead of updating. Useful for immutable objects. + // + // This does not apply to metadata field updates. + // + // +optional + AlwaysRecreate bool `json:"alwaysRecreate,omitempty"` + + // labels is a list of claimed label keys or label wildcard patterns that are synchronized from the provider to the consumer for objects owned by the provider. + // + // By default, no labels are synced. + // + // +optional + Labels []Matcher `json:"labels,omitempty"` + + // labelsOnConsumerOwnedObjects is a list of claimed label key wildcard patterns that are synchronized from the provider to the consumer for objects owned by the consumer. + // + // By default, no labels are synced. + // + // +optional + LabelsOnConsumerOwnedObjects []Matcher `json:"labelsOnConsumerOwnedObjects,omitempty"` + + // annotations is a list of claimed annotation keys or annotation wildcard patterns that are synchronized from the provider to the consumer for objects owned by the provider. + // + // By default, no annotations are synced. + // + // +optional + Annotations []Matcher `json:"annotations,omitempty"` + + // annotationsOnConsumerOwnedObjects is a list of claimed annotation key wildcard patterns that are synchronized from the provider to the consumer for objects owned by the consumer. + // + // By default, no annotations are synced. + // + // +optional + AnnotationsOnConsumerOwnedObjects []Matcher `json:"annotationsOnConsumerOwnedObjects,omitempty"` +} + +type ResourceSelector struct { + // names is a list of specific resource names to select. + // Names matches the metadata.name field of the underlying object. + // An entry of "*" anywhere in the list means all object names of the group/resource within the "namespaces" field are claimed. + // Wildcard entries other than "*" and regular expressions are currently unsupported. + // If a resources name matches any value in names, the resource name is considered matching. + // + // // +kubebuilder:validation:XValidation:rule="self.all(n, n.matches('^[A-z-]+|[*]$'))",message="only names or * are allowed" + // +kubebuilder:default:={"*"} + // +optional + Names []string `json:"names,omitempty"` + + // namespaces represents namespaces where an object of the given group/resource may be managed. + // Namespaces matches against the metadata.namespace field. A value of "*" matches namespaced objects across all namespaces. + // If a resources namespace matches any value in namespaces, the resource namespace is considered matching. + // If the claim is for a cluster-scoped resource, namespaces has to explicitly be set to an empty array to prevent defaulting to "*". + // If the "names" field is unset, all objects of the group/resource within the listed namespaces (or cluster) will be claimed. + // + // // +kubebuilder:validation:XValidation:rule="self.all(n, n.matches('^[A-z-]+|[*]$'))",message="only names or * are allowed" + // +kubebuilder:default:={"*"} + // +optional + Namespaces []string `json:"namespaces,omitempty"` + + // labelSelectors is a list of label selectors matching selected resources. label selectors follow the same rules as kubernetes label selectors, + // see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. + LabelSelectors []map[string]string `json:"labelSelectors,omitempty"` + + // fieldSelectors is a list of field selectors matching selected resources, + // see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/. + FieldSelectors []string `json:"fieldSelectors,omitempty"` + + // owner matches the resource's owner. If an owner selector is set, resources owned by other owners will not be claimed. + // Resources without a present owner will be considered, if configured owner could be the owner of the object. + // For example, if the consumer creates a resource that is claimed by the provider for reading. In this case the resource + // will be marked as owned by the consumer, and handled as such in further reconciliations. + // An unset owner selector means objects from both sides are considered. + // + // +kubebuilder:validation:Enum=Provider;Consumer + // +optional + Owner Owner `json:"owner,omitempty"` +} + +type Owner string + +const ( + // provider means that the owner of the resource is the Provider. + Provider Owner = "Provider" + + // consumer means that the owner of the resource is the Consumer. + Consumer Owner = "Consumer" +) + type APIServiceBindingStatus struct { // providerPrettyName is the pretty name of the service provider cluster. This // can be shared among different APIServiceBindings. diff --git a/pkg/apis/kubebind/v1alpha1/apiserviceexport_types.go b/pkg/apis/kubebind/v1alpha1/apiserviceexport_types.go index a802ad167..74d226191 100644 --- a/pkg/apis/kubebind/v1alpha1/apiserviceexport_types.go +++ b/pkg/apis/kubebind/v1alpha1/apiserviceexport_types.go @@ -79,6 +79,9 @@ func (in *APIServiceExport) SetConditions(conditions conditionsapi.Conditions) { type APIServiceExportSpec struct { APIServiceExportCRDSpec `json:",inline"` + // +optional + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` + // informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. // // Cluster: The konnector has permission to watch all namespaces at once and cluster-scoped resources. diff --git a/pkg/apis/kubebind/v1alpha1/apiserviceexportrequest_types.go b/pkg/apis/kubebind/v1alpha1/apiserviceexportrequest_types.go index f5234bb05..a0d329825 100644 --- a/pkg/apis/kubebind/v1alpha1/apiserviceexportrequest_types.go +++ b/pkg/apis/kubebind/v1alpha1/apiserviceexportrequest_types.go @@ -110,6 +110,12 @@ type APIServiceExportRequestResource struct { // versions is a list of versions that should be exported. If this is empty // a sensible default is chosen by the service provider. Versions []string `json:"versions,omitempty"` + + // permissionClaims records decisions about permission claims requested by the service provider. + // Individual claims can be accepted or rejected. If accepted, the API service provider gets the + // requested access to the specified resources in this workspace. Access is granted per + // GroupResource, identity, and other properties. + PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` } // GroupResource identifies a resource. diff --git a/pkg/apis/kubebind/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kubebind/v1alpha1/zz_generated.deepcopy.go index f16e3d1d0..b1bb2ca50 100644 --- a/pkg/apis/kubebind/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kubebind/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ func (in *APIServiceBinding) DeepCopyInto(out *APIServiceBinding) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -93,6 +93,13 @@ func (in *APIServiceBindingList) DeepCopyObject() runtime.Object { func (in *APIServiceBindingSpec) DeepCopyInto(out *APIServiceBindingSpec) { *out = *in out.KubeconfigSecretRef = in.KubeconfigSecretRef + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]AcceptablePermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -284,6 +291,13 @@ func (in *APIServiceExportRequestResource) DeepCopyInto(out *APIServiceExportReq *out = make([]string, len(*in)) copy(*out, *in) } + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -397,6 +411,13 @@ func (in *APIServiceExportSchema) DeepCopy() *APIServiceExportSchema { func (in *APIServiceExportSpec) DeepCopyInto(out *APIServiceExportSpec) { *out = *in in.APIServiceExportCRDSpec.DeepCopyInto(&out.APIServiceExportCRDSpec) + if in.PermissionClaims != nil { + in, out := &in.PermissionClaims, &out.PermissionClaims + *out = make([]PermissionClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -560,6 +581,23 @@ func (in *APIServiceNamespaceStatus) DeepCopy() *APIServiceNamespaceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AcceptablePermissionClaim) DeepCopyInto(out *AcceptablePermissionClaim) { + *out = *in + in.PermissionClaim.DeepCopyInto(&out.PermissionClaim) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AcceptablePermissionClaim. +func (in *AcceptablePermissionClaim) DeepCopy() *AcceptablePermissionClaim { + if in == nil { + return nil + } + out := new(AcceptablePermissionClaim) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationMethod) DeepCopyInto(out *AuthenticationMethod) { *out = *in @@ -809,6 +847,22 @@ 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 *CreateOptions) DeepCopyInto(out *CreateOptions) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CreateOptions. +func (in *CreateOptions) DeepCopy() *CreateOptions { + if in == nil { + return nil + } + out := new(CreateOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GroupResource) DeepCopyInto(out *GroupResource) { *out = *in @@ -841,6 +895,22 @@ func (in *LocalSecretKeyRef) DeepCopy() *LocalSecretKeyRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Matcher) DeepCopyInto(out *Matcher) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Matcher. +func (in *Matcher) DeepCopy() *Matcher { + if in == nil { + return nil + } + out := new(Matcher) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NameObjectMeta) DeepCopyInto(out *NameObjectMeta) { *out = *in @@ -872,3 +942,187 @@ func (in *OAuth2CodeGrant) DeepCopy() *OAuth2CodeGrant { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnConflictOptions) DeepCopyInto(out *OnConflictOptions) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnConflictOptions. +func (in *OnConflictOptions) DeepCopy() *OnConflictOptions { + if in == nil { + return nil + } + out := new(OnConflictOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PermissionClaim) DeepCopyInto(out *PermissionClaim) { + *out = *in + out.GroupResource = in.GroupResource + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(ResourceSelector) + (*in).DeepCopyInto(*out) + } + if in.Read != nil { + in, out := &in.Read, &out.Read + *out = new(ReadOptions) + (*in).DeepCopyInto(*out) + } + if in.Create != nil { + in, out := &in.Create, &out.Create + *out = new(CreateOptions) + **out = **in + } + if in.OnConflict != nil { + in, out := &in.OnConflict, &out.OnConflict + *out = new(OnConflictOptions) + **out = **in + } + if in.Update != nil { + in, out := &in.Update, &out.Update + *out = new(UpdateOptions) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PermissionClaim. +func (in *PermissionClaim) DeepCopy() *PermissionClaim { + if in == nil { + return nil + } + out := new(PermissionClaim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReadOptions) DeepCopyInto(out *ReadOptions) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.LabelsOnProviderOwnedObjects != nil { + in, out := &in.LabelsOnProviderOwnedObjects, &out.LabelsOnProviderOwnedObjects + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.OverrideAnnotations != nil { + in, out := &in.OverrideAnnotations, &out.OverrideAnnotations + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadOptions. +func (in *ReadOptions) DeepCopy() *ReadOptions { + if in == nil { + return nil + } + out := new(ReadOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceSelector) DeepCopyInto(out *ResourceSelector) { + *out = *in + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LabelSelectors != nil { + in, out := &in.LabelSelectors, &out.LabelSelectors + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } + if in.FieldSelectors != nil { + in, out := &in.FieldSelectors, &out.FieldSelectors + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSelector. +func (in *ResourceSelector) DeepCopy() *ResourceSelector { + if in == nil { + return nil + } + out := new(ResourceSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpdateOptions) DeepCopyInto(out *UpdateOptions) { + *out = *in + if in.Fields != nil { + in, out := &in.Fields, &out.Fields + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Preserving != nil { + in, out := &in.Preserving, &out.Preserving + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.LabelsOnConsumerOwnedObjects != nil { + in, out := &in.LabelsOnConsumerOwnedObjects, &out.LabelsOnConsumerOwnedObjects + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.AnnotationsOnConsumerOwnedObjects != nil { + in, out := &in.AnnotationsOnConsumerOwnedObjects, &out.AnnotationsOnConsumerOwnedObjects + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateOptions. +func (in *UpdateOptions) DeepCopy() *UpdateOptions { + if in == nil { + return nil + } + out := new(UpdateOptions) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go new file mode 100644 index 000000000..5b948941f --- /dev/null +++ b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_controller.go @@ -0,0 +1,411 @@ +/* +Copyright 2022 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package claimedresources + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + dynamicclient "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamiclister" + "k8s.io/client-go/informers" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" + bindlisters "github.com/kube-bind/kube-bind/pkg/client/listers/kubebind/v1alpha1" + "github.com/kube-bind/kube-bind/pkg/indexers" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/multinsinformer" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic" +) + +const ( + controllerName = "kube-bind-konnector-claimed-object" +) + +// NewController returns a new controller reconciling downstream objects to upstream. +func NewController( + gvr schema.GroupVersionResource, + claim kubebindv1alpha1.PermissionClaim, + providerNamespace string, + consumerConfig, providerConfig *rest.Config, + consumerDynamicInformer informers.GenericInformer, + providerDynamicInformer multinsinformer.GetterInformer, + serviceNamespaceInformer dynamic.Informer[bindlisters.APIServiceNamespaceLister], +) (*controller, error) { + queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) + + logger := klog.Background().WithValues("controller", controllerName, "gvr", gvr) + + providerConfig = rest.CopyConfig(providerConfig) + providerConfig = rest.AddUserAgent(providerConfig, controllerName) + + providerClient, err := dynamicclient.NewForConfig(providerConfig) + if err != nil { + return nil, err + } + consumerClient, err := dynamicclient.NewForConfig(consumerConfig) + if err != nil { + return nil, err + } + + dynamicConsumerLister := dynamiclister.New(consumerDynamicInformer.Informer().GetIndexer(), gvr) + c := &controller{ + queue: queue, + + claim: claim, + + consumerClient: consumerClient, + providerClient: providerClient, + + consumerDynamicLister: dynamicConsumerLister, + consumerDynamicIndexer: consumerDynamicInformer.Informer().GetIndexer(), + + providerDynamicInformer: providerDynamicInformer, + + serviceNamespaceInformer: serviceNamespaceInformer, + + providerNamespace: providerNamespace, + + readReconciler: readReconciler{ + getServiceNamespace: func(upstreamNamespace string) (*kubebindv1alpha1.APIServiceNamespace, error) { + sns, err := serviceNamespaceInformer.Informer().GetIndexer().ByIndex(indexers.ServiceNamespaceByNamespace, upstreamNamespace) + if err != nil { + return nil, err + } + if len(sns) == 0 { + return nil, errors.NewNotFound(kubebindv1alpha1.SchemeGroupVersion.WithResource("APIServiceNamespace").GroupResource(), upstreamNamespace) + } + return sns[0].(*kubebindv1alpha1.APIServiceNamespace), nil + + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + return consumerClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + }, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + obj, err := providerDynamicInformer.Get(ns, name) + if err != nil { + return nil, err + } + return obj.(*unstructured.Unstructured), nil + }, + createProviderObject: func(ctx context.Context, obj *unstructured.Unstructured) error { + _, err := providerClient.Resource(gvr).Namespace(obj.GetNamespace()).Create(ctx, obj, metav1.CreateOptions{}) + return err + }, + updateProviderObject: func(ctx context.Context, obj *unstructured.Unstructured) error { + _, err := providerClient.Resource(gvr).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{}) + return err + }, + deleteProviderObject: func(ctx context.Context, ns, name string) error { + return providerClient.Resource(gvr).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}) + }, + deleteConsumerObject: func(ctx context.Context, ns, name string) error { + return consumerClient.Resource(gvr).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}) + }, + updateConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return consumerClient.Resource(gvr).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{}) + }, + createConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return consumerClient.Resource(gvr).Namespace(obj.GetNamespace()).Create(ctx, obj, metav1.CreateOptions{}) + }, + }, + } + + consumerDynamicInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueConsumer(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueConsumer(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueConsumer(logger, obj) + }, + }) + + providerDynamicInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueProvider(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueProvider(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueProvider(logger, obj) + }, + }) + + return c, nil +} + +// controller reconciles upstream objects to downstream. +type controller struct { + queue workqueue.RateLimitingInterface + + claim kubebindv1alpha1.PermissionClaim + + consumerClient dynamicclient.Interface + providerClient dynamicclient.Interface + + consumerDynamicLister dynamiclister.Lister + consumerDynamicIndexer cache.Indexer + + providerDynamicInformer multinsinformer.GetterInformer + + serviceNamespaceInformer dynamic.Informer[bindlisters.APIServiceNamespaceLister] + + providerNamespace string + + readReconciler +} + +func (c *controller) isClaimed(obj *unstructured.Unstructured) bool { + + var found string + for k, v := range obj.GetAnnotations() { + if k == annotation { + found = v + break + } + } + + annotationMatches := false + if c.claim.Selector == nil || c.claim.Selector.Owner == "" { + annotationMatches = true + } else { + annotationMatches = found == "" || found == string(c.claim.Selector.Owner) + } + + nameMatch := false + if c.claim.Selector == nil || len(c.claim.Selector.Names) == 0 { + nameMatch = true + } else { + for _, name := range c.claim.Selector.Names { + if name == obj.GetName() || name == "*" { + nameMatch = true + break + } + } + } + + // TODO namespace match + + return nameMatch && annotationMatches +} + +func (c *controller) enqueueConsumer(logger klog.Logger, obj interface{}) { + o := obj.(*unstructured.Unstructured) + if !c.isClaimed(o) { + return + } + + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + ns, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + runtime.HandleError(err) + return + } + + if ns != "" { + sn, err := c.serviceNamespaceInformer.Lister().APIServiceNamespaces(c.providerNamespace).Get(ns) + if err != nil { + if !errors.IsNotFound(err) { + runtime.HandleError(err) + } + return + } + if sn.Namespace == c.providerNamespace && sn.Status.Namespace != "" { + key := fmt.Sprintf("%s/%s", sn.Status.Namespace, name) + logger.V(2).Info("queueing Unstructured", "key", key) + c.queue.Add(key) + return + } + return + } + + logger.V(2).Info("queueing Unstructured", "key", key) + c.queue.Add(key) +} + +func (c *controller) enqueueProvider(logger klog.Logger, obj interface{}) { + upstreamKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + ns, name, err := cache.SplitMetaNamespaceKey(upstreamKey) + if err != nil { + runtime.HandleError(err) + return + } + + if ns != "" { + sns, err := c.serviceNamespaceInformer.Informer().GetIndexer().ByIndex(indexers.ServiceNamespaceByNamespace, ns) + if err != nil { + if !errors.IsNotFound(err) { + runtime.HandleError(err) + } + return + } + for _, obj := range sns { + sn := obj.(*kubebindv1alpha1.APIServiceNamespace) + if sn.Namespace == c.providerNamespace { + key := fmt.Sprintf("%s/%s", sn.Name, name) + logger.V(2).Info("queueing Unstructured", "key", key) + c.queue.Add(upstreamKey) + return + } + } + return + } + + logger.V(2).Info("queueing Unstructured", "key", upstreamKey) + c.queue.Add(upstreamKey) +} + +func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj interface{}) { + snKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + ns, name, err := cache.SplitMetaNamespaceKey(snKey) + if err != nil { + runtime.HandleError(err) + return + } + if ns != c.providerNamespace { + return // not for us + } + + sn, err := c.serviceNamespaceInformer.Lister().APIServiceNamespaces(ns).Get(name) + if err != nil { + logger.Error(err, "\n\ncould not list") + runtime.HandleError(err) + return + } + + if sn.Namespace == "" { + return // not ready + } + + logger.Info("enqueueing service namespace", "name", sn.Status.Namespace) + objs, err := c.providerDynamicInformer.List(sn.Status.Namespace) + if err != nil { + runtime.HandleError(err) + return + } + for _, obj := range objs { + + logger.Info("enqueueing provider object", "obj", obj) + + key, err := cache.MetaNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + continue + } + logger.V(2).Info("queueing Unstructured", "key", key, "reason", "APIServiceNamespace", "ServiceNamespaceKey", key) + c.queue.Add(key) + } +} + +// Start starts the controller, which stops when ctx.Done() is closed. +func (c *controller) Start(ctx context.Context, numThreads int) { + defer runtime.HandleCrash() + defer c.queue.ShutDown() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + c.serviceNamespaceInformer.Informer().AddDynamicEventHandler(ctx, controllerName, cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + c.enqueueServiceNamespace(logger, obj) + }, + UpdateFunc: func(_, newObj interface{}) { + c.enqueueServiceNamespace(logger, newObj) + }, + DeleteFunc: func(obj interface{}) { + c.enqueueServiceNamespace(logger, obj) + }, + }) + + for i := 0; i < numThreads; i++ { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *controller) startWorker(ctx context.Context) { + defer runtime.HandleCrash() + + for c.processNextWorkItem(ctx) { + } +} + +func (c *controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k.(string) + + logger := klog.FromContext(ctx).WithValues("key", key) + ctx = klog.NewContext(ctx, logger) + logger.V(2).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *controller) process(ctx context.Context, key string) error { + ns, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + runtime.HandleError(err) + return nil // we cannot do anything + } + + //logger := klog.FromContext(ctx) + + return c.reconcile(ctx, ns, name) +} diff --git a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler.go b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler.go new file mode 100644 index 000000000..642c791af --- /dev/null +++ b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler.go @@ -0,0 +1,245 @@ +/* +Copyright 2023 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 claimedresources + +import ( + "context" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog/v2" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" +) + +const annotation = "kube-bind.io/resource-owner" + +type readReconciler struct { + getServiceNamespace func(upstreamNamespace string) (*kubebindv1alpha1.APIServiceNamespace, error) + getProviderObject func(ns, name string) (*unstructured.Unstructured, error) + createProviderObject func(ctx context.Context, obj *unstructured.Unstructured) error + updateProviderObject func(ctx context.Context, obj *unstructured.Unstructured) error + deleteProviderObject func(ctx context.Context, ns, name string) error + + getConsumerObject func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) + updateConsumerObject func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) + createConsumerObject func(ctx context.Context, ob *unstructured.Unstructured) (*unstructured.Unstructured, error) + deleteConsumerObject func(ctx context.Context, ns, name string) error +} + +// reconcile syncs upstream claimed resources to downstream. +func (r *readReconciler) reconcile(ctx context.Context, providerNS, name string) error { + logger := klog.FromContext(ctx) + logger = logger.WithValues("name", name, "providerNamespace", providerNS) + + logger.Info("reconciling object") + consumerNS := "" + if providerNS != "" { + sn, err := r.getServiceNamespace(providerNS) + if err != nil && !errors.IsNotFound(err) { + return err + } else if errors.IsNotFound(err) { + runtime.HandleError(err) + return err // hoping the APIServiceNamespace will be created soon. Otherwise, this item goes into backoff. + } + if sn.Status.Namespace == "" { + runtime.HandleError(err) + return err // hoping the status is set soon. + } + + logger = logger.WithValues("providerNamespace", sn.Status.Namespace) + consumerNS = sn.Name + logger = logger.WithValues("consumerNamespace", consumerNS) + + ctx = klog.NewContext(ctx, logger) + } + + providerObj, providerErr := r.getProviderObject(providerNS, name) + if providerErr != nil && !errors.IsNotFound(providerErr) { + return providerErr + } + consumerObj, consumerErr := r.getConsumerObject(ctx, consumerNS, name) + if consumerErr != nil && !errors.IsNotFound(consumerErr) { + return consumerErr + } + + if errors.IsNotFound(providerErr) && errors.IsNotFound(consumerErr) { + // Nothing to do + return nil + } + + // Determine owner + owner, err := determineOwner(providerObj, consumerObj) + if err != nil { // nothing we can do + logger.Error(err, "could not determine owner") + return nil + } + logger = logger.WithValues("owner", owner) + + switch owner { + case kubebindv1alpha1.Provider: + if errors.IsNotFound(providerErr) { + err := r.deleteConsumerObject(ctx, consumerNS, name) + if errors.IsNotFound(err) { + return nil + } + return err + } + ownerCandidate := providerObj.DeepCopy() + + // Set owner annotation if needed + r.makeProviderOwner(ctx, ownerCandidate) + if !equality.Semantic.DeepEqual(providerObj, ownerCandidate) { + if err := r.updateProviderObject(ctx, ownerCandidate); err != nil { + return err + } + } + + if errors.IsNotFound(consumerErr) { + logger.Info("Creating missing downstream object", "downstreamNamespace", providerNS, "downstreamName", providerObj.GetName()) + + candidate := candidateFromOwnerObj(consumerNS, providerObj) + r.makeProviderOwner(ctx, candidate) + + if _, err := r.createConsumerObject(ctx, candidate); err != nil { + return err + } + + return nil + } + + if providerObj.GetDeletionTimestamp() != nil && !providerObj.GetDeletionTimestamp().IsZero() { + logger.Info("Deleting downstream object because it has been deleted upstream", "downStreamNamespace", providerNS, "downstreamName", providerObj.GetName()) + if err := r.deleteConsumerObject(ctx, providerNS, providerObj.GetName()); err != nil { + return err + } + } + + candidate := candidateFromOwnerObj(consumerNS, providerObj) + if !reflect.DeepEqual(candidate, consumerObj) { + logger.Info("Updating downstream object data", "downstreamNamespace", consumerNS, "downstreamName", consumerObj.GetName()) + if _, err := r.updateConsumerObject(ctx, candidate); err != nil { + logger.Error(err, "error updating consumer object") + return err + } + } + + case kubebindv1alpha1.Consumer: + if errors.IsNotFound(consumerErr) { + logger.Info("Owner copy of the object is gone, deleting downstream object", "name", name, "namespace", providerNS) + err := r.deleteProviderObject(ctx, providerNS, name) + if errors.IsNotFound(err) { + return nil + } + return err + } + + ownerCandidate := consumerObj.DeepCopy() + r.makeConsumerOwner(ownerCandidate) + if !equality.Semantic.DeepEqual(consumerObj, ownerCandidate) { + logger.Info("setting owner annotation for Consumer object") + if _, err := r.updateConsumerObject(ctx, ownerCandidate); err != nil { + return err + } + } + + candidate := candidateFromOwnerObj(providerNS, ownerCandidate) + r.makeConsumerOwner(candidate) + + if errors.IsNotFound(providerErr) { + logger.Info("creating consumer owned object at provider") + return r.createProviderObject(ctx, candidate) + } + + if !equality.Semantic.DeepEqual(providerObj, candidate) { + logger.Info("updating consumer owned object at provider") + return r.updateProviderObject(ctx, candidate) + } + } + + return nil +} + +func (r readReconciler) makeConsumerOwner(obj *unstructured.Unstructured) { + a := obj.GetAnnotations() + if a == nil { + a = map[string]string{} + } + a[annotation] = string(kubebindv1alpha1.Consumer) + obj.SetAnnotations(a) +} + +func (r readReconciler) makeProviderOwner(ctx context.Context, obj *unstructured.Unstructured) { + + a := obj.GetAnnotations() + if a == nil { + a = map[string]string{} + } + a[annotation] = string(kubebindv1alpha1.Provider) + obj.SetAnnotations(a) +} + +func candidateFromOwnerObj(downstreamNS string, obj *unstructured.Unstructured) *unstructured.Unstructured { + // clean up object + candidate := obj.DeepCopy() + candidate.SetUID("") + candidate.SetResourceVersion("") + candidate.SetNamespace(downstreamNS) + candidate.SetManagedFields(nil) + candidate.SetDeletionTimestamp(nil) + candidate.SetDeletionGracePeriodSeconds(nil) + candidate.SetOwnerReferences(nil) + candidate.SetFinalizers(nil) + candidate.SetNamespace(downstreamNS) + + return candidate +} + +// determineOwner determines the owner of a resource given at least one object exists either on the +// consumer or provider side +func determineOwner(providerObj, consumerObj *unstructured.Unstructured) (kubebindv1alpha1.Owner, error) { + if providerObj != nil { + ownerAnn := providerObj.GetAnnotations()[annotation] + switch ownerAnn { + case "Provider": + return kubebindv1alpha1.Provider, nil + case "Consumer": + return kubebindv1alpha1.Consumer, nil + } + if ownerAnn == "" && consumerObj == nil { + return kubebindv1alpha1.Provider, nil + } + } + + if consumerObj != nil { + ownerAnn := consumerObj.GetAnnotations()[annotation] + switch ownerAnn { + case "Provider": + return kubebindv1alpha1.Provider, nil + case "Consumer": + return kubebindv1alpha1.Consumer, nil + } + if ownerAnn == "" && providerObj == nil { + return kubebindv1alpha1.Consumer, nil + } + } + return "", fmt.Errorf("unable to determine owner") +} diff --git a/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler_test.go b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler_test.go new file mode 100644 index 000000000..6067d3792 --- /dev/null +++ b/pkg/konnector/controllers/cluster/claimedresources/claimedresources_reconciler_test.go @@ -0,0 +1,435 @@ +/* +Copyright 2023 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 claimedresources + +import ( + "context" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" +) + +func TestDownstreamCreation(t *testing.T) { + t.Parallel() + + var createdObj *unstructured.Unstructured + var providerObj *unstructured.Unstructured + + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "cluster-x-default", + }, + }, + }, nil + }, + getConsumerObject: notFound, + createConsumerObject: func(ctx context.Context, ob *unstructured.Unstructured) (*unstructured.Unstructured, error) { + createdObj = ob + return ob, nil + }, + updateProviderObject: func(ctx context.Context, obj *unstructured.Unstructured) error { + providerObj = obj + return nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if createdObj == nil { + t.Error("reconcile did not create an object", createdObj) + } + + if v, ok := createdObj.GetAnnotations()["kube-bind.io/resource-owner"]; !ok || v != "Provider" { + t.Error("created object did not have 'kube-bind.io/resource-owner: Provider' annotation", createdObj) + } + if v, ok := providerObj.GetAnnotations()["kube-bind.io/resource-owner"]; !ok || v != "Provider" { + t.Error("pre-existing object was not updated to be the owner") + } +} + +func TestUpstreamCreation(t *testing.T) { + t.Parallel() + + var createdObj *unstructured.Unstructured + var providerObj *unstructured.Unstructured + + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + return notFound(context.TODO(), ns, name) + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "cluster-x-default", + }, + }, + }, nil + }, + createProviderObject: func(ctx context.Context, ob *unstructured.Unstructured) error { + createdObj = ob + return nil + }, + updateConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + providerObj = obj + return obj, nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if createdObj == nil { + t.Error("reconcile did not create an object", createdObj) + } + + if v, ok := createdObj.GetAnnotations()["kube-bind.io/resource-owner"]; !ok || v != "Consumer" { + t.Error("created object did not have 'kube-bind.io/resource-owner: Consumer' annotation", createdObj) + } + if v, ok := providerObj.GetAnnotations()["kube-bind.io/resource-owner"]; !ok || v != "Consumer" { + t.Error("pre-existing object was not updated to be the owner") + } +} + +func defaultNamespace(upstreamNamespace string) (*v1alpha1.APIServiceNamespace, error) { + return &v1alpha1.APIServiceNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "kube-bind", + }, + Spec: v1alpha1.APIServiceNamespaceSpec{}, + Status: v1alpha1.APIServiceNamespaceStatus{ + Namespace: "cluster-x-default", + }, + }, nil +} + +func notFound(_ context.Context, ns, name string) (*unstructured.Unstructured, error) { + return nil, errors.NewNotFound(v1.Resource("Secret"), name) +} +func TestUpstreamDeletion(t *testing.T) { + t.Parallel() + + var deleteNsn struct { + ns, name string + } + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getConsumerObject: notFound, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "kube-bind.io/resource-owner": "Consumer", + }, + "name": "dummy", + "namespace": "default", + }, + }, + }, nil + }, + deleteProviderObject: func(ctx context.Context, ns, name string) error { + deleteNsn = struct { + ns string + name string + }{ns: ns, name: name} + return nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if deleteNsn.name != "dummy" || deleteNsn.ns != "cluster-x-default" { + t.Error("reconcile deleted the wrong object", deleteNsn) + } +} +func TestDownstreamDeletion(t *testing.T) { + t.Parallel() + + var deleteNsn struct { + ns, name string + } + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + return nil, errors.NewNotFound(v1.Resource("Secret"), name) + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "kube-bind.io/resource-owner": "Provider", + }, + "name": "dummy", + "namespace": "cluster-x-default", + }, + }, + }, nil + }, + deleteConsumerObject: func(ctx context.Context, ns, name string) error { + deleteNsn = struct { + ns string + name string + }{ns: ns, name: name} + return nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if deleteNsn.name != "dummy" || deleteNsn.ns != "default" { + t.Error("reconcile deleted the wrong object", deleteNsn) + } +} + +func TestDownstreamDeletionAlreadyGone(t *testing.T) { + t.Parallel() + + var deleteNsn struct { + ns, name string + } + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + return nil, errors.NewNotFound(v1.Resource("Secret"), name) + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "kube-bind.io/resource-owner": "Provider", + }, + "name": "dummy", + "namespace": "cluster-x-default", + }, + }, + }, nil + }, + deleteConsumerObject: func(ctx context.Context, ns, name string) error { + deleteNsn = struct { + ns string + name string + }{ns: ns, name: name} + return errors.NewNotFound(v1.Resource("Secret"), name) + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if deleteNsn.name != "dummy" || deleteNsn.ns != "default" { + t.Error("reconcile deleted the wrong object", deleteNsn) + } +} + +func TestDownstreamUpdate(t *testing.T) { + t.Parallel() + + var updateObj *unstructured.Unstructured + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetUnstructuredContent( + map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "cluster-x-default", + "annotations": map[string]interface{}{ + "kube-bind.io/resource-owner": "Consumer", + }, + }, + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + ) + return obj, nil + }, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetUnstructuredContent( + map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "default", + }, + "data": map[string]interface{}{ + "username": "user", + }, + }, + ) + return obj, nil + }, + updateProviderObject: func(ctx context.Context, obj *unstructured.Unstructured) error { + updateObj = obj + return nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if updateObj == nil { + t.Fatal("update object nil") + } + if v, ok := updateObj.GetAnnotations()["kube-bind.io/resource-owner"]; !ok || v != "Consumer" { + t.Error("updated object did not have 'kube-bind.io/resource-owner: Consumer' annotation") + } +} + +func TestUpdate(t *testing.T) { + t.Parallel() + + var updateObj *unstructured.Unstructured + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetUnstructuredContent( + map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "cluster-x-default", + "annotations": map[string]interface{}{ + "kube-bind.io/resource-owner": "Provider", + }, + }, + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + ) + return obj, nil + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetUnstructuredContent( + map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "default", + }, + "data": map[string]interface{}{ + "username": "user", + }, + }, + ) + return obj, nil + }, + updateConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + updateObj = obj + return obj, nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } + + if updateObj == nil { + t.Fatal("update object nil") + } + if v, ok := updateObj.GetAnnotations()["kube-bind.io/resource-owner"]; !ok || v != "Provider" { + t.Error("updated object did not have 'kube-bind.io/resource-owner: Provider' annotation") + } +} +func TestUpdateNotNeeded(t *testing.T) { + t.Parallel() + + r := readReconciler{ + getServiceNamespace: defaultNamespace, + getProviderObject: func(ns, name string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetUnstructuredContent( + map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "cluster-x-default", + "annotations": map[string]interface{}{"kube-bind.io/resource-owner": "Provider"}, + }, + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + ) + return obj, nil + }, + getConsumerObject: func(ctx context.Context, ns, name string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetUnstructuredContent( + map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "dummy", + "namespace": "default", + }, + "data": map[string]interface{}{ + "username": "user", + "password": "pass", + }, + }, + ) + obj.SetAnnotations(map[string]string{ + "kube-bind.io/resource-owner": "Provider", + }) + return obj, nil + }, + updateConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + t.Fatal("update function called although not needed", obj) + return nil, nil + }, + } + + err := r.reconcile(context.TODO(), "cluster-x-default", "dummy") + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go index e6c574754..fbc557cf9 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go @@ -36,6 +36,7 @@ import ( conditionsapi "github.com/kube-bind/kube-bind/pkg/apis/third_party/conditions/apis/conditions/v1alpha1" "github.com/kube-bind/kube-bind/pkg/apis/third_party/conditions/util/conditions" bindlisters "github.com/kube-bind/kube-bind/pkg/client/listers/kubebind/v1alpha1" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/claimedresources" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/multinsinformer" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/spec" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/status" @@ -209,6 +210,72 @@ func (r *reconciler) ensureControllers(ctx context.Context, name string, export return nil // nothing we can do here } + var claimControllers []func(context.Context, int) + for _, claim := range binding.Spec.PermissionClaims { + claim := claim + + if claim.State != kubebindv1alpha1.ClaimAccepted { + logger.Info("skipping non accepted claim", "claim", claim) + continue + } + + claimGVR := runtimeschema.GroupVersionResource{ + Group: claim.Group, + Version: claim.Version, + Resource: claim.Resource, + } + + var providerInf multinsinformer.GetterInformer + + syncClusterScoped := claim.Selector != nil && claim.Selector.Namespaces == nil + + if syncClusterScoped { + factory := dynamicinformer.NewDynamicSharedInformerFactory(dynamicProviderClient, time.Minute*30) + factory.ForResource(claimGVR).Lister() // wire the GVR up in the informer factory + providerInf = multinsinformer.GetterInformerWrapper{ + GVR: claimGVR, + Delegate: factory, + } + } else { + providerInf, err = multinsinformer.NewDynamicMultiNamespaceInformer( + claimGVR, + r.providerNamespace, + r.providerConfig, + r.serviceNamespaceInformer, + ) + if err != nil { + logger.Info("aborting", "error", err) + return err + } + } + claimedCtrl, err := claimedresources.NewController( + claimGVR, + claim.PermissionClaim, + r.providerNamespace, + r.consumerConfig, + r.providerConfig, + consumerInf.ForResource(claimGVR), + providerInf, + r.serviceNamespaceInformer, + ) + + if err != nil { + runtime.HandleError(err) + return nil //nothing we can do here + } + logger.Info("creating claim reconciler", "gvr", claimGVR) + + claimControllers = append(claimControllers, func(ctx context.Context, i int) { + providerInf.Start(ctx) + + providerSynced := providerInf.WaitForCacheSync(ctx.Done()) + logger.V(2).Info("Synced informers", "provider", providerSynced) + + claimedCtrl.Start(ctx, i) + }) + + } + ctx, cancel := context.WithCancel(ctx) consumerInf.Start(ctx.Done()) @@ -224,6 +291,10 @@ func (r *reconciler) ensureControllers(ctx context.Context, name string, export go specCtrl.Start(ctx, 1) go statusCtrl.Start(ctx, 1) + + for _, f := range claimControllers { + go f(ctx, 1) + } }() r.lock.Lock() diff --git a/pkg/kubectl/bind-apiservice/plugin/servicebindings.go b/pkg/kubectl/bind-apiservice/plugin/servicebindings.go index 431ed7f14..3c79a1c0e 100644 --- a/pkg/kubectl/bind-apiservice/plugin/servicebindings.go +++ b/pkg/kubectl/bind-apiservice/plugin/servicebindings.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kube Bind Authors. +Copyright 2023 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. @@ -17,8 +17,12 @@ limitations under the License. package plugin import ( + "bufio" + "bytes" "context" "fmt" + "io" + "strings" "time" apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" @@ -69,6 +73,26 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co continue } + var permissionClaims []kubebindv1alpha1.AcceptablePermissionClaim + for _, c := range resource.PermissionClaims { + accepted, err := b.promptYesNo(c) + if err != nil { + return nil, err + } + + var state kubebindv1alpha1.AcceptablePermissionClaimState + if accepted { + state = kubebindv1alpha1.ClaimAccepted + } else { + state = kubebindv1alpha1.ClaimRejected + } + + permissionClaims = append(permissionClaims, kubebindv1alpha1.AcceptablePermissionClaim{ + PermissionClaim: c, + State: state, + }) + } + // create new APIServiceBinding. first := true if err := wait.PollInfinite(1*time.Second, func() (bool, error) { @@ -89,6 +113,7 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co }, Namespace: "kube-bind", }, + PermissionClaims: permissionClaims, }, }, metav1.CreateOptions{}) if err != nil { @@ -115,3 +140,180 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co return bindings, nil } + +func printPermissionClaim(w io.Writer, p kubebindv1alpha1.PermissionClaim) error { + var b bytes.Buffer + + var groupResource string + if p.GroupResource.Group != "" { + groupResource = fmt.Sprintf("%s objects (apiVersion: \"%s/%s\")", p.GroupResource.Resource, p.GroupResource.Group, p.Version) + } else { + groupResource = fmt.Sprintf("%s objects (apiVersion: \"%s\")", p.GroupResource.Resource, p.Version) + } + + if err := writeFirstLines(&b, groupResource, p); err != nil { + return err + } + + if err := writeCreate(&b, p); err != nil { + return err + } + + if err := writeOnConflict(&b, p); err != nil { + return err + } + + if err := writeUpdateClause(&b, p); err != nil { + return err + } + + if err := writeRequiredAndAcceptance(&b, p.Required); err != nil { + return err + } + + _, err := fmt.Fprint(w, b.String()) + return err +} + +func writeFirstLines(b *bytes.Buffer, groupResource string, claim kubebindv1alpha1.PermissionClaim) error { + var err error + + donate := claim.AutoDonate + + adopt := claim.AutoAdopt + + var names []string + var owner kubebindv1alpha1.Owner + if claim.Selector != nil { + names = claim.Selector.Names + owner = claim.Selector.Owner + } + + var verb string + switch owner { + case kubebindv1alpha1.Provider: + verb = "write" + case kubebindv1alpha1.Consumer: + verb = "read" + default: + verb = "read and write" + } + + switch { + case !donate && !adopt: + groupResource = verb + " " + groupResource + case donate && !adopt: + groupResource = "create user owned " + groupResource + case !donate && adopt: + groupResource = "have ownership of " + groupResource + } + + var ref string + if len(names) > 0 { + ref = " which are referenced with:" + for _, name := range names { + ref = fmt.Sprintf("%s\n\t- name: \"%s\"", ref, name) + } + ref += "\n" + } else { + ref += " " + } + + _, err = fmt.Fprintf(b, "The provider wants to %s%son your cluster.\n", groupResource, ref) + + return err + +} + +func writeCreate(b io.StringWriter, claim kubebindv1alpha1.PermissionClaim) error { + var err error + + switch { + case claim.Create == nil || !claim.Create.ReplaceExisting: + //_, err = b.WriteString("Conflicting objects will not be overwritten. ") + case claim.Create.ReplaceExisting: + _, err = b.WriteString("Conflicting objects will be replaced by the provider. ") + } + + return err +} + +func writeOnConflict(b io.StringWriter, claim kubebindv1alpha1.PermissionClaim) error { + var err error + + switch { + case claim.OnConflict == nil || !claim.OnConflict.RecreateWhenConsumerSideDeleted: + //_, err = b.WriteString("Created objects will not be recreated upon deletion. ") + case claim.OnConflict.RecreateWhenConsumerSideDeleted: + _, err = b.WriteString("Created objects will be recreated upon deletion. ") + default: //Do nothing + } + + return err +} + +func writeUpdateClause(b *bytes.Buffer, claim kubebindv1alpha1.PermissionClaim) error { + var err error + + if claim.Update == nil { + return nil + } + + if claim.Update.Fields != nil { + _, err = fmt.Fprintf(b, "The following fields of the objects will still be able to be changed by the provider:\n") + } + if claim.Update.Preserving != nil { + _, err = b.WriteString("The following fields of the objects will be preserved by the provider:\n") + } + + for _, s := range append(claim.Update.Fields, claim.Update.Preserving...) { + _, err = fmt.Fprintf(b, "\t\"%s\"\n", s) + } + + if claim.Update.AlwaysRecreate { + _, err = b.WriteString("Modification of said objects will by handled by deletion and recreation of said objects.\n") + } + + return err +} + +func writeRequiredAndAcceptance(b *bytes.Buffer, required bool) error { + var err error + + if required { + _, err = fmt.Fprint(b, "Accepting this Permission is required in order to proceed.\n") + } + if !required { + _, err = fmt.Fprint(b, "Accepting this Permission is optional.\n") + } + if err != nil { + return nil + } + + _, err = fmt.Fprint(b, "Do you accept this Permission? [No,Yes]\n") + + return err +} + +func (opt BindAPIServiceOptions) promptYesNo(p kubebindv1alpha1.PermissionClaim) (bool, error) { + + reader := bufio.NewReader(opt.Options.IOStreams.In) + + for { + if err := printPermissionClaim(opt.Options.Out, p); err != nil { + return false, err + } + + response, err := reader.ReadString('\n') + if err != nil { + return false, err + } + + response = strings.ToLower(strings.TrimSpace(response)) + if response == "y" || response == "yes" { + return true, nil + } else if response == "n" || response == "no" { + return false, nil + } + } +} diff --git a/pkg/kubectl/bind-apiservice/plugin/servicebindings_test.go b/pkg/kubectl/bind-apiservice/plugin/servicebindings_test.go new file mode 100644 index 000000000..48384643b --- /dev/null +++ b/pkg/kubectl/bind-apiservice/plugin/servicebindings_test.go @@ -0,0 +1,824 @@ +/* +Copyright 2023 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 plugin + +import ( + "bytes" + "os" + "testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" +) + +func TestHumanReadablePromt(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + testData kubebindv1alpha1.PermissionClaim + expectedOutput string + }{ + {"Owner=Provider", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,Required=false", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: false, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is optional.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,Selector.Names={foo}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar"}, + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,GroupResource.Group", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "example.com", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + }, + "The provider wants to write foo objects (apiVersion: \"example.com/v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,Selector.Names={bar},GroupResource.Group", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "example.com", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar"}, + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + }, + "The provider wants to write foo objects (apiVersion: \"example.com/v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,CreateOptions={}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Create: &kubebindv1alpha1.CreateOptions{}, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,AutoDonate=false", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + AutoDonate: false, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,AutoDonate=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + AutoDonate: true, + }, + "The provider wants to create user owned foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,OnConflict={}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + OnConflict: &kubebindv1alpha1.OnConflictOptions{}, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,Create.ReplaceExisting=false", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Create: &kubebindv1alpha1.CreateOptions{ + ReplaceExisting: false, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,Create.ReplaceExisting=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Create: &kubebindv1alpha1.CreateOptions{ + ReplaceExisting: true, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Conflicting objects will be replaced by the provider. " + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,OnConflict.RecreateWhenConsumerSideDeleted=false", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + OnConflict: &kubebindv1alpha1.OnConflictOptions{ + RecreateWhenConsumerSideDeleted: false, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,OnConflict.RecreateWhenConsumerSideDeleted=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + OnConflict: &kubebindv1alpha1.OnConflictOptions{ + RecreateWhenConsumerSideDeleted: true, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Created objects will be recreated upon deletion. " + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,UpdateOptions={}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{}, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,UpdateOptions.Fields", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Fields: []string{"foo", "bar"}, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will still be able to be changed by the provider:\n" + // TODO + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,UpdateOptions.Preserving", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Preserving: []string{"foo", "bar"}, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will be preserved by the provider:\n" + + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,UpdateOptions.AlwaysRecreate=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{ + AlwaysRecreate: true, + }, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Modification of said objects will by handled by deletion and recreation of said objects.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,UpdateOptions.Fields,AutoDonate=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + AutoDonate: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Fields: []string{"foo", "bar"}, + }, + }, + "The provider wants to create user owned foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will still be able to be changed by the provider:\n" + + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,UpdateOptions.Preserving,AutoDonate=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + AutoDonate: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Preserving: []string{"foo", "bar"}, + }, + }, + "The provider wants to create user owned foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will be preserved by the provider:\n" + + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,Selector.Names={bar}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar"}, + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,GroupResource.Group", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "example.com", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + }, + "The provider wants to read foo objects (apiVersion: \"example.com/v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,Selector.Names={bar},GroupResource.Group", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "example.com", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar"}, + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + }, + "The provider wants to read foo objects (apiVersion: \"example.com/v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,Adopt=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + AutoAdopt: true, + Required: true, + }, + "The provider wants to have ownership of foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,Selector.Names={bar},Adopt=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar"}, + Owner: kubebindv1alpha1.Consumer, + }, + AutoAdopt: true, + Required: true, + }, + "The provider wants to have ownership of foo objects (apiVersion: \"v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,OnConflict={}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + OnConflict: &kubebindv1alpha1.OnConflictOptions{}, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,Create.ReplaceExisting=false", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + Create: &kubebindv1alpha1.CreateOptions{ + ReplaceExisting: false, + }, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,Create.ReplaceExisting=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + Create: &kubebindv1alpha1.CreateOptions{ + ReplaceExisting: true, + }, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Conflicting objects will be replaced by the provider. " + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,UpdateOptions={}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{}, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,UpdateOptions.Fields", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Fields: []string{"foo", "bar"}, + }, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will still be able to be changed by the provider:\n" + + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,UpdateOptions.Preserving", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Preserving: []string{"foo", "bar"}, + }, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will be preserved by the provider:\n" + + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,UpdateOptions.AlwaysRecreate=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + Update: &kubebindv1alpha1.UpdateOptions{ + AlwaysRecreate: true, + }, + }, + "The provider wants to read foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Modification of said objects will by handled by deletion and recreation of said objects.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,UpdateOptions.Fields,Adopt=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + AutoAdopt: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Fields: []string{"foo", "bar"}, + }, + }, + "The provider wants to have ownership of foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will still be able to be changed by the provider:\n" + + "\t\"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Consumer,UpdateOptions.Preserving,Adopt=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Owner: kubebindv1alpha1.Consumer, + }, + Required: true, + AutoAdopt: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Preserving: []string{"foo", "bar"}, + }, + }, + "The provider wants to have ownership of foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will be preserved by the provider:\n" + " \"foo\"\n" + + "\t\"bar\"\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Selector={}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{}, + Required: true, + }, + "The provider wants to read and write foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Selector.Owner=\"\",Selector.Names={bar}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar"}, + }, + Required: true, + }, + "The provider wants to read and write foo objects (apiVersion: \"v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Selector={},AutoDonate=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{}, + Required: true, + AutoDonate: true, + }, + "The provider wants to create user owned foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Selector={},AutoDonate=true,update.Fields=[\"spec\"]", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{}, + AutoDonate: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Fields: []string{"spec"}, + }, + }, + "The provider wants to create user owned foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will still be able to be changed by the provider:\n" + + "\t\"spec\"\n" + + "Accepting this Permission is optional.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Selector={},adopt=true", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{}, + Required: true, + AutoAdopt: true, + }, + "The provider wants to have ownership of foo objects (apiVersion: \"v1\") on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Selector={},adopt=true,update.Fields=[\"spec\"]", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{}, + AutoAdopt: true, + Update: &kubebindv1alpha1.UpdateOptions{ + Fields: []string{"spec"}, + }, + }, + "The provider wants to have ownership of foo objects (apiVersion: \"v1\") on your cluster.\n" + + "The following fields of the objects will still be able to be changed by the provider:\n" + + "\t\"spec\"\n" + + "Accepting this Permission is optional.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + {"Owner=Provider,Selector.Names={bar,baz}", + kubebindv1alpha1.PermissionClaim{ + GroupResource: kubebindv1alpha1.GroupResource{ + Group: "", + Resource: "foo", + }, + Version: "v1", + Selector: &kubebindv1alpha1.ResourceSelector{ + Names: []string{"bar", "baz"}, + Owner: kubebindv1alpha1.Provider, + }, + Required: true, + }, + "The provider wants to write foo objects (apiVersion: \"v1\") which are referenced with:\n" + + "\t- name: \"bar\"\n" + + "\t- name: \"baz\"\n" + + "on your cluster.\n" + + "Accepting this Permission is required in order to proceed.\n" + + "Do you accept this Permission? [No,Yes]\n", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var output bytes.Buffer + var input bytes.Buffer + input.WriteString("y\n") + opts := NewBindAPIServiceOptions(genericclioptions.IOStreams{In: &input, Out: &output, ErrOut: os.Stderr}) + b, err := opts.promptYesNo(tt.testData) + if output.String() != tt.expectedOutput { + t.Errorf("Expected IO Output did not match. got: \"\n%s\"\nwanted: \"\n%s\"\n", output.String(), tt.expectedOutput) + } + if b == false || (err != nil) { + t.Errorf("Expected Return value did not match. got: \"%v\", \"%v\"", b, err) + } + }) + } +} diff --git a/pkg/kubectl/bind/cmd/cmd.go b/pkg/kubectl/bind/cmd/cmd.go index e2776cc25..63333831d 100644 --- a/pkg/kubectl/bind/cmd/cmd.go +++ b/pkg/kubectl/bind/cmd/cmd.go @@ -64,6 +64,7 @@ func New(streams genericclioptions.IOStreams) (*cobra.Command, error) { return nil }, RunE: func(cmd *cobra.Command, args []string) error { + defer opts.Cleanup() if err := logsv1.ValidateAndApply(opts.Logs, nil); err != nil { return err } diff --git a/pkg/kubectl/bind/plugin/bind.go b/pkg/kubectl/bind/plugin/bind.go index d5bb90d30..84f35bc8a 100644 --- a/pkg/kubectl/bind/plugin/bind.go +++ b/pkg/kubectl/bind/plugin/bind.go @@ -17,7 +17,6 @@ limitations under the License. package plugin import ( - "bytes" "context" "crypto/sha256" "encoding/json" @@ -70,11 +69,16 @@ type BindOptions struct { // Runner is runs the command. It can be replaced in tests. Runner func(cmd *exec.Cmd) error - flags *pflag.FlagSet + flags *pflag.FlagSet + outFile *os.File } // NewBindOptions returns new BindOptions. func NewBindOptions(streams genericclioptions.IOStreams) *BindOptions { + f, err := os.CreateTemp("", "*.yaml") + if err != nil { + panic(err) + } opts := &BindOptions{ Options: base.NewOptions(streams), Logs: logs.NewOptions(), @@ -83,6 +87,7 @@ func NewBindOptions(streams genericclioptions.IOStreams) *BindOptions { Runner: func(cmd *exec.Cmd) error { return cmd.Run() }, + outFile: f, } return opts @@ -271,11 +276,17 @@ func (b *BindOptions) Run(ctx context.Context, urlCh chan<- string) error { return err } + f := b.outFile + _, err = f.Write(bs) + if err != nil { + return err + } + args := []string{ "apiservice", "--remote-kubeconfig-namespace", secret.Namespace, "--remote-kubeconfig-name", secret.Name, - "-f", "-", + "-f", f.Name(), } b.flags.VisitAll(func(flag *pflag.Flag) { if flag.Changed && PassOnFlags.Has(flag.Name) { @@ -292,7 +303,7 @@ func (b *BindOptions) Run(ctx context.Context, urlCh chan<- string) error { fmt.Fprintf(b.Options.ErrOut, "🚀 Executing: %s %s\n", "kubectl bind", strings.Join(args, " ")) // nolint: errcheck fmt.Fprintf(b.Options.ErrOut, "✨ Use \"-o yaml\" and \"--dry-run\" to get the APIServiceExportRequest.\n and pass it to \"kubectl bind apiservice\" directly. Great for automation.\n") command := exec.CommandContext(ctx, executable, append(args, "--no-banner")...) - command.Stdin = bytes.NewReader(bs) + command.Stdin = b.Options.IOStreams.In command.Stdout = b.Options.Out command.Stderr = b.Options.ErrOut if err := b.Runner(command); err != nil { @@ -303,6 +314,10 @@ func (b *BindOptions) Run(ctx context.Context, urlCh chan<- string) error { return nil } +func (opts *BindOptions) Cleanup() { + os.Remove(opts.outFile.Name()) +} + func ClusterID(ns *corev1.Namespace) string { hash := sha256.Sum224([]byte(ns.UID)) base62hash := toBase62(hash) diff --git a/test/e2e/bind/fixtures/consumer/bootstrap.go b/test/e2e/bind/fixtures/consumer/bootstrap.go new file mode 100644 index 000000000..a263d0caf --- /dev/null +++ b/test/e2e/bind/fixtures/consumer/bootstrap.go @@ -0,0 +1,42 @@ +/* +Copyright 2023 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 consumer + +import ( + "context" + "embed" + "testing" + + "github.com/stretchr/testify/require" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + + "github.com/kube-bind/kube-bind/pkg/bootstrap" +) + +//go:embed *.yaml +var raw embed.FS + +func Bootstrap(t *testing.T, discoveryClient discovery.DiscoveryInterface, dynamicClient dynamic.Interface, batteriesIncluded sets.String) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + err := bootstrap.Bootstrap(ctx, discoveryClient, dynamicClient, batteriesIncluded, raw) + require.NoError(t, err) +} diff --git a/test/e2e/bind/fixtures/consumer/crd-mangodb.yaml b/test/e2e/bind/fixtures/consumer/crd-mangodb.yaml new file mode 100644 index 000000000..0ec795060 --- /dev/null +++ b/test/e2e/bind/fixtures/consumer/crd-mangodb.yaml @@ -0,0 +1,58 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: mangodbs.mangodb.com + labels: + kube-bind.io/exported: "true" +spec: + group: mangodb.com + names: + kind: MangoDB + listKind: MangoDBList + plural: mangodbs + singular: mangodb + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + tier: + type: string + enum: + - Dedicated + - Shared + default: Shared + region: + type: string + default: us-east-1 + minLength: 1 + backup: + type: boolean + default: false + tokenSecret: + type: string + minLength: 1 + required: + - tokenSecret + status: + type: object + properties: + phase: + type: string + enum: + - Pending + - Running + - Succeeded + - Failed + - Unknown + required: + - spec diff --git a/test/e2e/bind/fixtures/consumer/exporttemplate-mangodb.yaml b/test/e2e/bind/fixtures/consumer/exporttemplate-mangodb.yaml new file mode 100644 index 000000000..fd1b3de84 --- /dev/null +++ b/test/e2e/bind/fixtures/consumer/exporttemplate-mangodb.yaml @@ -0,0 +1,17 @@ +kind: APIServiceExportTemplate +apiVersion: example-backend.kube-bind.io/v1alpha1 +metadata: + name: "mangodbs" + namespace: default +spec: + APIServiceSelector: + resource: mangodbs + group: mangodb.com + permissionClaims: + - group: "" + resource: secrets + version: v1 + selector: + owner: Consumer + namespaces: + - "*" diff --git a/test/e2e/bind/fixtures/provider/exporttemplate-mangodb.yaml b/test/e2e/bind/fixtures/provider/exporttemplate-mangodb.yaml new file mode 100644 index 000000000..ee8fa9f04 --- /dev/null +++ b/test/e2e/bind/fixtures/provider/exporttemplate-mangodb.yaml @@ -0,0 +1,17 @@ +kind: APIServiceExportTemplate +apiVersion: example-backend.kube-bind.io/v1alpha1 +metadata: + name: "mangodbs" + namespace: default +spec: + APIServiceSelector: + resource: mangodbs + group: mangodb.com + permissionClaims: + - group: "" + resource: secrets + version: v1 + selector: + owner: Provider + namespaces: + - "*" diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index 4a220912a..acccc83a4 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -17,8 +17,10 @@ limitations under the License. package bind import ( + "bytes" "context" "fmt" + "reflect" "strings" "testing" "time" @@ -26,6 +28,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/headzoo/surf.v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -59,12 +62,12 @@ func testHappyCase(t *testing.T, scope kubebindv1alpha1.Scope) { t.Logf("Creating provider workspace") providerConfig, providerKubeconfig := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("test-happy-case-provider")) - t.Logf("Creating MangoDB CRD on provider side") - providerfixtures.Bootstrap(t, framework.DiscoveryClient(t, providerConfig), framework.DynamicClient(t, providerConfig), nil) - t.Logf("Starting backend with random port") addr, _ := framework.StartBackend(t, providerConfig, "--kubeconfig="+providerKubeconfig, "--listen-port=0", "--consumer-scope="+string(scope)) + t.Logf("Creating MangoDB CRD on provider side") + providerfixtures.Bootstrap(t, framework.DiscoveryClient(t, providerConfig), framework.DynamicClient(t, providerConfig), nil) + t.Logf("Creating consumer workspace and starting konnector") consumerConfig, consumerKubeconfig := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("test-happy-case-consumer")) framework.StartKonnector(t, consumerConfig, "--kubeconfig="+consumerKubeconfig) @@ -75,8 +78,10 @@ func testHappyCase(t *testing.T, scope kubebindv1alpha1.Scope) { providerClient := framework.DynamicClient(t, providerConfig).Resource( schema.GroupVersionResource{Group: "mangodb.com", Version: "v1alpha1", Resource: "mangodbs"}, ) - + providerKubeClient := framework.KubeClient(t, providerConfig) + consumerKubeClient := framework.KubeClient(t, consumerConfig) upstreamNS := "unknown" + downstreamNS := "unknown" for _, tc := range []struct { name string @@ -96,14 +101,15 @@ func testHappyCase(t *testing.T, scope kubebindv1alpha1.Scope) { { name: "MangoDB is bound", step: func(t *testing.T) { + in := bytes.NewBufferString("y\n") iostreams, _, _, _ := genericclioptions.NewTestIOStreams() authURLCh := make(chan string, 1) go simulateBrowser(t, authURLCh, "mangodbs") invocations := make(chan framework.SubCommandInvocation, 1) framework.Bind(t, iostreams, authURLCh, invocations, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector") inv := <-invocations - requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "-", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) - framework.BindAPIService(t, inv.Stdin, "", inv.Args...) + requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "*", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) + framework.BindAPIService(t, in, "", inv.Args...) t.Logf("Waiting for MangoDB CRD to be created on consumer side") crdClient := framework.ApiextensionsClient(t, consumerConfig).ApiextensionsV1().CustomResourceDefinitions() @@ -130,6 +136,17 @@ spec: return err == nil }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for MangoDB CRD to be created on consumer side") + t.Logf("Waiting for the MangoDB instance to be created on consumer side") + var consumerMangos *unstructured.UnstructuredList + require.Eventually(t, func() bool { + var err error + consumerMangos, err = consumerClient.List(ctx, metav1.ListOptions{}) + return err == nil && len(consumerMangos.Items) == 1 + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the MangoDB instance to be created on consumer side") + + // this is used everywhere further down + downstreamNS = consumerMangos.Items[0].GetNamespace() + t.Logf("Waiting for the MangoDB instance to be created on provider side") var mangos *unstructured.UnstructuredList require.Eventually(t, func() bool { @@ -154,6 +171,80 @@ spec: }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the MangoDB instance to be recreated upstream") }, }, + { + name: "claimed resource created upstream is created downstream", + step: func(t *testing.T) { + testSecret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: upstreamNS, + }, + Data: map[string][]byte{ + "test": []byte("dummy"), + }, + } + + _, err := providerKubeClient.CoreV1().Secrets(upstreamNS).Create(ctx, &testSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + require.Eventually(t, func() bool { + s, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return err == nil && reflect.DeepEqual(testSecret.Data, s.Data) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the claimed resource to be created downstream") + }, + }, + { + name: "claimed resource recreated downstream if created upstream", + step: func(t *testing.T) { + err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + require.NoError(t, err) + + require.Eventually(t, func() bool { + _, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the claimed resource to be created downstream") + }, + }, + { + name: "claimed resource updated upstream is updated downstream", + step: func(t *testing.T) { + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + obj, err := providerKubeClient.CoreV1().Secrets(upstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + require.NoError(t, err) + obj.Data["test"] = []byte("updated") + _, err = providerKubeClient.CoreV1().Secrets(upstreamNS).Update(ctx, obj, metav1.UpdateOptions{}) + return err + }) + require.NoError(t, err) + + require.Eventually(t, func() bool { + obj, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + require.NoError(t, err) + updatedValue, ok := obj.Data["test"] + if !ok { + return false + } + + return string(updatedValue) == "updated" + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for claimed secret to be updated downstream") + }, + }, + { + name: "claimed resources deleted by the provider are deleted downstream", + step: func(t *testing.T) { + err := providerKubeClient.CoreV1().Secrets(upstreamNS).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + require.NoError(t, err) + + require.Eventually(t, func() bool { + _, err := consumerKubeClient.CoreV1().Secrets(upstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return errors.IsNotFound(err) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for claimed secret to be deleted on consumer side") + }, + }, { name: "instance spec updated downstream is updated upstream", step: func(t *testing.T) { @@ -238,8 +329,8 @@ spec: invocations := make(chan framework.SubCommandInvocation, 1) framework.Bind(t, iostreams, authURLCh, invocations, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector") inv := <-invocations - requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "-", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) - framework.BindAPIService(t, inv.Stdin, "", inv.Args...) + requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "*", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) + framework.BindAPIService(t, bytes.NewBufferString("y\n"), "", inv.Args...) }, }, } { diff --git a/test/e2e/framework/backend.go b/test/e2e/framework/backend.go index d2cab8fcb..a85e29474 100644 --- a/test/e2e/framework/backend.go +++ b/test/e2e/framework/backend.go @@ -35,7 +35,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" + backendcrd "github.com/kube-bind/kube-bind/contrib/deploy/crd" backend "github.com/kube-bind/kube-bind/contrib/example-backend" + backendv1alpha1 "github.com/kube-bind/kube-bind/contrib/example-backend/apis/examplebackend/v1alpha1" "github.com/kube-bind/kube-bind/contrib/example-backend/options" "github.com/kube-bind/kube-bind/deploy/crd" kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" @@ -70,6 +72,12 @@ func StartBackendWithoutDefaultArgs(t *testing.T, clientConfig *rest.Config, arg ) require.NoError(t, err) + err = backendcrd.Create(ctx, + crdClient.ApiextensionsV1().CustomResourceDefinitions(), + metav1.GroupResource{Group: backendv1alpha1.GroupName, Resource: "apiserviceexporttemplates"}, + ) + require.NoError(t, err) + fs := pflag.NewFlagSet("example-backend", pflag.ContinueOnError) options := options.NewOptions() options.AddFlags(fs) diff --git a/test/e2e/framework/bind.go b/test/e2e/framework/bind.go index 647908fae..7aebfa55d 100644 --- a/test/e2e/framework/bind.go +++ b/test/e2e/framework/bind.go @@ -17,7 +17,6 @@ limitations under the License. package framework import ( - "bytes" "context" "io" "os" @@ -82,7 +81,7 @@ type SubCommandInvocation struct { Stdin []byte } -func BindAPIService(t *testing.T, Stdin []byte, positionalArg string, flags ...string) { +func BindAPIService(t *testing.T, Stdin io.Reader, positionalArg string, flags ...string) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -92,7 +91,7 @@ func BindAPIService(t *testing.T, Stdin []byte, positionalArg string, flags ...s } t.Logf("kubectl bind apiservice %s", strings.Join(args, " ")) - opts := bindapiserviceplugin.NewBindAPIServiceOptions(genericclioptions.IOStreams{In: bytes.NewReader(Stdin), Out: os.Stdout, ErrOut: os.Stderr}) + opts := bindapiserviceplugin.NewBindAPIServiceOptions(genericclioptions.IOStreams{In: Stdin, Out: os.Stdout, ErrOut: os.Stderr}) cmd := &cobra.Command{} opts.AddCmdFlags(cmd) err := cmd.Flags().Parse(flags) diff --git a/test/e2e/konnector/claimedresources_test.go b/test/e2e/konnector/claimedresources_test.go new file mode 100644 index 000000000..09c21aab8 --- /dev/null +++ b/test/e2e/konnector/claimedresources_test.go @@ -0,0 +1,543 @@ +/* +Copyright 2023 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 konnector + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gopkg.in/headzoo/surf.v1" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/yaml" + + kubebindv1alpha1 "github.com/kube-bind/kube-bind/pkg/apis/kubebind/v1alpha1" + consumerfixtures "github.com/kube-bind/kube-bind/test/e2e/bind/fixtures/consumer" + providerfixtures "github.com/kube-bind/kube-bind/test/e2e/bind/fixtures/provider" + "github.com/kube-bind/kube-bind/test/e2e/framework" +) + +func TestProviderOwned(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Logf("Creating provider workspace") + providerConfig, providerKubeconfig := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("test-claimed-resources-provider")) + + t.Logf("Starting backend with random port") + addr, _ := framework.StartBackend(t, providerConfig, "--kubeconfig="+providerKubeconfig, "--listen-port=0", "--consumer-scope="+string(kubebindv1alpha1.NamespacedScope)) + + t.Logf("Creating MangoDB CRD on provider side") + providerfixtures.Bootstrap(t, framework.DiscoveryClient(t, providerConfig), framework.DynamicClient(t, providerConfig), nil) + + t.Logf("Creating consumer workspace and starting konnector") + consumerConfig, consumerKubeconfig := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("test-claimed-resources-provider")) + framework.StartKonnector(t, consumerConfig, "--kubeconfig="+consumerKubeconfig) + + providerKubeClient := framework.KubeClient(t, providerConfig) + consumerKubeClient := framework.KubeClient(t, consumerConfig) + + consumerClient := framework.DynamicClient(t, consumerConfig).Resource( + schema.GroupVersionResource{Group: "mangodb.com", Version: "v1alpha1", Resource: "mangodbs"}, + ).Namespace("default") + providerClient := framework.DynamicClient(t, providerConfig).Resource( + schema.GroupVersionResource{Group: "mangodb.com", Version: "v1alpha1", Resource: "mangodbs"}, + ) + + upstreamNS := "unknown" + downstreamNS := "unknown" + + for _, tc := range []struct { + name string + step func(t *testing.T) + }{ + { + name: "MangoDB is bound dry run", + step: func(t *testing.T) { + iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams() + authURLDryRunCh := make(chan string, 1) + go simulateBrowser(t, authURLDryRunCh, "mangodbs") + framework.Bind(t, iostreams, authURLDryRunCh, nil, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector", "--dry-run") + _, err := yaml.YAMLToJSON(bufOut.Bytes()) + require.NoError(t, err) + }, + }, + { + name: "MangoDB is bound", + step: func(t *testing.T) { + in := bytes.NewBufferString("y\n") + iostreams, _, _, _ := genericclioptions.NewTestIOStreams() + authURLCh := make(chan string, 1) + go simulateBrowser(t, authURLCh, "mangodbs") + invocations := make(chan framework.SubCommandInvocation, 1) + framework.Bind(t, iostreams, authURLCh, invocations, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector") + inv := <-invocations + requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "*", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) + framework.BindAPIService(t, in, "", inv.Args...) + + t.Logf("Waiting for MangoDB CRD to be created on consumer side") + crdClient := framework.ApiextensionsClient(t, consumerConfig).ApiextensionsV1().CustomResourceDefinitions() + require.Eventually(t, func() bool { + _, err := crdClient.Get(ctx, "mangodbs.mangodb.com", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for MangoDB CRD to be created on consumer side") + }, + }, + { + name: "instances are synced", + step: func(t *testing.T) { + t.Logf("Trying to create MangoDB on consumer side") + + require.Eventually(t, func() bool { + _, err := consumerClient.Create(ctx, toUnstructured(t, ` +apiVersion: mangodb.com/v1alpha1 +kind: MangoDB +metadata: + name: test +spec: + tokenSecret: credentials +`), metav1.CreateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for MangoDB CRD to be created on consumer side") + + t.Logf("Waiting for the MangoDB instance to be created on consumer side") + var consumerMangos *unstructured.UnstructuredList + require.Eventually(t, func() bool { + var err error + consumerMangos, err = consumerClient.List(ctx, metav1.ListOptions{}) + return err == nil && len(consumerMangos.Items) == 1 + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the MangoDB instance to be created on consumer side") + + // this is used everywhere further down + downstreamNS = consumerMangos.Items[0].GetNamespace() + + t.Logf("Waiting for the MangoDB instance to be created on provider side") + var mangos *unstructured.UnstructuredList + require.Eventually(t, func() bool { + var err error + mangos, err = providerClient.List(ctx, metav1.ListOptions{}) + return err == nil && len(mangos.Items) == 1 + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the MangoDB instance to be created on provider side") + + // this is used everywhere further down + upstreamNS = mangos.Items[0].GetNamespace() + }, + }, + { + name: "secret creation at provider", + step: func(t *testing.T) { + testSecret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: upstreamNS, + }, + Data: map[string][]byte{ + "test": []byte("dummy"), + }, + } + + _, err := providerKubeClient.CoreV1().Secrets(upstreamNS).Create(ctx, &testSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + }, + }, + { + name: "secret is automatically created at consumer", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + _, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be created at consumer") + }, + }, + { + name: "secret updated at consumer is overwritten", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + s, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + s.Data["test"] = []byte("updated") + + _, err = consumerKubeClient.CoreV1().Secrets(downstreamNS).Update(ctx, s, metav1.UpdateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be updated at consumer") + + require.Eventually(t, func() bool { + s, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + if v, ok := s.Data["test"]; ok && equality.Semantic.DeepEqual(v, []byte("dummy")) { + return true + } + return false + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be overwritten at consumer") + }, + }, + { + name: "secret is automatically updated at consumer", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + s, err := providerKubeClient.CoreV1().Secrets(upstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + s.Data["test"] = []byte("updated") + + _, err = providerKubeClient.CoreV1().Secrets(upstreamNS).Update(ctx, s, metav1.UpdateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be updated at provider") + + require.Eventually(t, func() bool { + s, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + if v, ok := s.Data["test"]; ok && equality.Semantic.DeepEqual(v, []byte("updated")) { + return true + } + return false + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be updated at consumer") + }, + }, + { + name: "secret deleted at consumer is automatically recreated", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted at consumer") + + require.Eventually(t, func() bool { + _, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return !errors.IsNotFound(err) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be recreated at consumer") + }, + }, + { + name: "secret is automatically deleted at consumer", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + err := providerKubeClient.CoreV1().Secrets(upstreamNS).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted at provider") + + require.Eventually(t, func() bool { + _, err := consumerKubeClient.CoreV1().Secrets(downstreamNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return errors.IsNotFound(err) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted at consumer") + }, + }, + } { + tc.step(t) + } +} + +func TestConsumerOwned(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Logf("Creating provider workspace") + providerConfig, providerKubeconfig := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("test-claimed-resources-consumer")) + + t.Logf("Starting backend with random port") + addr, _ := framework.StartBackend(t, providerConfig, "--kubeconfig="+providerKubeconfig, "--listen-port=0", "--consumer-scope="+string(kubebindv1alpha1.NamespacedScope)) + + t.Logf("Creating MangoDB CRD on provider side") + consumerfixtures.Bootstrap(t, framework.DiscoveryClient(t, providerConfig), framework.DynamicClient(t, providerConfig), nil) + + t.Logf("Creating consumer workspace and starting konnector") + consumerConfig, consumerKubeconfig := framework.NewWorkspace(t, framework.ClientConfig(t), framework.WithGenerateName("test-claimed-resources-consumer")) + framework.StartKonnector(t, consumerConfig, "--kubeconfig="+consumerKubeconfig) + + providerKubeClient := framework.KubeClient(t, providerConfig) + consumerKubeClient := framework.KubeClient(t, consumerConfig) + + consumerClient := framework.DynamicClient(t, consumerConfig).Resource( + schema.GroupVersionResource{Group: "mangodb.com", Version: "v1alpha1", Resource: "mangodbs"}, + ).Namespace("default") + providerClient := framework.DynamicClient(t, providerConfig).Resource( + schema.GroupVersionResource{Group: "mangodb.com", Version: "v1alpha1", Resource: "mangodbs"}, + ) + + providerNS := "unknown" + consumerNS := "unknown" + + for _, tc := range []struct { + name string + step func(t *testing.T) + }{ + { + name: "MangoDB is bound dry run", + step: func(t *testing.T) { + iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams() + authURLDryRunCh := make(chan string, 1) + go simulateBrowser(t, authURLDryRunCh, "mangodbs") + framework.Bind(t, iostreams, authURLDryRunCh, nil, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector", "--dry-run") + _, err := yaml.YAMLToJSON(bufOut.Bytes()) + require.NoError(t, err) + }, + }, + { + name: "MangoDB is bound", + step: func(t *testing.T) { + in := bytes.NewBufferString("y\n") + iostreams, _, _, _ := genericclioptions.NewTestIOStreams() + authURLCh := make(chan string, 1) + go simulateBrowser(t, authURLCh, "mangodbs") + invocations := make(chan framework.SubCommandInvocation, 1) + framework.Bind(t, iostreams, authURLCh, invocations, fmt.Sprintf("http://%s/export", addr.String()), "--kubeconfig", consumerKubeconfig, "--skip-konnector") + inv := <-invocations + requireEqualSlicePattern(t, []string{"apiservice", "--remote-kubeconfig-namespace", "*", "--remote-kubeconfig-name", "*", "-f", "*", "--kubeconfig=" + consumerKubeconfig, "--skip-konnector=true", "--no-banner"}, inv.Args) + framework.BindAPIService(t, in, "", inv.Args...) + + t.Logf("Waiting for MangoDB CRD to be created on consumer side") + crdClient := framework.ApiextensionsClient(t, consumerConfig).ApiextensionsV1().CustomResourceDefinitions() + require.Eventually(t, func() bool { + _, err := crdClient.Get(ctx, "mangodbs.mangodb.com", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for MangoDB CRD to be created on consumer side") + }, + }, + { + name: "instances are synced", + step: func(t *testing.T) { + t.Logf("Trying to create MangoDB on consumer side") + + require.Eventually(t, func() bool { + _, err := consumerClient.Create(ctx, toUnstructured(t, ` +apiVersion: mangodb.com/v1alpha1 +kind: MangoDB +metadata: + name: test +spec: + tokenSecret: credentials +`), metav1.CreateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for MangoDB CRD to be created on consumer side") + + t.Logf("Waiting for the MangoDB instance to be created on consumer side") + var consumerMangos *unstructured.UnstructuredList + require.Eventually(t, func() bool { + var err error + consumerMangos, err = consumerClient.List(ctx, metav1.ListOptions{}) + return err == nil && len(consumerMangos.Items) == 1 + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the MangoDB instance to be created on consumer side") + + // this is used everywhere further down + consumerNS = consumerMangos.Items[0].GetNamespace() + + t.Logf("Waiting for the MangoDB instance to be created on provider side") + var mangos *unstructured.UnstructuredList + require.Eventually(t, func() bool { + var err error + mangos, err = providerClient.List(ctx, metav1.ListOptions{}) + return err == nil && len(mangos.Items) == 1 + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the MangoDB instance to be created on provider side") + + // this is used everywhere further down + providerNS = mangos.Items[0].GetNamespace() + }, + }, + { + name: "secret creation at consumer", + step: func(t *testing.T) { + testSecret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: consumerNS, + }, + Data: map[string][]byte{ + "test": []byte("dummy"), + }, + } + + _, err := consumerKubeClient.CoreV1().Secrets(consumerNS).Create(ctx, &testSecret, metav1.CreateOptions{}) + require.NoError(t, err) + }, + }, + { + name: "secret is automatically created at provider", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + _, err := providerKubeClient.CoreV1().Secrets(providerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be created on provider side") + }, + }, + { + name: "secret updated at provider is overwritten", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + s, err := providerKubeClient.CoreV1().Secrets(providerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + s.Data["test"] = []byte("updated") + + _, err = providerKubeClient.CoreV1().Secrets(providerNS).Update(ctx, s, metav1.UpdateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be updated at provider") + + require.Eventually(t, func() bool { + s, err := providerKubeClient.CoreV1().Secrets(providerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + if v, ok := s.Data["test"]; ok && equality.Semantic.DeepEqual(v, []byte("dummy")) { + return true + } + return false + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be overwritten at consumer") + }, + }, + { + name: "secret is automatically updated at provider", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + s, err := consumerKubeClient.CoreV1().Secrets(consumerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + s.Data["test"] = []byte("updated") + + _, err = consumerKubeClient.CoreV1().Secrets(consumerNS).Update(ctx, s, metav1.UpdateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be updated at consumer") + + require.Eventually(t, func() bool { + s, err := providerKubeClient.CoreV1().Secrets(providerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if err != nil { + return false + } + + if v, ok := s.Data["test"]; ok && equality.Semantic.DeepEqual(v, []byte("updated")) { + return true + } + return false + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be updated at provider") + + }, + }, + { + name: "secret deleted at provider is automatically recreated", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + err := providerKubeClient.CoreV1().Secrets(providerNS).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted at provider") + + require.Eventually(t, func() bool { + _, err := providerKubeClient.CoreV1().Secrets(providerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + return !errors.IsNotFound(err) + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be recreated at provider") + }, + }, + { + name: "secret is automatically deleted at provider", + step: func(t *testing.T) { + require.Eventually(t, func() bool { + err := consumerKubeClient.CoreV1().Secrets(consumerNS).Delete(ctx, "test-secret", metav1.DeleteOptions{}) + return err == nil + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted at consumer") + + require.Eventually(t, func() bool { + s, err := providerKubeClient.CoreV1().Secrets(providerNS).Get(ctx, "test-secret", metav1.GetOptions{}) + if errors.IsNotFound(err) { + return true + } else { + t.Logf("secret still exists: %+v", s) + return false + } + }, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for secret to be deleted at provider") + }, + }, + } { + tc.step(t) + } +} + +func simulateBrowser(t *testing.T, authURLCh chan string, resource string) { + browser := surf.NewBrowser() + authURL := <-authURLCh + + t.Logf("Browsing to auth URL: %s", authURL) + err := browser.Open(authURL) + require.NoError(t, err) + + t.Logf("Waiting for browser to be at /resources") + framework.BrowerEventuallyAtPath(t, browser, "/resources") + + t.Logf("Clicking %s", resource) + err = browser.Click("a." + resource) + require.NoError(t, err) + + t.Logf("Waiting for browser to be forwarded to client") + framework.BrowerEventuallyAtPath(t, browser, "/callback") +} + +func requireEqualSlicePattern(t *testing.T, pattern []string, slice []string) { + t.Helper() + + require.Equal(t, len(pattern), len(slice), "slice length doesn't match pattern length\n got: %s\nexpected: %s", strings.Join(slice, " "), strings.Join(pattern, " ")) + + for i, s := range slice { + if pattern[i] == "*" { + continue + } + require.Equal(t, pattern[i], s, "slice doesn't match pattern at index %d\n got: %s\nexpected: %s", i, strings.Join(slice, " "), strings.Join(pattern, " ")) + } +} + +func toUnstructured(t *testing.T, manifest string) *unstructured.Unstructured { + t.Helper() + + obj := map[string]interface{}{} + err := yaml.Unmarshal([]byte(manifest), &obj) + require.NoError(t, err) + + return &unstructured.Unstructured{Object: obj} +}