[cozystack-api] Implement TenantNamespace, TenantModules, TenantSecret and TenantSecretsTable resources

[cozystack-controller] Introduce new dashboard-controller
[dashboard] Introduce new dashboard based on openapi-ui

Co-authored-by: kklinch0 <kklinch0@gmail.com>
Signed-off-by: kklinch0 <kklinch0@gmail.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil
2025-09-24 11:33:27 +02:00
parent 789666d53b
commit 0afc3c1e86
368 changed files with 15196 additions and 24782 deletions

View File

@@ -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
}

View File

@@ -37,6 +37,9 @@ type ApplicationStatus struct {
// +optional
Version string `json:"version,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Namespace holds the computed namespace for Tenant applications.
// +optional
Namespace string `json:"namespace,omitempty"`
}
// GetConditions returns the status conditions of the object.

View File

@@ -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
},
}
}

View File

@@ -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))
}

View File

@@ -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)
}

22
pkg/apis/core/register.go Normal file
View File

@@ -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"
)

View File

@@ -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"

View File

@@ -0,0 +1,69 @@
// 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{},
&TenantSecretsTable{},
&TenantSecretsTableList{},
&TenantModule{},
&TenantModuleList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret, TenantSecretsTable, TenantModule")
}

View File

@@ -0,0 +1,40 @@
// 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
// TenantModule represents a HelmRelease with the label internal.cozystack.io/tenantmodule=true
type TenantModule struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// AppVersion represents the version of the Helm chart
AppVersion string `json:"appVersion,omitempty"`
// Status contains the module status
Status TenantModuleStatus `json:"status,omitempty"`
}
// TenantModuleStatus represents the status of a TenantModule
type TenantModuleStatus struct {
// Version represents the last attempted revision
Version string `json:"version,omitempty"`
// Conditions represent the latest available observations of the module's state
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TenantModuleList contains a list of TenantModule
type TenantModuleList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TenantModule `json:"items"`
}
// DeepCopy methods are generated by deepcopy-gen

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// TenantSecretEntry represents a single key from a Secret's data.
type TenantSecretEntry struct {
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TenantSecretsTable is a virtual, namespaced resource that exposes every key
// of Secrets labelled cozystack.io/ui=true as a separate object.
type TenantSecretsTable struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Data TenantSecretEntry `json:"data,omitempty"`
}
// DeepCopy methods are generated by deepcopy-gen
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TenantSecretsTableList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TenantSecretsTable `json:"items"`
}
// DeepCopy methods are generated by deepcopy-gen

View File

@@ -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
}

View File

@@ -0,0 +1,326 @@
//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 (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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 *TenantModule) DeepCopyInto(out *TenantModule) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Status.DeepCopyInto(&out.Status)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantModule.
func (in *TenantModule) DeepCopy() *TenantModule {
if in == nil {
return nil
}
out := new(TenantModule)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantModule) 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 *TenantModuleList) DeepCopyInto(out *TenantModuleList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantModule, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantModuleList.
func (in *TenantModuleList) DeepCopy() *TenantModuleList {
if in == nil {
return nil
}
out := new(TenantModuleList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantModuleList) 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 *TenantModuleStatus) DeepCopyInto(out *TenantModuleStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantModuleStatus.
func (in *TenantModuleStatus) DeepCopy() *TenantModuleStatus {
if in == nil {
return nil
}
out := new(TenantModuleStatus)
in.DeepCopyInto(out)
return out
}
// 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 *TenantSecretEntry) DeepCopyInto(out *TenantSecretEntry) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretEntry.
func (in *TenantSecretEntry) DeepCopy() *TenantSecretEntry {
if in == nil {
return nil
}
out := new(TenantSecretEntry)
in.DeepCopyInto(out)
return out
}
// 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
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecretsTable) DeepCopyInto(out *TenantSecretsTable) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Data = in.Data
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretsTable.
func (in *TenantSecretsTable) DeepCopy() *TenantSecretsTable {
if in == nil {
return nil
}
out := new(TenantSecretsTable)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantSecretsTable) 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 *TenantSecretsTableList) DeepCopyInto(out *TenantSecretsTableList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantSecretsTable, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretsTableList.
func (in *TenantSecretsTableList) DeepCopy() *TenantSecretsTableList {
if in == nil {
return nil
}
out := new(TenantSecretsTableList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantSecretsTableList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -27,13 +27,20 @@ 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"
tenantmodulestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantmodule"
tenantnamespacestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantnamespace"
tenantsecretstorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecret"
tenantsecretstablestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecretstable"
)
var (
@@ -42,11 +49,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 +81,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 +106,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 +128,51 @@ 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(),
),
)
coreV1alpha1Storage["tenantsecretstables"] = cozyregistry.RESTInPeace(
tenantsecretstablestorage.NewREST(
clientset.CoreV1(),
),
)
coreV1alpha1Storage["tenantmodules"] = cozyregistry.RESTInPeace(
tenantmodulestorage.NewREST(
dynamicClient,
),
)
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
}

View File

@@ -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)
}

View File

@@ -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 users 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
}

View File

@@ -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))
})
}

View File

@@ -33,6 +33,16 @@ 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.TenantModule": schema_pkg_apis_core_v1alpha1_TenantModule(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantModuleList": schema_pkg_apis_core_v1alpha1_TenantModuleList(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantModuleStatus": schema_pkg_apis_core_v1alpha1_TenantModuleStatus(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.TenantSecretEntry": schema_pkg_apis_core_v1alpha1_TenantSecretEntry(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretList": schema_pkg_apis_core_v1alpha1_TenantSecretList(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable": schema_pkg_apis_core_v1alpha1_TenantSecretsTable(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTableList": schema_pkg_apis_core_v1alpha1_TenantSecretsTableList(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),
@@ -244,6 +254,13 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref common.ReferenceCallbac
},
},
},
"namespace": {
SchemaProps: spec.SchemaProps{
Description: "Namespace holds the computed namespace for Tenant applications.",
Type: []string{"string"},
Format: "",
},
},
},
},
},
@@ -252,6 +269,462 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref common.ReferenceCallbac
}
}
func schema_pkg_apis_core_v1alpha1_TenantModule(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantModule represents a HelmRelease with the label internal.cozystack.io/tenantmodule=true",
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"),
},
},
"appVersion": {
SchemaProps: spec.SchemaProps{
Description: "AppVersion represents the version of the Helm chart",
Type: []string{"string"},
Format: "",
},
},
"status": {
SchemaProps: spec.SchemaProps{
Description: "Status contains the module status",
Default: map[string]interface{}{},
Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantModuleStatus"),
},
},
},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantModuleStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantModuleList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantModuleList contains a list of TenantModule",
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.TenantModule"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantModule", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantModuleStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantModuleStatus represents the status of a TenantModule",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"version": {
SchemaProps: spec.SchemaProps{
Description: "Version represents the last attempted revision",
Type: []string{"string"},
Format: "",
},
},
"conditions": {
SchemaProps: spec.SchemaProps{
Description: "Conditions represent the latest available observations of the module's state",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition"},
}
}
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_TenantSecretEntry(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantSecretEntry represents a single key from a Secret's data.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"key": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"value": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
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_core_v1alpha1_TenantSecretsTable(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantSecretsTable is a virtual, namespaced resource that exposes every key of Secrets labelled cozystack.io/ui=true as a separate 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"),
},
},
"data": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry"),
},
},
},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecretsTableList(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.TenantSecretsTable"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable", "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{

View File

@@ -997,6 +997,12 @@ func (r *REST) convertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1al
}
}
app.SetConditions(conditions)
// Add namespace field for Tenant applications
if r.kindName == "Tenant" {
app.Status.Namespace = r.computeTenantNamespace(hr.Namespace, app.Name)
}
return app, nil
}
@@ -1183,6 +1189,25 @@ func getReadyStatus(conditions []metav1.Condition) string {
return "Unknown"
}
// computeTenantNamespace computes the namespace for a Tenant application based on the specified logic
func (r *REST) computeTenantNamespace(currentNamespace, tenantName string) string {
hrName := r.releaseConfig.Prefix + tenantName
switch {
case currentNamespace == "tenant-root" && hrName == "tenant-root":
// 1) root tenant inside root namespace
return "tenant-root"
case currentNamespace == "tenant-root":
// 2) any other tenant in root namespace
return fmt.Sprintf("tenant-%s", tenantName)
default:
// 3) tenant in a dedicated namespace
return fmt.Sprintf("%s-%s", currentNamespace, tenantName)
}
}
// Destroy releases resources associated with REST
func (r *REST) Destroy() {
// No additional actions needed to release resources.

View File

@@ -0,0 +1,801 @@
/*
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 tenantmodule
import (
"context"
"fmt"
"net/http"
"sync"
"time"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
fields "k8s.io/apimachinery/pkg/fields"
labels "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/util/duration"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/client-go/dynamic"
"k8s.io/klog/v2"
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
// Ensure REST implements necessary interfaces
var (
_ rest.Lister = &REST{}
_ rest.Getter = &REST{}
_ rest.Watcher = &REST{}
_ rest.TableConvertor = &REST{}
_ rest.Scoper = &REST{}
_ rest.SingularNameProvider = &REST{}
)
// Define constants for label filtering
const (
TenantModuleLabelKey = "apps.cozystack.io/tenantmodule"
TenantModuleLabelValue = "true"
singularName = "tenantmodule"
)
// Define the GroupVersionResource for HelmRelease
var helmReleaseGVR = schema.GroupVersionResource{
Group: "helm.toolkit.fluxcd.io",
Version: "v2",
Resource: "helmreleases",
}
// REST implements the RESTStorage interface for TenantModule resources
type REST struct {
dynamicClient dynamic.Interface
gvr schema.GroupVersionResource
gvk schema.GroupVersionKind
kindName string
singularName string
}
// NewREST creates a new REST storage for TenantModule
func NewREST(dynamicClient dynamic.Interface) *REST {
return &REST{
dynamicClient: dynamicClient,
gvr: schema.GroupVersionResource{
Group: corev1alpha1.GroupName,
Version: "v1alpha1",
Resource: "tenantmodules",
},
gvk: schema.GroupVersion{
Group: corev1alpha1.GroupName,
Version: "v1alpha1",
}.WithKind("TenantModule"),
kindName: "TenantModule",
singularName: singularName,
}
}
// NamespaceScoped indicates whether the resource is namespaced
func (r *REST) NamespaceScoped() bool {
return true
}
// GetSingularName returns the singular name of the resource
func (r *REST) GetSingularName() string {
return r.singularName
}
// Get retrieves a TenantModule by converting the corresponding HelmRelease
func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
namespace, err := r.getNamespace(ctx)
if err != nil {
klog.Errorf("Failed to get namespace: %v", err)
return nil, err
}
klog.V(6).Infof("Attempting to retrieve TenantModule %s in namespace %s", name, namespace)
// Get the corresponding HelmRelease
hr, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Get(ctx, name, *options)
if err != nil {
klog.Errorf("Error retrieving HelmRelease for TenantModule %s: %v", name, err)
// Check if the error is a NotFound error
if apierrors.IsNotFound(err) {
// Return a NotFound error for the TenantModule resource instead of HelmRelease
return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
// For other errors, return them as-is
return nil, err
}
// Check if HelmRelease has the required label
if !r.hasTenantModuleLabel(hr) {
klog.Errorf("HelmRelease %s does not have the required label %s=%s", name, TenantModuleLabelKey, TenantModuleLabelValue)
// Return a NotFound error for the TenantModule resource
return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
// Convert HelmRelease to TenantModule
convertedModule, err := r.ConvertHelmReleaseToTenantModule(hr)
if err != nil {
klog.Errorf("Conversion error from HelmRelease to TenantModule for resource %s: %v", name, err)
return nil, fmt.Errorf("conversion error: %v", err)
}
// Explicitly set apiVersion and kind for TenantModule
convertedModule.TypeMeta = metav1.TypeMeta{
APIVersion: "core.cozystack.io/v1alpha1",
Kind: r.kindName,
}
// Convert TenantModule to unstructured format
unstructuredModule, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&convertedModule)
if err != nil {
klog.Errorf("Failed to convert TenantModule to unstructured for resource %s: %v", name, err)
return nil, fmt.Errorf("failed to convert TenantModule to unstructured: %v", err)
}
// Explicitly set apiVersion and kind in unstructured object
unstructuredModule["apiVersion"] = "core.cozystack.io/v1alpha1"
unstructuredModule["kind"] = r.kindName
klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s to unstructured", name, r.gvr.Resource)
return &unstructured.Unstructured{Object: unstructuredModule}, nil
}
// List retrieves a list of TenantModules by converting HelmReleases
func (r *REST) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
namespace, err := r.getNamespace(ctx)
if err != nil {
klog.Errorf("Failed to get namespace: %v", err)
return nil, err
}
klog.V(6).Infof("Attempting to list TenantModules in namespace %s with options: %v", namespace, options)
// Get resource name from the request (if any)
var resourceName string
if requestInfo, ok := request.RequestInfoFrom(ctx); ok {
resourceName = requestInfo.Name
}
// Initialize variables for selector mapping
var helmFieldSelector string
var helmLabelSelector string
// Process field.selector
if options.FieldSelector != nil {
fs, err := fields.ParseSelector(options.FieldSelector.String())
if err != nil {
klog.Errorf("Invalid field selector: %v", err)
return nil, fmt.Errorf("invalid field selector: %v", err)
}
// Check if selector is for metadata.name
if name, exists := fs.RequiresExactMatch("metadata.name"); exists {
// Create new field.selector for HelmRelease
helmFieldSelector = fields.OneTermEqualSelector("metadata.name", name).String()
} else {
// If field.selector contains other fields, map them directly
helmFieldSelector = fs.String()
}
}
// Process label.selector - add the tenant module label requirement
tenantModuleReq, err := labels.NewRequirement(TenantModuleLabelKey, selection.Equals, []string{TenantModuleLabelValue})
if err != nil {
klog.Errorf("Error creating tenant module label requirement: %v", err)
return nil, fmt.Errorf("error creating tenant module label requirement: %v", err)
}
labelRequirements := []labels.Requirement{*tenantModuleReq}
if options.LabelSelector != nil {
ls := options.LabelSelector.String()
parsedLabels, err := labels.Parse(ls)
if err != nil {
klog.Errorf("Invalid label selector: %v", err)
return nil, fmt.Errorf("invalid label selector: %v", err)
}
if !parsedLabels.Empty() {
reqs, _ := parsedLabels.Requirements()
labelRequirements = append(labelRequirements, reqs...)
}
}
helmLabelSelector = labels.NewSelector().Add(labelRequirements...).String()
// Set ListOptions for HelmRelease with selector mapping
metaOptions := metav1.ListOptions{
FieldSelector: helmFieldSelector,
LabelSelector: helmLabelSelector,
}
// List HelmReleases with mapped selectors
hrList, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).List(ctx, metaOptions)
if err != nil {
klog.Errorf("Error listing HelmReleases: %v", err)
return nil, err
}
// Initialize unstructured items array
items := make([]unstructured.Unstructured, 0)
// Iterate over HelmReleases and convert to TenantModules
for _, hr := range hrList.Items {
// Double-check the label requirement
if !r.hasTenantModuleLabel(&hr) {
continue
}
module, err := r.ConvertHelmReleaseToTenantModule(&hr)
if err != nil {
klog.Errorf("Error converting HelmRelease %s to TenantModule: %v", hr.GetName(), err)
continue
}
// If resourceName is set, check for match
if resourceName != "" && module.Name != resourceName {
continue
}
// Apply label.selector
if options.LabelSelector != nil {
sel, err := labels.Parse(options.LabelSelector.String())
if err != nil {
klog.Errorf("Invalid label selector: %v", err)
continue
}
if !sel.Matches(labels.Set(module.Labels)) {
continue
}
}
// Apply field.selector by name and namespace (if specified)
if options.FieldSelector != nil {
fs, err := fields.ParseSelector(options.FieldSelector.String())
if err != nil {
klog.Errorf("Invalid field selector: %v", err)
continue
}
fieldsSet := fields.Set{
"metadata.name": module.Name,
"metadata.namespace": module.Namespace,
}
if !fs.Matches(fieldsSet) {
continue
}
}
// Convert TenantModule to unstructured
unstructuredModule, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&module)
if err != nil {
klog.Errorf("Error converting TenantModule %s to unstructured: %v", module.Name, err)
continue
}
items = append(items, unstructured.Unstructured{Object: unstructuredModule})
}
// Explicitly set apiVersion and kind in unstructured object
moduleList := &unstructured.UnstructuredList{}
moduleList.SetAPIVersion("core.cozystack.io/v1alpha1")
moduleList.SetKind(r.kindName + "List")
moduleList.SetResourceVersion(hrList.GetResourceVersion())
moduleList.Items = items
klog.V(6).Infof("Successfully listed %d TenantModule resources in namespace %s", len(items), namespace)
return moduleList, nil
}
// Watch sets up a watch on HelmReleases, filters them based on tenant module label, and converts events to TenantModules
func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
namespace, err := r.getNamespace(ctx)
if err != nil {
klog.Errorf("Failed to get namespace: %v", err)
return nil, err
}
klog.V(6).Infof("Setting up watch for TenantModules in namespace %s with options: %v", namespace, options)
// Get request information, including resource name if specified
var resourceName string
if requestInfo, ok := request.RequestInfoFrom(ctx); ok {
resourceName = requestInfo.Name
}
// Initialize variables for selector mapping
var helmFieldSelector string
var helmLabelSelector string
// Process field.selector
if options.FieldSelector != nil {
fs, err := fields.ParseSelector(options.FieldSelector.String())
if err != nil {
klog.Errorf("Invalid field selector: %v", err)
return nil, fmt.Errorf("invalid field selector: %v", err)
}
// Check if selector is for metadata.name
if name, exists := fs.RequiresExactMatch("metadata.name"); exists {
// Create new field.selector for HelmRelease
helmFieldSelector = fields.OneTermEqualSelector("metadata.name", name).String()
} else {
// If field.selector contains other fields, map them directly
helmFieldSelector = fs.String()
}
}
// Process label.selector - add the tenant module label requirement
tenantModuleReq, err := labels.NewRequirement(TenantModuleLabelKey, selection.Equals, []string{TenantModuleLabelValue})
if err != nil {
klog.Errorf("Error creating tenant module label requirement: %v", err)
return nil, fmt.Errorf("error creating tenant module label requirement: %v", err)
}
labelRequirements := []labels.Requirement{*tenantModuleReq}
if options.LabelSelector != nil {
ls := options.LabelSelector.String()
parsedLabels, err := labels.Parse(ls)
if err != nil {
klog.Errorf("Invalid label selector: %v", err)
return nil, fmt.Errorf("invalid label selector: %v", err)
}
if !parsedLabels.Empty() {
reqs, _ := parsedLabels.Requirements()
labelRequirements = append(labelRequirements, reqs...)
}
}
helmLabelSelector = labels.NewSelector().Add(labelRequirements...).String()
// Set ListOptions for HelmRelease with selector mapping
metaOptions := metav1.ListOptions{
Watch: true,
ResourceVersion: options.ResourceVersion,
FieldSelector: helmFieldSelector,
LabelSelector: helmLabelSelector,
}
// Start watch on HelmRelease with mapped selectors
helmWatcher, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Watch(ctx, metaOptions)
if err != nil {
klog.Errorf("Error setting up watch for HelmReleases: %v", err)
return nil, err
}
// Create a custom watcher to transform events
customW := &customWatcher{
resultChan: make(chan watch.Event),
stopChan: make(chan struct{}),
}
go func() {
defer close(customW.resultChan)
for {
select {
case event, ok := <-helmWatcher.ResultChan():
if !ok {
// The watcher has been closed, attempt to re-establish the watch
klog.Warning("HelmRelease watcher closed, attempting to re-establish")
// Implement retry logic or exit based on your requirements
return
}
// Check if the object is a *v1.Status
if status, ok := event.Object.(*metav1.Status); ok {
klog.V(4).Infof("Received Status object in HelmRelease watch: %v", status.Message)
continue // Skip processing this event
}
// Proceed with processing Unstructured objects
matches, err := r.isRelevantHelmRelease(&event)
if err != nil {
klog.V(4).Infof("Non-critical error filtering HelmRelease event: %v", err)
continue
}
if !matches {
continue
}
// Convert HelmRelease to TenantModule
module, err := r.ConvertHelmReleaseToTenantModule(event.Object.(*unstructured.Unstructured))
if err != nil {
klog.Errorf("Error converting HelmRelease to TenantModule: %v", err)
continue
}
// Apply field.selector by name if specified
if resourceName != "" && module.Name != resourceName {
continue
}
// Apply label.selector
if options.LabelSelector != nil {
sel, err := labels.Parse(options.LabelSelector.String())
if err != nil {
klog.Errorf("Invalid label selector: %v", err)
continue
}
if !sel.Matches(labels.Set(module.Labels)) {
continue
}
}
// Convert TenantModule to unstructured
unstructuredModule, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&module)
if err != nil {
klog.Errorf("Failed to convert TenantModule to unstructured: %v", err)
continue
}
// Create watch event with TenantModule object
moduleEvent := watch.Event{
Type: event.Type,
Object: &unstructured.Unstructured{Object: unstructuredModule},
}
// Send event to custom watcher
select {
case customW.resultChan <- moduleEvent:
case <-customW.stopChan:
return
case <-ctx.Done():
return
}
case <-customW.stopChan:
return
case <-ctx.Done():
return
}
}
}()
klog.V(6).Infof("Custom watch established successfully")
return customW, nil
}
// customWatcher wraps the original watcher and filters/converts events
type customWatcher struct {
resultChan chan watch.Event
stopChan chan struct{}
stopOnce sync.Once
}
// Stop terminates the watch
func (cw *customWatcher) Stop() {
cw.stopOnce.Do(func() {
close(cw.stopChan)
})
}
// ResultChan returns the event channel
func (cw *customWatcher) ResultChan() <-chan watch.Event {
return cw.resultChan
}
// isRelevantHelmRelease checks if the HelmRelease has the tenant module label
func (r *REST) isRelevantHelmRelease(event *watch.Event) (bool, error) {
if event.Object == nil {
return false, nil
}
// Check if the object is a *v1.Status
if status, ok := event.Object.(*metav1.Status); ok {
// Log at a less severe level or handle specific status errors if needed
klog.V(4).Infof("Received Status object in HelmRelease watch: %v", status.Message)
return false, nil // Not relevant for processing as a HelmRelease
}
// Proceed if it's an Unstructured object
hr, ok := event.Object.(*unstructured.Unstructured)
if !ok {
return false, fmt.Errorf("expected Unstructured object, got %T", event.Object)
}
return r.hasTenantModuleLabel(hr), nil
}
// hasTenantModuleLabel checks if a HelmRelease has the required tenant module label
func (r *REST) hasTenantModuleLabel(hr *unstructured.Unstructured) bool {
labels := hr.GetLabels()
if labels == nil {
return false
}
value, exists := labels[TenantModuleLabelKey]
return exists && value == TenantModuleLabelValue
}
// filterInternalLabels removes internal tenant module labels from the map
func filterInternalLabels(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 == TenantModuleLabelKey {
continue
}
out[k] = v
}
return out
}
// getNamespace extracts the namespace from the context
func (r *REST) getNamespace(ctx context.Context) (string, error) {
namespace, ok := request.NamespaceFrom(ctx)
if !ok {
err := fmt.Errorf("namespace not found in context")
klog.Errorf(err.Error())
return "", err
}
return namespace, nil
}
// ConvertHelmReleaseToTenantModule converts a HelmRelease to a TenantModule
func (r *REST) ConvertHelmReleaseToTenantModule(hr *unstructured.Unstructured) (corev1alpha1.TenantModule, error) {
klog.V(6).Infof("Converting HelmRelease to TenantModule for resource %s", hr.GetName())
var helmRelease helmv2.HelmRelease
// Convert unstructured to HelmRelease struct
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hr.Object, &helmRelease)
if err != nil {
klog.Errorf("Error converting from unstructured to HelmRelease: %v", err)
return corev1alpha1.TenantModule{}, err
}
// Convert HelmRelease struct to TenantModule struct
module, err := r.convertHelmReleaseToTenantModule(&helmRelease)
if err != nil {
klog.Errorf("Error converting from HelmRelease to TenantModule: %v", err)
return corev1alpha1.TenantModule{}, err
}
klog.V(6).Infof("Successfully converted HelmRelease %s to TenantModule", hr.GetName())
return module, nil
}
// convertHelmReleaseToTenantModule implements the actual conversion logic
func (r *REST) convertHelmReleaseToTenantModule(hr *helmv2.HelmRelease) (corev1alpha1.TenantModule, error) {
if hr == nil {
return corev1alpha1.TenantModule{}, fmt.Errorf("HelmRelease is nil")
}
// Safely extract chart version, handling nil cases
var appVersion string
if hr.Spec.Chart != nil {
appVersion = hr.Spec.Chart.Spec.Version
}
module := corev1alpha1.TenantModule{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.cozystack.io/v1alpha1",
Kind: r.kindName,
},
AppVersion: appVersion,
ObjectMeta: metav1.ObjectMeta{
Name: hr.Name,
Namespace: hr.Namespace,
UID: hr.GetUID(),
ResourceVersion: hr.GetResourceVersion(),
CreationTimestamp: hr.CreationTimestamp,
DeletionTimestamp: hr.DeletionTimestamp,
Labels: filterInternalLabels(hr.Labels),
Annotations: hr.Annotations,
},
Status: corev1alpha1.TenantModuleStatus{
Version: hr.Status.LastAttemptedRevision,
},
}
var conditions []metav1.Condition
for _, hrCondition := range hr.GetConditions() {
if hrCondition.Type == "Ready" || hrCondition.Type == "Released" {
conditions = append(conditions, metav1.Condition{
LastTransitionTime: hrCondition.LastTransitionTime,
Reason: hrCondition.Reason,
Message: hrCondition.Message,
Status: hrCondition.Status,
Type: hrCondition.Type,
})
}
}
module.Status.Conditions = conditions
return module, nil
}
// ConvertToTable implements the TableConvertor interface for displaying resources in a table format
func (r *REST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
klog.V(6).Infof("ConvertToTable: received object of type %T", object)
var table metav1.Table
switch obj := object.(type) {
case *unstructured.UnstructuredList:
modules := make([]corev1alpha1.TenantModule, 0, len(obj.Items))
for _, u := range obj.Items {
var m corev1alpha1.TenantModule
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &m)
if err != nil {
klog.Errorf("Failed to convert Unstructured to TenantModule: %v", err)
continue
}
modules = append(modules, m)
}
table = r.buildTableFromTenantModules(modules)
table.ListMeta.ResourceVersion = obj.GetResourceVersion()
case *unstructured.Unstructured:
var module corev1alpha1.TenantModule
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &module)
if err != nil {
klog.Errorf("Failed to convert Unstructured to TenantModule: %v", err)
return nil, fmt.Errorf("failed to convert Unstructured to TenantModule: %v", err)
}
table = r.buildTableFromTenantModule(module)
table.ListMeta.ResourceVersion = obj.GetResourceVersion()
default:
resource := schema.GroupResource{}
if info, ok := request.RequestInfoFrom(ctx); ok {
resource = schema.GroupResource{Group: info.APIGroup, Resource: info.Resource}
}
return nil, errNotAcceptable{
resource: resource,
message: "object does not implement the Object interfaces",
}
}
// Handle table options
if opt, ok := tableOptions.(*metav1.TableOptions); ok && opt != nil && opt.NoHeaders {
table.ColumnDefinitions = nil
}
table.TypeMeta = metav1.TypeMeta{
APIVersion: "meta.k8s.io/v1",
Kind: "Table",
}
klog.V(6).Infof("ConvertToTable: returning table with %d rows", len(table.Rows))
return &table, nil
}
// buildTableFromTenantModules constructs a table from a list of TenantModules
func (r *REST) buildTableFromTenantModules(modules []corev1alpha1.TenantModule) metav1.Table {
table := metav1.Table{
ColumnDefinitions: []metav1.TableColumnDefinition{
{Name: "NAME", Type: "string", Description: "Name of the TenantModule", Priority: 0},
{Name: "READY", Type: "string", Description: "Ready status of the TenantModule", Priority: 0},
{Name: "AGE", Type: "string", Description: "Age of the TenantModule", Priority: 0},
{Name: "VERSION", Type: "string", Description: "Version of the TenantModule", Priority: 0},
},
Rows: make([]metav1.TableRow, 0, len(modules)),
}
now := time.Now()
for _, module := range modules {
row := metav1.TableRow{
Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)},
Object: runtime.RawExtension{Object: &module},
}
table.Rows = append(table.Rows, row)
}
return table
}
// buildTableFromTenantModule constructs a table from a single TenantModule
func (r *REST) buildTableFromTenantModule(module corev1alpha1.TenantModule) metav1.Table {
table := metav1.Table{
ColumnDefinitions: []metav1.TableColumnDefinition{
{Name: "NAME", Type: "string", Description: "Name of the TenantModule", Priority: 0},
{Name: "READY", Type: "string", Description: "Ready status of the TenantModule", Priority: 0},
{Name: "AGE", Type: "string", Description: "Age of the TenantModule", Priority: 0},
{Name: "VERSION", Type: "string", Description: "Version of the TenantModule", Priority: 0},
},
Rows: []metav1.TableRow{},
}
now := time.Now()
row := metav1.TableRow{
Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)},
Object: runtime.RawExtension{Object: &module},
}
table.Rows = append(table.Rows, row)
return table
}
// getVersion returns the module version or a placeholder if unknown
func getVersion(version string) string {
if version == "" {
return "<unknown>"
}
return version
}
// computeAge calculates the age of the object based on CreationTimestamp and current time
func computeAge(creationTime, currentTime time.Time) string {
ageDuration := currentTime.Sub(creationTime)
return duration.HumanDuration(ageDuration)
}
// getReadyStatus returns the ready status based on conditions
func getReadyStatus(conditions []metav1.Condition) string {
for _, condition := range conditions {
if condition.Type == "Ready" {
switch condition.Status {
case metav1.ConditionTrue:
return "True"
case metav1.ConditionFalse:
return "False"
default:
return "Unknown"
}
}
}
return "Unknown"
}
// Destroy releases resources associated with REST
func (r *REST) Destroy() {
// No additional actions needed to release resources.
}
// New creates a new instance of TenantModule
func (r *REST) New() runtime.Object {
return &corev1alpha1.TenantModule{}
}
// NewList returns an empty list of TenantModule objects
func (r *REST) NewList() runtime.Object {
return &corev1alpha1.TenantModuleList{}
}
// Kind returns the resource kind used for API discovery
func (r *REST) Kind() string {
return r.gvk.Kind
}
// GroupVersionKind returns the GroupVersionKind for REST
func (r *REST) GroupVersionKind(schema.GroupVersion) schema.GroupVersionKind {
return r.gvk
}
// errNotAcceptable indicates that the resource does not support conversion to Table
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(),
}
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,451 @@
// SPDX-License-Identifier: Apache-2.0
// TenantSecret registry namespaced view over Secrets labelled
// "internal.cozystack.io/tenantsecret=true". Internal tenant secret labels are hidden.
package tenantsecret
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"sort"
"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 (
tsLabelKey = "apps.cozystack.io/tenantresource"
tsLabelValue = "true"
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 == tsLabelKey {
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: 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[tsLabelKey] = tsLabelValue
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(tsLabelKey, selection.Equals, []string{tsLabelValue})
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 tenant secret label is preserved
if out.Labels[tsLabelKey] != tsLabelValue {
out.Labels[tsLabelKey] = tsLabelValue
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{tsLabelKey: tsLabelValue}.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,
}
}

View File

@@ -0,0 +1,317 @@
// SPDX-License-Identifier: Apache-2.0
// TenantSecretsTable registry namespaced, read-only flattened view over
// Secrets labelled "internal.cozystack.io/tenantsecret=true". Each data key is a separate object.
package tenantsecretstable
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"sort"
"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/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"
)
const (
tsLabelKey = "apps.cozystack.io/tenantresource"
tsLabelValue = "true"
kindObj = "TenantSecretsTable"
kindObjList = "TenantSecretsTableList"
singularName = "tenantsecretstable"
resourcePlural = "tenantsecretstables"
)
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: resourcePlural,
},
}
}
var (
_ rest.Getter = &REST{}
_ rest.Lister = &REST{}
_ rest.Watcher = &REST{}
_ rest.TableConvertor = &REST{}
_ rest.Scoper = &REST{}
_ rest.SingularNameProvider = &REST{}
_ rest.Storage = &REST{}
)
func (*REST) NamespaceScoped() bool { return true }
func (*REST) New() runtime.Object { return &corev1alpha1.TenantSecretsTable{} }
func (*REST) NewList() runtime.Object {
return &corev1alpha1.TenantSecretsTableList{}
}
func (*REST) Kind() string { return kindObj }
func (r *REST) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
return r.gvr.GroupVersion().WithKind(kindObj)
}
func (*REST) GetSingularName() string { return singularName }
func (*REST) Destroy() {}
func nsFrom(ctx context.Context) (string, error) {
ns, ok := request.NamespaceFrom(ctx)
if !ok {
return "", fmt.Errorf("namespace required")
}
return ns, nil
}
// -----------------------
// Get/List
// -----------------------
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
}
// We need to identify secret name and key. Iterate secrets in namespace with tenant secret label
// and return the matching composed object.
list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{LabelSelector: labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String()})
if err != nil {
return nil, err
}
for i := range list.Items {
sec := &list.Items[i]
for k, v := range sec.Data {
composed := composedName(sec.Name, k)
if composed == name {
return secretKeyToObj(sec, k, v), nil
}
}
}
return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtime.Object, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
sel := labels.NewSelector()
req, _ := labels.NewRequirement(tsLabelKey, selection.Equals, []string{tsLabelValue})
sel = sel.Add(*req)
if opts.LabelSelector != nil {
if reqs, _ := opts.LabelSelector.Requirements(); len(reqs) > 0 {
sel = sel.Add(reqs...)
}
}
fieldSel := ""
if opts.FieldSelector != nil {
fieldSel = opts.FieldSelector.String()
}
list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{LabelSelector: sel.String(), FieldSelector: fieldSel})
if err != nil {
return nil, err
}
out := &corev1alpha1.TenantSecretsTableList{
TypeMeta: metav1.TypeMeta{APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: kindObjList},
ListMeta: list.ListMeta,
}
for i := range list.Items {
sec := &list.Items[i]
// Ensure stable ordering of keys
keys := make([]string, 0, len(sec.Data))
for k := range sec.Data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := sec.Data[k]
o := secretKeyToObj(sec, k, v)
out.Items = append(out.Items, *o)
}
}
sort.Slice(out.Items, func(i, j int) bool { return out.Items[i].Name < out.Items[j].Name })
return out, nil
}
// -----------------------
// Watch
// -----------------------
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{tsLabelKey: tsLabelValue}.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
}
// Emit an event per key
for k, v := range sec.Data {
obj := secretKeyToObj(sec, k, v)
ch <- watch.Event{Type: ev.Type, Object: obj}
}
}
}()
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.TenantSecretsTable) metav1.TableRow {
return metav1.TableRow{
Cells: []interface{}{o.Name, o.Data.Name, o.Data.Key, humanAge(o.CreationTimestamp.Time, now)},
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: "SECRET", Type: "string"},
{Name: "KEY", Type: "string"},
{Name: "AGE", Type: "string"},
},
}
switch v := obj.(type) {
case *corev1alpha1.TenantSecretsTableList:
for i := range v.Items {
tbl.Rows = append(tbl.Rows, row(&v.Items[i]))
}
tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion
case *corev1alpha1.TenantSecretsTable:
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 composedName(secretName, key string) string {
return secretName + "-" + key
}
func humanAge(t time.Time, now time.Time) string {
d := now.Sub(t)
// simple human duration
if d.Hours() >= 24 {
return fmt.Sprintf("%dd", int(d.Hours()/24))
}
if d.Hours() >= 1 {
return fmt.Sprintf("%dh", int(d.Hours()))
}
if d.Minutes() >= 1 {
return fmt.Sprintf("%dm", int(d.Minutes()))
}
return fmt.Sprintf("%ds", int(d.Seconds()))
}
func secretKeyToObj(sec *corev1.Secret, key string, val []byte) *corev1alpha1.TenantSecretsTable {
return &corev1alpha1.TenantSecretsTable{
TypeMeta: metav1.TypeMeta{APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: kindObj},
ObjectMeta: metav1.ObjectMeta{
Name: sec.Name,
Namespace: sec.Namespace,
UID: sec.UID,
ResourceVersion: sec.ResourceVersion,
CreationTimestamp: sec.CreationTimestamp,
Labels: filterUserLabels(sec.Labels),
Annotations: sec.Annotations,
},
Data: corev1alpha1.TenantSecretEntry{
Name: sec.Name,
Key: key,
Value: toBase64String(val),
},
}
}
func filterUserLabels(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 == tsLabelKey {
continue
}
out[k] = v
}
return out
}
func toBase64String(b []byte) string {
const enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
// Minimal base64 encoder to avoid extra deps; for readability we could use stdlib encoding/base64
// but keeping inline is fine; however using stdlib is clearer.
// Using stdlib:
return base64.StdEncoding.EncodeToString(b)
}
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,
}
}

View File

@@ -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 }