From 8b97d87d90e9298ee4722eaaa5f8e9f32181efec Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 5 Aug 2025 16:34:36 +0200 Subject: [PATCH] Refactor and implement TenantSecret Signed-off-by: Andrei Kvapil --- packages/apps/tenant/templates/tenant.yaml | 20 + .../system/cozystack-api/templates/rbac.yaml | 2 +- .../templates/tenantnamespaces-rbac.yaml | 26 + pkg/apis/core/v1alpha1/register.go | 4 +- .../{types.go => tenantnamespace_types.go} | 0 pkg/apis/core/v1alpha1/tenantsecret_types.go | 24 + .../core/v1alpha1/zz_generated.deepcopy.go | 81 ++++ pkg/apiserver/apiserver.go | 56 +-- pkg/generated/openapi/zz_generated.openapi.go | 120 +++++ pkg/registry/core/tenantnamespace/rest.go | 382 ++++++++------- pkg/registry/core/tenantsecret/rest.go | 456 ++++++++++++++++++ 11 files changed, 939 insertions(+), 232 deletions(-) create mode 100644 packages/system/cozystack-api/templates/tenantnamespaces-rbac.yaml rename pkg/apis/core/v1alpha1/{types.go => tenantnamespace_types.go} (100%) create mode 100644 pkg/apis/core/v1alpha1/tenantsecret_types.go create mode 100644 pkg/registry/core/tenantsecret/rest.go diff --git a/packages/apps/tenant/templates/tenant.yaml b/packages/apps/tenant/templates/tenant.yaml index b1724376..9904740d 100644 --- a/packages/apps/tenant/templates/tenant.yaml +++ b/packages/apps/tenant/templates/tenant.yaml @@ -39,6 +39,11 @@ rules: resources: - workloadmonitors verbs: ["get", "list", "watch"] +- apiGroups: + - core.cozystack.io + resources: + - tenantsecrets + verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -188,6 +193,11 @@ rules: resources: - workloadmonitors verbs: ["get", "list", "watch"] + - apiGroups: + - core.cozystack.io + resources: + - tenantsecrets + verbs: ["get", "list", "watch"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 @@ -279,6 +289,11 @@ rules: resources: - workloadmonitors verbs: ["get", "list", "watch"] + - apiGroups: + - core.cozystack.io + resources: + - tenantsecrets + verbs: ["get", "list", "watch"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 @@ -346,6 +361,11 @@ rules: resources: - workloadmonitors verbs: ["get", "list", "watch"] + - apiGroups: + - core.cozystack.io + resources: + - tenantsecrets + verbs: ["get", "list", "watch"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/packages/system/cozystack-api/templates/rbac.yaml b/packages/system/cozystack-api/templates/rbac.yaml index b6a3469b..3ab6c938 100644 --- a/packages/system/cozystack-api/templates/rbac.yaml +++ b/packages/system/cozystack-api/templates/rbac.yaml @@ -4,7 +4,7 @@ metadata: name: cozystack-api rules: - apiGroups: [""] - resources: ["namespaces"] + resources: ["namespaces", "secrets"] verbs: ["get", "watch", "list"] - apiGroups: ["admissionregistration.k8s.io"] resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations", "validatingadmissionpolicies", "validatingadmissionpolicybindings"] diff --git a/packages/system/cozystack-api/templates/tenantnamespaces-rbac.yaml b/packages/system/cozystack-api/templates/tenantnamespaces-rbac.yaml new file mode 100644 index 00000000..48cd3071 --- /dev/null +++ b/packages/system/cozystack-api/templates/tenantnamespaces-rbac.yaml @@ -0,0 +1,26 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: tenantnamespaces-read +rules: +- apiGroups: + - core.cozystack.io + resources: + - tenantnamespaces + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tenantnamespaces-read-authenticated +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tenantnamespaces-read +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:authenticated diff --git a/pkg/apis/core/v1alpha1/register.go b/pkg/apis/core/v1alpha1/register.go index a68d1624..1370b28e 100644 --- a/pkg/apis/core/v1alpha1/register.go +++ b/pkg/apis/core/v1alpha1/register.go @@ -57,7 +57,9 @@ func RegisterStaticTypes(scheme *runtime.Scheme) { scheme.AddKnownTypes(SchemeGroupVersion, &TenantNamespace{}, &TenantNamespaceList{}, + &TenantSecret{}, + &TenantSecretList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) - klog.V(1).Info("Registered static kind: TenantNamespace") + klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret") } diff --git a/pkg/apis/core/v1alpha1/types.go b/pkg/apis/core/v1alpha1/tenantnamespace_types.go similarity index 100% rename from pkg/apis/core/v1alpha1/types.go rename to pkg/apis/core/v1alpha1/tenantnamespace_types.go diff --git a/pkg/apis/core/v1alpha1/tenantsecret_types.go b/pkg/apis/core/v1alpha1/tenantsecret_types.go new file mode 100644 index 00000000..917e9cc2 --- /dev/null +++ b/pkg/apis/core/v1alpha1/tenantsecret_types.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type TenantSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Same semantics as core/v1 Secret. + Type string `json:"type,omitempty"` + Data map[string][]byte `json:"data,omitempty"` + StringData map[string]string `json:"stringData,omitempty"` // write-only hint +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type TenantSecretList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TenantSecret `json:"items"` +} diff --git a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go index b29bdcfe..0a84fde1 100644 --- a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -83,3 +83,84 @@ func (in *TenantNamespaceList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantSecret) DeepCopyInto(out *TenantSecret) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string][]byte, len(*in)) + for key, val := range *in { + var outVal []byte + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]byte, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.StringData != nil { + in, out := &in.StringData, &out.StringData + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecret. +func (in *TenantSecret) DeepCopy() *TenantSecret { + if in == nil { + return nil + } + out := new(TenantSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TenantSecret) 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 *TenantSecretList) DeepCopyInto(out *TenantSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TenantSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretList. +func (in *TenantSecretList) DeepCopy() *TenantSecretList { + if in == nil { + return nil + } + out := new(TenantSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TenantSecretList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 835787c3..9c627387 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -38,6 +38,7 @@ import ( cozyregistry "github.com/cozystack/cozystack/pkg/registry" applicationstorage "github.com/cozystack/cozystack/pkg/registry/apps/application" tenantnamespacestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantnamespace" + tenantsecretstorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecret" ) var ( @@ -130,51 +131,20 @@ func (c completedConfig) New() (*CozyServer, error) { return nil, fmt.Errorf("create kube clientset: %v", err) } - v1alpha1storage := map[string]rest.Storage{} - - // --- static, cluster-scoped resource --- - v1alpha1storage["tenantnamespaces"] = cozyregistry.RESTInPeace( - tenantnamespacestorage.NewREST( - dynamicClient, - clientset.AuthorizationV1(), - 20, - ), - ) - - // --- dynamically-configured, per-tenant resources --- - for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(dynamicClient, &resConfig) - v1alpha1storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) - } - - for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(dynamicClient, &resConfig) - v1alpha1storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) - } - // --- static, cluster-scoped resource for core group --- coreV1alpha1Storage := map[string]rest.Storage{} coreV1alpha1Storage["tenantnamespaces"] = cozyregistry.RESTInPeace( tenantnamespacestorage.NewREST( - dynamicClient, + clientset.CoreV1(), clientset.AuthorizationV1(), 20, ), ) - - // --- dynamically-configured, per-tenant resources for apps group --- - appsV1alpha1Storage := map[string]rest.Storage{} - for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(dynamicClient, &resConfig) - appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) - } - - // Register groups with separate storage maps - appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs) - appsApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = appsV1alpha1Storage - if err := s.GenericAPIServer.InstallAPIGroup(&appsApiGroupInfo); err != nil { - return nil, err - } + coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace( + tenantsecretstorage.NewREST( + clientset.CoreV1(), + ), + ) coreApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(core.GroupName, Scheme, metav1.ParameterCodec, Codecs) coreApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = coreV1alpha1Storage @@ -182,5 +152,17 @@ func (c completedConfig) New() (*CozyServer, error) { return nil, err } + // --- dynamically-configured, per-tenant resources --- + appsV1alpha1Storage := map[string]rest.Storage{} + for _, resConfig := range c.ResourceConfig.Resources { + storage := applicationstorage.NewREST(dynamicClient, &resConfig) + appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) + } + appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs) + appsApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = appsV1alpha1Storage + if err := s.GenericAPIServer.InstallAPIGroup(&appsApiGroupInfo); err != nil { + return nil, err + } + return s, nil } diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 5d214578..c85874e4 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -35,6 +35,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus": schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref), "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace": schema_pkg_apis_core_v1alpha1_TenantNamespace(ref), "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespaceList": schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref), + "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret": schema_pkg_apis_core_v1alpha1_TenantSecret(ref), + "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretList": schema_pkg_apis_core_v1alpha1_TenantSecretList(ref), "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionRequest": schema_pkg_apis_apiextensions_v1_ConversionRequest(ref), "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionResponse": schema_pkg_apis_apiextensions_v1_ConversionResponse(ref), "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionReview": schema_pkg_apis_apiextensions_v1_ConversionReview(ref), @@ -338,6 +340,124 @@ func schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref common.ReferenceCallb } } +func schema_pkg_apis_core_v1alpha1_TenantSecret(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Same semantics as core/v1 Secret.", + Type: []string{"string"}, + Format: "", + }, + }, + "data": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "byte", + }, + }, + }, + }, + }, + "stringData": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_core_v1alpha1_TenantSecretList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + func schema_pkg_apis_apiextensions_v1_ConversionRequest(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/registry/core/tenantnamespace/rest.go b/pkg/registry/core/tenantnamespace/rest.go index 1ca57422..3b18f3e3 100644 --- a/pkg/registry/core/tenantnamespace/rest.go +++ b/pkg/registry/core/tenantnamespace/rest.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// TenantNamespace registry: read-only view over core Namespaces whose names -// start with “tenant-”. +// TenantNamespace registry: read-only view over Namespaces whose names start +// with “tenant-”. package tenantnamespace @@ -13,58 +13,57 @@ import ( "sync" "time" - corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" authorizationv1 "k8s.io/api/authorization/v1" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - meta "k8s.io/apimachinery/pkg/api/meta" metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/client-go/dynamic" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/klog/v2" + + corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" ) const ( - coreNSGroup = "" - coreNSVersion = "v1" - coreNSRes = "namespaces" - prefix = "tenant-" - singularName = "tenantnamespace" + prefix = "tenant-" + singularName = "tenantnamespace" ) -// Verify interface conformance. +// ----------------------------------------------------------------------------- +// REST storage +// ----------------------------------------------------------------------------- + var ( _ rest.Lister = &REST{} _ rest.Getter = &REST{} - _ rest.Scoper = &REST{} _ rest.Watcher = &REST{} _ rest.TableConvertor = &REST{} - _ rest.Storage = &REST{} + _ rest.Scoper = &REST{} _ rest.SingularNameProvider = &REST{} ) -// REST provides read-only storage over Namespaces. type REST struct { - dynamic dynamic.Interface - authClient authorizationv1client.AuthorizationV1Interface // <-- NEW - maxWorkers int // <-- NEW + core corev1client.CoreV1Interface + authClient authorizationv1client.AuthorizationV1Interface + maxWorkers int gvr schema.GroupVersionResource } -func NewREST(dynamicClient dynamic.Interface, - authClient authorizationv1client.AuthorizationV1Interface, +func NewREST( + coreCli corev1client.CoreV1Interface, + authCli authorizationv1client.AuthorizationV1Interface, maxWorkers int, ) *REST { return &REST{ - dynamic: dynamicClient, - authClient: authClient, + core: coreCli, + authClient: authCli, maxWorkers: maxWorkers, gvr: schema.GroupVersionResource{ Group: corev1alpha1.GroupName, @@ -75,88 +74,74 @@ func NewREST(dynamicClient dynamic.Interface, } // ----------------------------------------------------------------------------- -// rest.Scoper +// Basic meta // ----------------------------------------------------------------------------- -func (r *REST) NamespaceScoped() bool { return false } - -// ----------------------------------------------------------------------------- -// Object & name helpers -// ----------------------------------------------------------------------------- - -func (r *REST) New() runtime.Object { return &corev1alpha1.TenantNamespace{} } -func (r *REST) NewList() runtime.Object { return &corev1alpha1.TenantNamespaceList{} } -func (r *REST) Kind() string { return "TenantNamespace" } +func (*REST) NamespaceScoped() bool { return false } +func (*REST) New() runtime.Object { return &corev1alpha1.TenantNamespace{} } +func (*REST) NewList() runtime.Object { + return &corev1alpha1.TenantNamespaceList{} +} +func (*REST) Kind() string { return "TenantNamespace" } func (r *REST) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind { return r.gvr.GroupVersion().WithKind("TenantNamespace") } -func (r *REST) GetSingularName() string { return singularName } +func (*REST) GetSingularName() string { return singularName } // ----------------------------------------------------------------------------- // Lister / Getter // ----------------------------------------------------------------------------- -func listCoreNamespaces(ctx context.Context, cli dynamic.Interface) (*unstructured.UnstructuredList, error) { - return cli.Resource(schema.GroupVersionResource{ - Group: coreNSGroup, - Version: coreNSVersion, - Resource: coreNSRes, - }).List(ctx, metav1.ListOptions{}) -} - -func (r *REST) List(ctx context.Context, _ *metainternal.ListOptions) (runtime.Object, error) { - nsList, err := listCoreNamespaces(ctx, r.dynamic) - if err != nil && apierrors.IsForbidden(err) { - nsList, err = listCoreNamespaces(ctx, r.dynamic) - if err != nil { - return nil, err - } - - } +func (r *REST) List( + ctx context.Context, + _ *metainternal.ListOptions, +) (runtime.Object, error) { + nsList, err := r.core.Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } - var tenantObjs []unstructured.Unstructured + var tenantNames []string for i := range nsList.Items { - if strings.HasPrefix(nsList.Items[i].GetName(), prefix) { - tenantObjs = append(tenantObjs, nsList.Items[i]) + if strings.HasPrefix(nsList.Items[i].Name, prefix) { + tenantNames = append(tenantNames, nsList.Items[i].Name) } } - allowed, err := r.filterAccessibleTenantNamespaces(ctx, tenantObjs) + allowed, err := r.filterAccessible(ctx, tenantNames) if err != nil { return nil, err } - return r.buildTenantNamespaceList(nsList.GetResourceVersion(), allowed), nil + + return r.makeList(nsList, allowed), nil } -func (r *REST) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) { +func (r *REST) Get( + ctx context.Context, + name string, + opts *metav1.GetOptions, +) (runtime.Object, error) { if !strings.HasPrefix(name, prefix) { return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) } - u, err := r.dynamic.Resource(schema.GroupVersionResource{ - Group: coreNSGroup, - Version: coreNSVersion, - Resource: coreNSRes, - }).Get(ctx, name, *opts) + ns, err := r.core.Namespaces().Get(ctx, name, *opts) if err != nil { return nil, err } return &corev1alpha1.TenantNamespace{ TypeMeta: metav1.TypeMeta{ - APIVersion: "core.cozystack.io/v1alpha1", + APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: "TenantNamespace", }, ObjectMeta: metav1.ObjectMeta{ - Name: u.GetName(), - UID: u.GetUID(), - ResourceVersion: u.GetResourceVersion(), - CreationTimestamp: u.GetCreationTimestamp(), - Labels: u.GetLabels(), - Annotations: u.GetAnnotations(), + Name: ns.Name, + UID: ns.UID, + ResourceVersion: ns.ResourceVersion, + CreationTimestamp: ns.CreationTimestamp, + Labels: ns.Labels, + Annotations: ns.Annotations, }, }, nil } @@ -166,27 +151,43 @@ func (r *REST) Get(ctx context.Context, name string, opts *metav1.GetOptions) (r // ----------------------------------------------------------------------------- func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { - nsWatch, err := r.dynamic.Resource(schema.GroupVersionResource{ - Group: coreNSGroup, - Version: coreNSVersion, - Resource: coreNSRes, - }).Watch(ctx, metav1.ListOptions{ - ResourceVersion: opts.ResourceVersion, + nsWatch, err := r.core.Namespaces().Watch(ctx, metav1.ListOptions{ Watch: true, + ResourceVersion: opts.ResourceVersion, }) if err != nil { return nil, err } - tenantWatch := watch.Filter(nsWatch, func(e watch.Event) (watch.Event, bool) { - acc, err := meta.Accessor(e.Object) - if err != nil { - return e, false - } - return e, strings.HasPrefix(acc.GetName(), prefix) - }) + events := make(chan watch.Event) + pw := watch.NewProxyWatcher(events) - return tenantWatch, nil + go func() { + defer pw.Stop() + for ev := range nsWatch.ResultChan() { + ns, ok := ev.Object.(*corev1.Namespace) + if !ok || !strings.HasPrefix(ns.Name, prefix) { + continue + } + out := &corev1alpha1.TenantNamespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1alpha1.SchemeGroupVersion.String(), + Kind: "TenantNamespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ns.Name, + UID: ns.UID, + ResourceVersion: ns.ResourceVersion, + CreationTimestamp: ns.CreationTimestamp, + Labels: ns.Labels, + Annotations: ns.Annotations, + }, + } + events <- watch.Event{Type: ev.Type, Object: out} + } + }() + + return pw, nil } // ----------------------------------------------------------------------------- @@ -195,18 +196,15 @@ func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) { now := time.Now() - build := func(o runtime.Object, name string, created time.Time) metav1.TableRow { + row := func(o *corev1alpha1.TenantNamespace) metav1.TableRow { return metav1.TableRow{ - Cells: []interface{}{name, duration.HumanDuration(now.Sub(created))}, + Cells: []interface{}{o.Name, duration.HumanDuration(now.Sub(o.CreationTimestamp.Time))}, Object: runtime.RawExtension{Object: o}, } } - table := &metav1.Table{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "meta.k8s.io/v1", - Kind: "Table", - }, + tbl := &metav1.Table{ + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"}, ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "NAME", Type: "string"}, {Name: "AGE", Type: "string"}, @@ -214,101 +212,91 @@ func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.O } switch v := obj.(type) { - case *corev1alpha1.TenantNamespaceList: for i := range v.Items { - ns := &v.Items[i] - table.Rows = append(table.Rows, build(ns, ns.Name, ns.CreationTimestamp.Time)) + tbl.Rows = append(tbl.Rows, row(&v.Items[i])) } - + tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion case *corev1alpha1.TenantNamespace: - table.Rows = append(table.Rows, build(v, v.Name, v.CreationTimestamp.Time)) - - case *unstructured.UnstructuredList: - for i := range v.Items { - it := &v.Items[i] - table.Rows = append(table.Rows, build(it, it.GetName(), it.GetCreationTimestamp().Time)) - } - - case *unstructured.Unstructured: - table.Rows = append(table.Rows, build(v, v.GetName(), v.GetCreationTimestamp().Time)) - + tbl.Rows = append(tbl.Rows, row(v)) + tbl.ListMeta.ResourceVersion = v.ResourceVersion default: - return nil, errNotAcceptable{ - resource: r.gvr.GroupResource(), - message: fmt.Sprintf("unexpected object type %T", obj), - } + return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)} + } + return tbl, nil +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +func (r *REST) makeList(src *corev1.NamespaceList, allowed []string) *corev1alpha1.TenantNamespaceList { + set := map[string]struct{}{} + for _, n := range allowed { + set[n] = struct{}{} } - return table, nil -} - -// ----------------------------------------------------------------------------- -// Destroy — satisfy rest.Storage; nothing to clean up. -// ----------------------------------------------------------------------------- - -func (r *REST) Destroy() {} - -// ----------------------------------------------------------------------------- -// Local “NotAcceptable” error helper. -// ----------------------------------------------------------------------------- - -type errNotAcceptable struct { - resource schema.GroupResource - message string -} - -func (e errNotAcceptable) Error() string { return e.message } - -func (e errNotAcceptable) Status() metav1.Status { - return metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusNotAcceptable, - Reason: metav1.StatusReason("NotAcceptable"), - Message: e.Error(), - } -} - -func (r *REST) buildTenantNamespaceList(rv string, names []string) *corev1alpha1.TenantNamespaceList { out := &corev1alpha1.TenantNamespaceList{ - TypeMeta: metav1.TypeMeta{APIVersion: "core.cozystack.io/v1alpha1", Kind: "TenantNamespaceList"}, - ListMeta: metav1.ListMeta{ResourceVersion: rv}, + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1alpha1.SchemeGroupVersion.String(), + Kind: "TenantNamespaceList", + }, + ListMeta: metav1.ListMeta{ResourceVersion: src.ResourceVersion}, } - for _, name := range names { + + for i := range src.Items { + ns := &src.Items[i] + if _, ok := set[ns.Name]; !ok { + continue + } out.Items = append(out.Items, corev1alpha1.TenantNamespace{ - TypeMeta: metav1.TypeMeta{APIVersion: "core.cozystack.io/v1alpha1", Kind: "TenantNamespace"}, - ObjectMeta: metav1.ObjectMeta{Name: name}, + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1alpha1.SchemeGroupVersion.String(), + Kind: "TenantNamespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ns.Name, + UID: ns.UID, + ResourceVersion: ns.ResourceVersion, + CreationTimestamp: ns.CreationTimestamp, + Labels: ns.Labels, + Annotations: ns.Annotations, + }, }) } return out } -func (r *REST) filterAccessibleTenantNamespaces( - ctx context.Context, all []unstructured.Unstructured, +func (r *REST) filterAccessible( + ctx context.Context, + names []string, ) ([]string, error) { - - var tenantNames []string - for i := range all { - name := all[i].GetName() - if strings.HasPrefix(name, prefix) { - tenantNames = append(tenantNames, name) - } + workers := int(math.Min(float64(r.maxWorkers), float64(len(names)))) + type job struct{ name string } + type res struct { + name string + allowed bool + err error } - - workers := int(math.Min(float64(r.maxWorkers), float64(len(tenantNames)))) - jobs := make(chan nsJob, workers) - out := make(chan nsJobRes, workers) + jobs := make(chan job, workers) + out := make(chan res, workers) var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) - go func() { r.sarWorker(ctx, jobs, out); wg.Done() }() + go func() { + defer wg.Done() + for j := range jobs { + ok, err := r.sar(ctx, j.name) + out <- res{j.name, ok, err} + } + }() } go func() { wg.Wait(); close(out) }() go func() { - for _, n := range tenantNames { - jobs <- nsJob{n} + for _, n := range names { + jobs <- job{n} } close(jobs) }() @@ -326,42 +314,50 @@ func (r *REST) filterAccessibleTenantNamespaces( return allowed, nil } -type nsJob struct { - name string -} +func (r *REST) sar(ctx context.Context, ns string) (bool, error) { + u, ok := request.UserFrom(ctx) + if !ok || u == nil { + return false, fmt.Errorf("user missing in context") + } -type nsJobRes struct { - name string - allowed bool - err error -} - -func (r *REST) sarWorker(ctx context.Context, jobs <-chan nsJob, res chan<- nsJobRes) { - for j := range jobs { - u, ok := request.UserFrom(ctx) - if !ok || u == nil { - res <- nsJobRes{j.name, false, fmt.Errorf("no user in context")} - continue - } - - sar := &authorizationv1.SubjectAccessReview{ - Spec: authorizationv1.SubjectAccessReviewSpec{ - User: u.GetName(), - Groups: u.GetGroups(), - ResourceAttributes: &authorizationv1.ResourceAttributes{ - Group: "cozystack.io", - Resource: "workloadmonitors", - Verb: "get", - Namespace: j.name, - }, + sar := &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + User: u.GetName(), + Groups: u.GetGroups(), + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Group: "cozystack.io", + Resource: "workloadmonitors", + Verb: "get", + Namespace: ns, }, - } + }, + } - reply, err := r.authClient.SubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) - if err != nil { - res <- nsJobRes{j.name, false, err} - continue - } - res <- nsJobRes{j.name, reply.Status.Allowed, nil} + rsp, err := r.authClient.SubjectAccessReviews(). + Create(ctx, sar, metav1.CreateOptions{}) + if err != nil { + return false, err + } + return rsp.Status.Allowed, nil +} + +// ----------------------------------------------------------------------------- +// Boiler-plate +// ----------------------------------------------------------------------------- + +func (*REST) Destroy() {} + +type notAcceptable struct { + resource schema.GroupResource + message string +} + +func (e notAcceptable) Error() string { return e.message } +func (e notAcceptable) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotAcceptable, + Reason: metav1.StatusReason("NotAcceptable"), + Message: e.message, } } diff --git a/pkg/registry/core/tenantsecret/rest.go b/pkg/registry/core/tenantsecret/rest.go new file mode 100644 index 00000000..c173e2d1 --- /dev/null +++ b/pkg/registry/core/tenantsecret/rest.go @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: Apache-2.0 +// TenantSecret registry – namespaced view over Secrets labelled +// “cozystack.io/ui=true”. Internal labels/annotations are hidden. + +package tenantsecret + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "sort" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + + corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" +) + +// ----------------------------------------------------------------------------- +// Constants & helpers +// ----------------------------------------------------------------------------- + +const ( + uiLabelKey = "cozystack.io/ui" + uiLabelValue = "true" + systemLabelPrefix = "internal.cozystack.io/" + systemAnnotPrefix = "internal.cozystack.io/" + singularName = "tenantsecret" + kindTenantSecret = "TenantSecret" + kindTenantSecretList = "TenantSecretList" +) + +func stripInternal(m map[string]string) map[string]string { + if m == nil { + return nil + } + out := make(map[string]string, len(m)) + for k, v := range m { + if k == uiLabelKey || + strings.HasPrefix(k, systemLabelPrefix) || + strings.HasPrefix(k, systemAnnotPrefix) { + continue + } + out[k] = v + } + return out +} + +func encodeStringData(sd map[string]string) map[string][]byte { + if len(sd) == 0 { + return nil + } + out := make(map[string][]byte, len(sd)) + for k, v := range sd { + out[k] = []byte(v) + } + return out +} + +func decodeStringData(d map[string][]byte) map[string]string { + if len(d) == 0 { + return nil + } + out := make(map[string]string, len(d)) + for k, v := range d { + out[k] = base64.StdEncoding.EncodeToString(v) + } + return out +} + +func secretToTenant(sec *corev1.Secret) *corev1alpha1.TenantSecret { + return &corev1alpha1.TenantSecret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1alpha1.SchemeGroupVersion.String(), + Kind: kindTenantSecret, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: sec.Name, + Namespace: sec.Namespace, + UID: sec.UID, + ResourceVersion: sec.ResourceVersion, + CreationTimestamp: sec.CreationTimestamp, + Labels: stripInternal(sec.Labels), + Annotations: stripInternal(sec.Annotations), + }, + Type: string(sec.Type), + Data: sec.Data, + StringData: decodeStringData(sec.Data), + } +} + +func tenantToSecret(ts *corev1alpha1.TenantSecret, cur *corev1.Secret) *corev1.Secret { + var out corev1.Secret + if cur != nil { + out = *cur.DeepCopy() + } + out.TypeMeta = metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"} + out.Name, out.Namespace = ts.Name, ts.Namespace + + if out.Labels == nil { + out.Labels = map[string]string{} + } + out.Labels[uiLabelKey] = uiLabelValue + for k, v := range ts.Labels { + out.Labels[k] = v + } + + if out.Annotations == nil { + out.Annotations = map[string]string{} + } + for k, v := range ts.Annotations { + out.Annotations[k] = v + } + + if len(ts.Data) != 0 { + out.Data = ts.Data + } else if len(ts.StringData) != 0 { + out.Data = encodeStringData(ts.StringData) + } + out.Type = corev1.SecretType(ts.Type) + return &out +} + +func nsFrom(ctx context.Context) (string, error) { + ns, ok := request.NamespaceFrom(ctx) + if !ok { + return "", apierrors.NewBadRequest("namespace required") + } + return ns, nil +} + +// ----------------------------------------------------------------------------- +// REST storage +// ----------------------------------------------------------------------------- + +var ( + _ rest.Creater = &REST{} + _ rest.Getter = &REST{} + _ rest.Lister = &REST{} + _ rest.Updater = &REST{} + _ rest.Patcher = &REST{} + _ rest.GracefulDeleter = &REST{} + _ rest.Watcher = &REST{} + _ rest.TableConvertor = &REST{} + _ rest.Scoper = &REST{} + _ rest.SingularNameProvider = &REST{} +) + +type REST struct { + core corev1client.CoreV1Interface + gvr schema.GroupVersionResource +} + +func NewREST(coreCli corev1client.CoreV1Interface) *REST { + return &REST{ + core: coreCli, + gvr: schema.GroupVersionResource{ + Group: corev1alpha1.GroupName, + Version: "v1alpha1", + Resource: "tenantsecrets", + }, + } +} + +// ----------------------------------------------------------------------------- +// Basic meta +// ----------------------------------------------------------------------------- + +func (*REST) NamespaceScoped() bool { return true } +func (*REST) New() runtime.Object { return &corev1alpha1.TenantSecret{} } +func (*REST) NewList() runtime.Object { + return &corev1alpha1.TenantSecretList{} +} +func (*REST) Kind() string { return kindTenantSecret } +func (r *REST) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind { + return r.gvr.GroupVersion().WithKind(kindTenantSecret) +} +func (*REST) GetSingularName() string { return singularName } + +// ----------------------------------------------------------------------------- +// CRUD +// ----------------------------------------------------------------------------- + +func (r *REST) Create( + ctx context.Context, + obj runtime.Object, + _ rest.ValidateObjectFunc, + opts *metav1.CreateOptions, +) (runtime.Object, error) { + in, ok := obj.(*corev1alpha1.TenantSecret) + if !ok { + return nil, fmt.Errorf("expected TenantSecret, got %T", obj) + } + + sec := tenantToSecret(in, nil) + out, err := r.core.Secrets(sec.Namespace).Create(ctx, sec, *opts) + if err != nil { + return nil, err + } + return secretToTenant(out), nil +} + +func (r *REST) Get( + ctx context.Context, + name string, + opts *metav1.GetOptions, +) (runtime.Object, error) { + ns, err := nsFrom(ctx) + if err != nil { + return nil, err + } + sec, err := r.core.Secrets(ns).Get(ctx, name, *opts) + if err != nil { + return nil, err + } + return secretToTenant(sec), nil +} + +func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtime.Object, error) { + ns, err := nsFrom(ctx) + if err != nil { + return nil, err + } + + ls := labels.NewSelector() + req, _ := labels.NewRequirement(uiLabelKey, selection.Equals, []string{uiLabelValue}) + ls = ls.Add(*req) + + if opts.LabelSelector != nil { + if reqs, _ := opts.LabelSelector.Requirements(); len(reqs) > 0 { + ls = ls.Add(reqs...) + } + } + + fieldSel := "" + if opts.FieldSelector != nil { + fieldSel = opts.FieldSelector.String() + } + + list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{ + LabelSelector: ls.String(), + FieldSelector: fieldSel, + }) + if err != nil { + return nil, err + } + + out := &corev1alpha1.TenantSecretList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1alpha1.SchemeGroupVersion.String(), + Kind: kindTenantSecretList, + }, + ListMeta: list.ListMeta, + } + + for i := range list.Items { + out.Items = append(out.Items, *secretToTenant(&list.Items[i])) + } + sort.Slice(out.Items, func(i, j int) bool { return out.Items[i].Name < out.Items[j].Name }) + return out, nil +} + +func (r *REST) Update( + ctx context.Context, + name string, + objInfo rest.UpdatedObjectInfo, + _ rest.ValidateObjectFunc, + _ rest.ValidateObjectUpdateFunc, + forceCreate bool, + opts *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + ns, err := nsFrom(ctx) + if err != nil { + return nil, false, err + } + + cur, err := r.core.Secrets(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return nil, false, err + } + + newObj, err := objInfo.UpdatedObject(ctx, nil) + if err != nil { + return nil, false, err + } + in := newObj.(*corev1alpha1.TenantSecret) + + newSec := tenantToSecret(in, cur) + if cur == nil { + if !forceCreate && err == nil { + return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) + } + out, err := r.core.Secrets(ns).Create(ctx, newSec, metav1.CreateOptions{}) + return secretToTenant(out), true, err + } + + newSec.ResourceVersion = cur.ResourceVersion + out, err := r.core.Secrets(ns).Update(ctx, newSec, *opts) + return secretToTenant(out), false, err +} + +func (r *REST) Delete( + ctx context.Context, + name string, + _ rest.ValidateObjectFunc, + opts *metav1.DeleteOptions, +) (runtime.Object, bool, error) { + ns, err := nsFrom(ctx) + if err != nil { + return nil, false, err + } + err = r.core.Secrets(ns).Delete(ctx, name, *opts) + return nil, err == nil, err +} + +func (r *REST) Patch( + ctx context.Context, + name string, + pt types.PatchType, + data []byte, + opts *metav1.PatchOptions, + subresources ...string, +) (runtime.Object, error) { + ns, err := nsFrom(ctx) + if err != nil { + return nil, err + } + + out, err := r.core.Secrets(ns). + Patch(ctx, name, pt, data, *opts, subresources...) + if err != nil { + return nil, err + } + + // Ensure UI label is preserved + if out.Labels[uiLabelKey] != uiLabelValue { + out.Labels[uiLabelKey] = uiLabelValue + out, _ = r.core.Secrets(ns).Update(ctx, out, metav1.UpdateOptions{}) + } + + return secretToTenant(out), nil +} + +// ----------------------------------------------------------------------------- +// Watcher +// ----------------------------------------------------------------------------- + +func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { + ns, err := nsFrom(ctx) + if err != nil { + return nil, err + } + + ls := labels.Set{uiLabelKey: uiLabelValue}.AsSelector().String() + base, err := r.core.Secrets(ns).Watch(ctx, metav1.ListOptions{ + Watch: true, + LabelSelector: ls, + ResourceVersion: opts.ResourceVersion, + }) + if err != nil { + return nil, err + } + + ch := make(chan watch.Event) + proxy := watch.NewProxyWatcher(ch) + + go func() { + defer proxy.Stop() + for ev := range base.ResultChan() { + sec, ok := ev.Object.(*corev1.Secret) + if !ok || sec == nil { + continue + } + tenant := secretToTenant(sec) + ch <- watch.Event{ + Type: ev.Type, + Object: tenant, + } + } + }() + + return proxy, nil +} + +// ----------------------------------------------------------------------------- +// TableConvertor +// ----------------------------------------------------------------------------- + +func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) { + now := time.Now() + row := func(o *corev1alpha1.TenantSecret) metav1.TableRow { + return metav1.TableRow{ + Cells: []interface{}{o.Name, o.Type, duration.HumanDuration(now.Sub(o.CreationTimestamp.Time))}, + Object: runtime.RawExtension{Object: o}, + } + } + + tbl := &metav1.Table{ + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"}, + ColumnDefinitions: []metav1.TableColumnDefinition{ + {Name: "NAME", Type: "string"}, + {Name: "TYPE", Type: "string"}, + {Name: "AGE", Type: "string"}, + }, + } + + switch v := obj.(type) { + case *corev1alpha1.TenantSecretList: + for i := range v.Items { + tbl.Rows = append(tbl.Rows, row(&v.Items[i])) + } + tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion + case *corev1alpha1.TenantSecret: + tbl.Rows = append(tbl.Rows, row(v)) + tbl.ListMeta.ResourceVersion = v.ResourceVersion + default: + return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)} + } + return tbl, nil +} + +// ----------------------------------------------------------------------------- +// Boiler-plate +// ----------------------------------------------------------------------------- + +func (*REST) Destroy() {} + +type notAcceptable struct { + resource schema.GroupResource + message string +} + +func (e notAcceptable) Error() string { return e.message } +func (e notAcceptable) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotAcceptable, + Reason: metav1.StatusReason("NotAcceptable"), + Message: e.message, + } +}