diff --git a/cmd/cozystack-api/main.go b/cmd/cozystack-api/main.go index b78edcb0..7fe0a1bb 100644 --- a/cmd/cozystack-api/main.go +++ b/cmd/cozystack-api/main.go @@ -26,8 +26,8 @@ import ( func main() { ctx := genericapiserver.SetupSignalContext() - options := server.NewAppsServerOptions(os.Stdout, os.Stderr) - cmd := server.NewCommandStartAppsServer(ctx, options) + options := server.NewCozyServerOptions(os.Stdout, os.Stderr) + cmd := server.NewCommandStartCozyServer(ctx, options) code := cli.Run(cmd) os.Exit(code) } 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/apiservice.yaml b/packages/system/cozystack-api/templates/apiservice.yaml index 5829561e..d0ab1185 100644 --- a/packages/system/cozystack-api/templates/apiservice.yaml +++ b/packages/system/cozystack-api/templates/apiservice.yaml @@ -11,3 +11,17 @@ spec: name: cozystack-api namespace: cozy-system version: v1alpha1 +--- +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + name: v1alpha1.core.cozystack.io +spec: + insecureSkipTLSVerify: true + group: core.cozystack.io + groupPriorityMinimum: 1000 + versionPriority: 15 + service: + name: cozystack-api + namespace: cozy-system + version: v1alpha1 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/apps/v1alpha1/register.go b/pkg/apis/apps/v1alpha1/register.go index a1b2586f..e112e1eb 100644 --- a/pkg/apis/apps/v1alpha1/register.go +++ b/pkg/apis/apps/v1alpha1/register.go @@ -1,18 +1,5 @@ -/* -Copyright 2024 The Cozystack 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. -*/ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 The Cozystack Authors. package v1alpha1 @@ -24,46 +11,50 @@ import ( "k8s.io/klog/v2" ) -// GroupName holds the API group name. +// ----------------------------------------------------------------------------- +// Group / version boiler-plate +// ----------------------------------------------------------------------------- + +// GroupName is the API group for every resource in this package. const GroupName = "apps.cozystack.io" -var ( - RegisteredGVKs []schema.GroupVersionKind -) - -// SchemeGroupVersion is group version used to register these objects +// SchemeGroupVersion is the canonical {group,version} for v1alpha1. var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} +// ----------------------------------------------------------------------------- +// Scheme registration helpers +// ----------------------------------------------------------------------------- + var ( - // SchemeBuilder allows to add this group to a scheme. - // TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api. - // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. + // SchemeBuilder is used by generated deepcopy code. SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder - - // AddToScheme adds this group to a scheme. - AddToScheme = localSchemeBuilder.AddToScheme + AddToScheme = localSchemeBuilder.AddToScheme ) func init() { - // We only register manually written functions here. The registration of the - // generated functions takes place in the generated files. The separation - // makes the code compile even when the generated files are missing. + // Manually-written types go here. Generated deepcopy code is wired in + // via `zz_generated.deepcopy.go`. localSchemeBuilder.Register(addKnownTypes) } -// Adds the list of known types to the given scheme. +// addKnownTypes is called from init(). func addKnownTypes(scheme *runtime.Scheme) error { metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } -// Resource takes an unqualified resource and returns a Group qualified GroupResource +// Resource turns an unqualified resource name into a fully-qualified one. func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } -// RegisterDynamicTypes registers types dynamically based on config +// ----------------------------------------------------------------------------- +// Public helpers consumed by the apiserver wiring +// ----------------------------------------------------------------------------- + +// RegisterDynamicTypes adds per-tenant “Application” kinds that are only known +// at runtime from a config file. func RegisterDynamicTypes(scheme *runtime.Scheme, cfg *config.ResourceConfig) error { for _, res := range cfg.Resources { kind := res.Application.Kind @@ -76,9 +67,7 @@ func RegisterDynamicTypes(scheme *runtime.Scheme, cfg *config.ResourceConfig) er scheme.AddKnownTypeWithName(gvkInternal, &Application{}) scheme.AddKnownTypeWithName(gvkInternal.GroupVersion().WithKind(kind+"List"), &ApplicationList{}) - klog.V(1).Infof("Registered kind: %s\n", kind) - RegisteredGVKs = append(RegisteredGVKs, gvk) + klog.V(1).Infof("Registered dynamic kind: %s", kind) } - return nil } diff --git a/pkg/apis/core/fuzzer/fuzzer.go b/pkg/apis/core/fuzzer/fuzzer.go new file mode 100644 index 00000000..dbf3ca39 --- /dev/null +++ b/pkg/apis/core/fuzzer/fuzzer.go @@ -0,0 +1,33 @@ +/* +Copyright 2024 The Cozystack 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 fuzzer + +import ( + "github.com/cozystack/cozystack/pkg/apis/core" + fuzz "github.com/google/gofuzz" + + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" +) + +// Funcs returns the fuzzer functions for the core api group. +var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + func(s *core.TenantNamespaceSpec, c fuzz.Continue) { + c.FuzzNoCustom(s) // fuzz self without calling this function again + }, + } +} diff --git a/pkg/apis/core/install/install.go b/pkg/apis/core/install/install.go new file mode 100644 index 00000000..0f210eb2 --- /dev/null +++ b/pkg/apis/core/install/install.go @@ -0,0 +1,29 @@ +/* +Copyright 2024 The Cozystack 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 install + +import ( + corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +// Install registers the API group and adds types to a scheme +func Install(scheme *runtime.Scheme) { + utilruntime.Must(corev1alpha1.AddToScheme(scheme)) + utilruntime.Must(scheme.SetVersionPriority(corev1alpha1.SchemeGroupVersion)) +} diff --git a/pkg/apis/core/install/roundtrip_test.go b/pkg/apis/core/install/roundtrip_test.go new file mode 100644 index 00000000..a11fa8d9 --- /dev/null +++ b/pkg/apis/core/install/roundtrip_test.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 The Cozystack 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 install + +import ( + "testing" + + corefuzzer "github.com/cozystack/cozystack/pkg/apis/core/fuzzer" + "k8s.io/apimachinery/pkg/api/apitesting/roundtrip" +) + +func TestRoundTripTypes(t *testing.T) { + roundtrip.RoundTripTestForAPIGroup(t, Install, corefuzzer.Funcs) + // TODO: enable protobuf generation for the sample-apiserver + // roundtrip.RoundTripProtobufTestForAPIGroup(t, Install, corefuzzer.Funcs) +} diff --git a/pkg/apis/core/register.go b/pkg/apis/core/register.go new file mode 100644 index 00000000..9739ffdb --- /dev/null +++ b/pkg/apis/core/register.go @@ -0,0 +1,22 @@ +/* +Copyright 2024 The Cozystack Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package core + +// GroupName is the group name used in this package +const ( + GroupName = "core.cozystack.io" +) diff --git a/pkg/apis/core/v1alpha1/doc.go b/pkg/apis/core/v1alpha1/doc.go new file mode 100644 index 00000000..15b059be --- /dev/null +++ b/pkg/apis/core/v1alpha1/doc.go @@ -0,0 +1,25 @@ +/* +Copyright 2024 The Cozystack 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. +*/ + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:conversion-gen=github.com/cozystack/cozystack/pkg/apis/core +// +k8s:conversion-gen=k8s.io/apiextensions-apiserver/pkg/apis/apiextensions +// +k8s:defaulter-gen=TypeMeta +// +groupName=core.cozystack.io + +// Package v1alpha1 is the v1alpha1 version of the API. +package v1alpha1 // import "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" diff --git a/pkg/apis/core/v1alpha1/register.go b/pkg/apis/core/v1alpha1/register.go new file mode 100644 index 00000000..1370b28e --- /dev/null +++ b/pkg/apis/core/v1alpha1/register.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 The Cozystack Authors. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" +) + +// ----------------------------------------------------------------------------- +// Group / version boiler-plate +// ----------------------------------------------------------------------------- + +// GroupName is the API group for every resource in this package. +const GroupName = "core.cozystack.io" + +// SchemeGroupVersion is the canonical {group,version} for v1alpha1. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// ----------------------------------------------------------------------------- +// Scheme registration helpers +// ----------------------------------------------------------------------------- + +var ( + // SchemeBuilder is used by generated deepcopy code. + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // Manually-written types go here. Generated deepcopy code is wired in + // via `zz_generated.deepcopy.go`. + localSchemeBuilder.Register(addKnownTypes) +} + +// addKnownTypes is called from init(). +func addKnownTypes(scheme *runtime.Scheme) error { + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource turns an unqualified resource name into a fully-qualified one. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// ----------------------------------------------------------------------------- +// Public helpers consumed by the apiserver wiring +// ----------------------------------------------------------------------------- + +// RegisterStaticTypes adds *compile-time* resources such as TenantNamespace. +func RegisterStaticTypes(scheme *runtime.Scheme) { + scheme.AddKnownTypes(SchemeGroupVersion, + &TenantNamespace{}, + &TenantNamespaceList{}, + &TenantSecret{}, + &TenantSecretList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret") +} diff --git a/pkg/apis/core/v1alpha1/tenantnamespace_types.go b/pkg/apis/core/v1alpha1/tenantnamespace_types.go new file mode 100644 index 00000000..506fb616 --- /dev/null +++ b/pkg/apis/core/v1alpha1/tenantnamespace_types.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 The Cozystack Authors. + +// This file contains the cluster-scoped “TenantNamespace” resource. +// A TenantNamespace represents an existing Kubernetes Namespace whose +// *name* starts with the prefix “tenant-”. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TenantNamespace is a thin wrapper around ObjectMeta. It has no spec/status +// because it merely reflects an existing Namespace object. +type TenantNamespace struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TenantNamespaceList is the list variant for TenantNamespace. +type TenantNamespaceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TenantNamespace `json:"items"` +} 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.conversion.go b/pkg/apis/core/v1alpha1/zz_generated.conversion.go new file mode 100644 index 00000000..3b3b068a --- /dev/null +++ b/pkg/apis/core/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,36 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2025 The Cozystack 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 conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + return nil +} diff --git a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..0a84fde1 --- /dev/null +++ b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,166 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2025 The Cozystack 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" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantNamespace) DeepCopyInto(out *TenantNamespace) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantNamespace. +func (in *TenantNamespace) DeepCopy() *TenantNamespace { + if in == nil { + return nil + } + out := new(TenantNamespace) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TenantNamespace) 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 *TenantNamespaceList) DeepCopyInto(out *TenantNamespaceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TenantNamespace, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantNamespaceList. +func (in *TenantNamespaceList) DeepCopy() *TenantNamespaceList { + if in == nil { + return nil + } + out := new(TenantNamespaceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TenantNamespaceList) 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 *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/apis/core/v1alpha1/zz_generated.defaults.go b/pkg/apis/core/v1alpha1/zz_generated.defaults.go new file mode 100644 index 00000000..4e70d879 --- /dev/null +++ b/pkg/apis/core/v1alpha1/zz_generated.defaults.go @@ -0,0 +1,33 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2025 The Cozystack 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 defaulter-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go new file mode 100644 index 00000000..060067de --- /dev/null +++ b/pkg/apis/core/validation/validation.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 The Cozystack 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 validation + +import ( + "github.com/cozystack/cozystack/pkg/apis/core" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateTenantNamespace validates a TenantNamespace. +func ValidateTenantNamespace(f *core.TenantNamespace) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, ValidateTenantNamespaceSpec(&f.Spec, field.NewPath("spec"))...) + + return allErrs +} + +// ValidateTenantNamespaceSpec validates a TenantNamespaceSpec. +func ValidateTenantNamespaceSpec(s *core.TenantNamespaceSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // TODO validation + + return allErrs +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 4e54d867..9c627387 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -27,13 +27,18 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" "github.com/cozystack/cozystack/pkg/apis/apps" - "github.com/cozystack/cozystack/pkg/apis/apps/install" + appsinstall "github.com/cozystack/cozystack/pkg/apis/apps/install" + coreinstall "github.com/cozystack/cozystack/pkg/apis/apps/install" + "github.com/cozystack/cozystack/pkg/apis/core" "github.com/cozystack/cozystack/pkg/config" - appsregistry "github.com/cozystack/cozystack/pkg/registry" + 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 ( @@ -42,11 +47,12 @@ var ( // Codecs provides methods for retrieving codecs and serializers for specific // versions and content types. Codecs = serializer.NewCodecFactory(Scheme) - AppsComponentName = "apps" + CozyComponentName = "cozy" ) func init() { - install.Install(Scheme) + appsinstall.Install(Scheme) + coreinstall.Install(Scheme) // Register HelmRelease types. if err := helmv2.AddToScheme(Scheme); err != nil { @@ -73,8 +79,8 @@ type Config struct { ResourceConfig *config.ResourceConfig } -// AppsServer holds the state for the Kubernetes master/api server. -type AppsServer struct { +// CozyServer holds the state for the Kubernetes master/api server. +type CozyServer struct { GenericAPIServer *genericapiserver.GenericAPIServer } @@ -98,19 +104,17 @@ func (cfg *Config) Complete() CompletedConfig { return CompletedConfig{&c} } -// New returns a new instance of AppsServer from the given configuration. -func (c completedConfig) New() (*AppsServer, error) { - genericServer, err := c.GenericConfig.New("apps-apiserver", genericapiserver.NewEmptyDelegate()) +// New returns a new instance of CozyServer from the given configuration. +func (c completedConfig) New() (*CozyServer, error) { + genericServer, err := c.GenericConfig.New("cozy-apiserver", genericapiserver.NewEmptyDelegate()) if err != nil { return nil, err } - s := &AppsServer{ + s := &CozyServer{ GenericAPIServer: genericServer, } - apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs) - // Create a dynamic client for HelmRelease using InClusterConfig. inClusterConfig, err := restclient.InClusterConfig() if err != nil { @@ -122,16 +126,41 @@ func (c completedConfig) New() (*AppsServer, error) { return nil, fmt.Errorf("unable to create dynamic client: %v", err) } - v1alpha1storage := map[string]rest.Storage{} - - for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(dynamicClient, &resConfig) - v1alpha1storage[resConfig.Application.Plural] = appsregistry.RESTInPeace(storage) + clientset, err := kubernetes.NewForConfig(inClusterConfig) + if err != nil { + return nil, fmt.Errorf("create kube clientset: %v", err) } - apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage + // --- static, cluster-scoped resource for core group --- + coreV1alpha1Storage := map[string]rest.Storage{} + coreV1alpha1Storage["tenantnamespaces"] = cozyregistry.RESTInPeace( + tenantnamespacestorage.NewREST( + clientset.CoreV1(), + clientset.AuthorizationV1(), + 20, + ), + ) + coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace( + tenantsecretstorage.NewREST( + clientset.CoreV1(), + ), + ) - if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil { + coreApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(core.GroupName, Scheme, metav1.ParameterCodec, Codecs) + coreApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = coreV1alpha1Storage + if err := s.GenericAPIServer.InstallAPIGroup(&coreApiGroupInfo); err != nil { + 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 } diff --git a/pkg/apiserver/scheme_test.go b/pkg/apiserver/scheme_test.go index e00a1d62..af420791 100644 --- a/pkg/apiserver/scheme_test.go +++ b/pkg/apiserver/scheme_test.go @@ -20,9 +20,11 @@ import ( "testing" appsfuzzer "github.com/cozystack/cozystack/pkg/apis/apps/fuzzer" + corefuzzer "github.com/cozystack/cozystack/pkg/apis/core/fuzzer" "k8s.io/apimachinery/pkg/api/apitesting/roundtrip" ) func TestRoundTripTypes(t *testing.T) { roundtrip.RoundTripTestForScheme(t, Scheme, appsfuzzer.Funcs) + roundtrip.RoundTripTestForScheme(t, Scheme, corefuzzer.Funcs) } diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 8f26b76c..25449ece 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -25,8 +25,9 @@ import ( "io" "net" - corev1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" - "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + v1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" "github.com/cozystack/cozystack/pkg/apiserver" "github.com/cozystack/cozystack/pkg/config" sampleopenapi "github.com/cozystack/cozystack/pkg/generated/openapi" @@ -47,8 +48,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// AppsServerOptions holds the state for the Apps API server -type AppsServerOptions struct { +// CozyServerOptions holds the state for the Cozy API server +type CozyServerOptions struct { RecommendedOptions *genericoptions.RecommendedOptions StdOut io.Writer @@ -61,12 +62,15 @@ type AppsServerOptions struct { ResourceConfig *config.ResourceConfig } -// NewAppsServerOptions returns a new instance of AppsServerOptions -func NewAppsServerOptions(out, errOut io.Writer) *AppsServerOptions { - o := &AppsServerOptions{ +// NewCozyServerOptions returns a new instance of CozyServerOptions +func NewCozyServerOptions(out, errOut io.Writer) *CozyServerOptions { + o := &CozyServerOptions{ RecommendedOptions: genericoptions.NewRecommendedOptions( "", - apiserver.Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion), + apiserver.Codecs.LegacyCodec( + corev1alpha1.SchemeGroupVersion, + appsv1alpha1.SchemeGroupVersion, + ), ), StdOut: out, StdErr: errOut, @@ -75,12 +79,12 @@ func NewAppsServerOptions(out, errOut io.Writer) *AppsServerOptions { return o } -// NewCommandStartAppsServer provides a CLI handler for the 'start apps-server' command -func NewCommandStartAppsServer(ctx context.Context, defaults *AppsServerOptions) *cobra.Command { +// NewCommandStartCozyServer provides a CLI handler for the 'start apps-server' command +func NewCommandStartCozyServer(ctx context.Context, defaults *CozyServerOptions) *cobra.Command { o := *defaults cmd := &cobra.Command{ - Short: "Launch an Apps API server", - Long: "Launch an Apps API server", + Short: "Launch an Cozystack API server", + Long: "Launch an Cozystack API server", PersistentPreRunE: func(*cobra.Command, []string) error { return utilversionpkg.DefaultComponentGlobalsRegistry.Set() }, @@ -91,7 +95,7 @@ func NewCommandStartAppsServer(ctx context.Context, defaults *AppsServerOptions) if err := o.Validate(args); err != nil { return err } - if err := o.RunAppsServer(c.Context()); err != nil { + if err := o.RunCozyServer(c.Context()); err != nil { return err } return nil @@ -103,18 +107,18 @@ func NewCommandStartAppsServer(ctx context.Context, defaults *AppsServerOptions) o.RecommendedOptions.AddFlags(flags) // The following lines demonstrate how to configure version compatibility and feature gates - // for the "Apps" component according to KEP-4330. + // for the "Cozy" component according to KEP-4330. - // Create a default version object for the "Apps" component. - defaultAppsVersion := "1.1" - // Register the "Apps" component in the global component registry, + // Create a default version object for the "Cozy" component. + defaultCozyVersion := "1.1" + // Register the "Cozy" component in the global component registry, // associating it with its effective version and feature gate configuration. _, appsFeatureGate := utilversionpkg.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister( - apiserver.AppsComponentName, utilversionpkg.NewEffectiveVersion(defaultAppsVersion), - featuregate.NewVersionedFeatureGate(version.MustParse(defaultAppsVersion)), + apiserver.CozyComponentName, utilversionpkg.NewEffectiveVersion(defaultCozyVersion), + featuregate.NewVersionedFeatureGate(version.MustParse(defaultCozyVersion)), ) - // Add feature gate specifications for the "Apps" component. + // Add feature gate specifications for the "Cozy" component. utilruntime.Must(appsFeatureGate.AddVersioned(map[featuregate.Feature]featuregate.VersionedSpecs{ // Example of adding feature gates: // "FeatureName": {{"v1", true}, {"v2", false}}, @@ -127,9 +131,9 @@ func NewCommandStartAppsServer(ctx context.Context, defaults *AppsServerOptions) utilfeature.DefaultMutableFeatureGate, ) - // Set the version emulation mapping from the "Apps" component to the kube component. + // Set the version emulation mapping from the "Cozy" component to the kube component. utilruntime.Must(utilversionpkg.DefaultComponentGlobalsRegistry.SetEmulationVersionMapping( - apiserver.AppsComponentName, utilversionpkg.DefaultKubeComponent, AppsVersionToKubeVersion, + apiserver.CozyComponentName, utilversionpkg.DefaultKubeComponent, CozyVersionToKubeVersion, )) // Add flags from the global component registry. @@ -139,9 +143,9 @@ func NewCommandStartAppsServer(ctx context.Context, defaults *AppsServerOptions) } // Complete fills in the fields that are not set -func (o *AppsServerOptions) Complete() error { +func (o *CozyServerOptions) Complete() error { scheme := runtime.NewScheme() - if err := corev1alpha1.AddToScheme(scheme); err != nil { + if err := v1alpha1.AddToScheme(scheme); err != nil { return fmt.Errorf("failed to register types: %w", err) } @@ -155,7 +159,7 @@ func (o *AppsServerOptions) Complete() error { return fmt.Errorf("client initialization failed: %w", err) } - crdList := &corev1alpha1.CozystackResourceDefinitionList{} + crdList := &v1alpha1.CozystackResourceDefinitionList{} if err := o.Client.List(context.Background(), crdList); err != nil { return fmt.Errorf("failed to list CozystackResourceDefinitions: %w", err) @@ -192,15 +196,15 @@ func (o *AppsServerOptions) Complete() error { } // Validate checks the correctness of the options -func (o AppsServerOptions) Validate(args []string) error { +func (o CozyServerOptions) Validate(args []string) error { var allErrors []error allErrors = append(allErrors, o.RecommendedOptions.Validate()...) allErrors = append(allErrors, utilversionpkg.DefaultComponentGlobalsRegistry.Validate()...) return utilerrors.NewAggregate(allErrors) } -// Config returns the configuration for the API server based on AppsServerOptions -func (o *AppsServerOptions) Config() (*apiserver.Config, error) { +// Config returns the configuration for the API server based on CozyServerOptions +func (o *CozyServerOptions) Config() (*apiserver.Config, error) { // TODO: set the "real" external address if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts( "localhost", o.AlternateDNS, []net.IP{netutils.ParseIPSloppy("127.0.0.1")}, @@ -208,8 +212,11 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) { return nil, fmt.Errorf("error creating self-signed certificates: %v", err) } - // First, register the dynamic types - err := v1alpha1.RegisterDynamicTypes(apiserver.Scheme, o.ResourceConfig) + // Register *compile-time* resources first. + corev1alpha1.RegisterStaticTypes(apiserver.Scheme) + + // Register *run-time* resources (from the user’s config file). + err := appsv1alpha1.RegisterDynamicTypes(apiserver.Scheme, o.ResourceConfig) if err != nil { return nil, fmt.Errorf("failed to register dynamic types: %v", err) } @@ -236,14 +243,14 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) { kindSchemas[r.Application.Kind] = r.Application.OpenAPISchema } - serverConfig.OpenAPIConfig.Info.Title = "Apps" + serverConfig.OpenAPIConfig.Info.Title = "Cozy" serverConfig.OpenAPIConfig.Info.Version = version serverConfig.OpenAPIConfig.PostProcessSpec = buildPostProcessV2(kindSchemas) serverConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config( sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme), ) - serverConfig.OpenAPIV3Config.Info.Title = "Apps" + serverConfig.OpenAPIV3Config.Info.Title = "Cozy" serverConfig.OpenAPIV3Config.Info.Version = version serverConfig.OpenAPIV3Config.PostProcessSpec = buildPostProcessV3(kindSchemas) @@ -252,7 +259,7 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) { utilversionpkg.DefaultKubeComponent, ) serverConfig.EffectiveVersion = utilversionpkg.DefaultComponentGlobalsRegistry.EffectiveVersionFor( - apiserver.AppsComponentName, + apiserver.CozyComponentName, ) if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil { @@ -266,8 +273,8 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) { return config, nil } -// RunAppsServer launches a new AppsServer based on AppsServerOptions -func (o AppsServerOptions) RunAppsServer(ctx context.Context) error { +// RunCozyServer launches a new CozyServer based on CozyServerOptions +func (o CozyServerOptions) RunCozyServer(ctx context.Context) error { config, err := o.Config() if err != nil { return err @@ -286,8 +293,8 @@ func (o AppsServerOptions) RunAppsServer(ctx context.Context) error { return server.GenericAPIServer.PrepareRun().RunWithContext(ctx) } -// AppsVersionToKubeVersion defines the version mapping between the Apps component and kube -func AppsVersionToKubeVersion(ver *version.Version) *version.Version { +// CozyVersionToKubeVersion defines the version mapping between the Cozy component and kube +func CozyVersionToKubeVersion(ver *version.Version) *version.Version { if ver.Major() != 1 { return nil } diff --git a/pkg/cmd/server/start_test.go b/pkg/cmd/server/start_test.go index ddff60d9..da96ed31 100644 --- a/pkg/cmd/server/start_test.go +++ b/pkg/cmd/server/start_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAppsEmulationVersionToKubeEmulationVersion(t *testing.T) { +func TestCozyEmulationVersionToKubeEmulationVersion(t *testing.T) { defaultKubeEffectiveVersion := utilversion.DefaultKubeEffectiveVersion() testCases := []struct { @@ -61,7 +61,7 @@ func TestAppsEmulationVersionToKubeEmulationVersion(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - mappedKubeEmulationVer := AppsVersionToKubeVersion(tc.appsEmulationVer) + mappedKubeEmulationVer := CozyVersionToKubeVersion(tc.appsEmulationVer) assert.True(t, mappedKubeEmulationVer.EqualTo(tc.expectedKubeEmulationVer)) }) } diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index ed1d159c..c85874e4 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -33,6 +33,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1.Application": schema_pkg_apis_apps_v1alpha1_Application(ref), "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1.ApplicationList": schema_pkg_apis_apps_v1alpha1_ApplicationList(ref), "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), @@ -252,6 +256,208 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref common.ReferenceCallbac } } +func schema_pkg_apis_core_v1alpha1_TenantNamespace(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TenantNamespace is a thin wrapper around ObjectMeta. It has no spec/status because it merely reflects an existing Namespace object.", + 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"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TenantNamespaceList is the list variant for TenantNamespace.", + 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.TenantNamespace"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +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 new file mode 100644 index 00000000..3b18f3e3 --- /dev/null +++ b/pkg/registry/core/tenantnamespace/rest.go @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache-2.0 +// TenantNamespace registry: read-only view over Namespaces whose names start +// with “tenant-”. + +package tenantnamespace + +import ( + "context" + "fmt" + "math" + "net/http" + "strings" + "sync" + "time" + + authorizationv1 "k8s.io/api/authorization/v1" + 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/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" + 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 ( + prefix = "tenant-" + singularName = "tenantnamespace" +) + +// ----------------------------------------------------------------------------- +// REST storage +// ----------------------------------------------------------------------------- + +var ( + _ rest.Lister = &REST{} + _ rest.Getter = &REST{} + _ rest.Watcher = &REST{} + _ rest.TableConvertor = &REST{} + _ rest.Scoper = &REST{} + _ rest.SingularNameProvider = &REST{} +) + +type REST struct { + core corev1client.CoreV1Interface + authClient authorizationv1client.AuthorizationV1Interface + maxWorkers int + gvr schema.GroupVersionResource +} + +func NewREST( + coreCli corev1client.CoreV1Interface, + authCli authorizationv1client.AuthorizationV1Interface, + maxWorkers int, +) *REST { + return &REST{ + core: coreCli, + authClient: authCli, + maxWorkers: maxWorkers, + gvr: schema.GroupVersionResource{ + Group: corev1alpha1.GroupName, + Version: "v1alpha1", + Resource: "tenantnamespaces", + }, + } +} + +// ----------------------------------------------------------------------------- +// Basic meta +// ----------------------------------------------------------------------------- + +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 (*REST) GetSingularName() string { return singularName } + +// ----------------------------------------------------------------------------- +// Lister / Getter +// ----------------------------------------------------------------------------- + +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 tenantNames []string + for i := range nsList.Items { + if strings.HasPrefix(nsList.Items[i].Name, prefix) { + tenantNames = append(tenantNames, nsList.Items[i].Name) + } + } + + allowed, err := r.filterAccessible(ctx, tenantNames) + if err != nil { + return nil, err + } + + return r.makeList(nsList, allowed), nil +} + +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) + } + + ns, err := r.core.Namespaces().Get(ctx, name, *opts) + if err != nil { + return nil, err + } + + return &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, + }, + }, nil +} + +// ----------------------------------------------------------------------------- +// Watcher +// ----------------------------------------------------------------------------- + +func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { + nsWatch, err := r.core.Namespaces().Watch(ctx, metav1.ListOptions{ + Watch: true, + ResourceVersion: opts.ResourceVersion, + }) + if err != nil { + return nil, err + } + + events := make(chan watch.Event) + pw := watch.NewProxyWatcher(events) + + 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 +} + +// ----------------------------------------------------------------------------- +// TableConvertor +// ----------------------------------------------------------------------------- + +func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) { + now := time.Now() + row := func(o *corev1alpha1.TenantNamespace) metav1.TableRow { + return metav1.TableRow{ + Cells: []interface{}{o.Name, 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: "AGE", Type: "string"}, + }, + } + + switch v := obj.(type) { + case *corev1alpha1.TenantNamespaceList: + for i := range v.Items { + tbl.Rows = append(tbl.Rows, row(&v.Items[i])) + } + tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion + case *corev1alpha1.TenantNamespace: + 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 +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +func (r *REST) makeList(src *corev1.NamespaceList, allowed []string) *corev1alpha1.TenantNamespaceList { + set := map[string]struct{}{} + for _, n := range allowed { + set[n] = struct{}{} + } + + out := &corev1alpha1.TenantNamespaceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1alpha1.SchemeGroupVersion.String(), + Kind: "TenantNamespaceList", + }, + ListMeta: metav1.ListMeta{ResourceVersion: src.ResourceVersion}, + } + + 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: 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) filterAccessible( + ctx context.Context, + names []string, +) ([]string, error) { + workers := int(math.Min(float64(r.maxWorkers), float64(len(names)))) + type job struct{ name string } + type res struct { + name string + allowed bool + err error + } + 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() { + 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 names { + jobs <- job{n} + } + close(jobs) + }() + + var allowed []string + for r := range out { + if r.err != nil { + klog.Errorf("SAR failed for %s: %v", r.name, r.err) + continue + } + if r.allowed { + allowed = append(allowed, r.name) + } + } + return allowed, nil +} + +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") + } + + sar := &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + User: u.GetName(), + Groups: u.GetGroups(), + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Group: "cozystack.io", + Resource: "workloadmonitors", + Verb: "get", + Namespace: ns, + }, + }, + } + + 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, + } +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 5131f71d..44d7022c 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -17,24 +17,17 @@ limitations under the License. package registry import ( - "github.com/cozystack/cozystack/pkg/registry/apps/application" - "k8s.io/apimachinery/pkg/runtime/schema" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" ) -// REST implements a RESTStorage for API services against etcd +// REST is a thin wrapper around genericregistry.Store that also satisfies +// the GroupVersionKindProvider interface if callers need it later. type REST struct { *genericregistry.Store - GVK schema.GroupVersionKind } -// Implement the GroupVersionKindProvider interface -func (r *REST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind { - return r.GVK -} - -// RESTInPeace creates REST for Application -func RESTInPeace(r *application.REST) rest.Storage { - return r -} +// RESTInPeace is a tiny helper so the call-site code reads nicely. It simply +// returns its argument, letting us defer (and centralise) any future error +// handling here. +func RESTInPeace(storage rest.Storage) rest.Storage { return storage }