Compare commits

...

3 Commits

Author SHA1 Message Date
Andrei Kvapil
8b97d87d90 Refactor and implement TenantSecret
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-08-05 16:34:36 +02:00
Andrei Kvapil
991e0479b9 filter namespaces by checking access to them
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-08-05 16:34:27 +02:00
Andrei Kvapil
7c5152963d [cozystack-api] Implement TenantNamespace resource
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-08-05 16:34:20 +02:00
26 changed files with 1748 additions and 110 deletions

View File

@@ -26,8 +26,8 @@ import (
func main() {
ctx := genericapiserver.SetupSignalContext()
options := server.NewAppsServerOptions(os.Stdout, os.Stderr)
cmd := server.NewCommandStartAppsServer(ctx, options)
options := server.NewCozyServerOptions(os.Stdout, os.Stderr)
cmd := server.NewCommandStartCozyServer(ctx, options)
code := cli.Run(cmd)
os.Exit(code)
}

View File

@@ -39,6 +39,11 @@ rules:
resources:
- workloadmonitors
verbs: ["get", "list", "watch"]
- apiGroups:
- core.cozystack.io
resources:
- tenantsecrets
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
@@ -188,6 +193,11 @@ rules:
resources:
- workloadmonitors
verbs: ["get", "list", "watch"]
- apiGroups:
- core.cozystack.io
resources:
- tenantsecrets
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
@@ -279,6 +289,11 @@ rules:
resources:
- workloadmonitors
verbs: ["get", "list", "watch"]
- apiGroups:
- core.cozystack.io
resources:
- tenantsecrets
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
@@ -346,6 +361,11 @@ rules:
resources:
- workloadmonitors
verbs: ["get", "list", "watch"]
- apiGroups:
- core.cozystack.io
resources:
- tenantsecrets
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -11,3 +11,17 @@ spec:
name: cozystack-api
namespace: cozy-system
version: v1alpha1
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.core.cozystack.io
spec:
insecureSkipTLSVerify: true
group: core.cozystack.io
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: cozystack-api
namespace: cozy-system
version: v1alpha1

View File

@@ -4,7 +4,7 @@ metadata:
name: cozystack-api
rules:
- apiGroups: [""]
resources: ["namespaces"]
resources: ["namespaces", "secrets"]
verbs: ["get", "watch", "list"]
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations", "validatingadmissionpolicies", "validatingadmissionpolicybindings"]

View File

@@ -0,0 +1,26 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tenantnamespaces-read
rules:
- apiGroups:
- core.cozystack.io
resources:
- tenantnamespaces
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tenantnamespaces-read-authenticated
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tenantnamespaces-read
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:authenticated

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

@@ -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,65 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 The Cozystack Authors.
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
)
// -----------------------------------------------------------------------------
// Group / version boiler-plate
// -----------------------------------------------------------------------------
// GroupName is the API group for every resource in this package.
const GroupName = "core.cozystack.io"
// SchemeGroupVersion is the canonical {group,version} for v1alpha1.
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
// -----------------------------------------------------------------------------
// Scheme registration helpers
// -----------------------------------------------------------------------------
var (
// SchemeBuilder is used by generated deepcopy code.
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// Manually-written types go here. Generated deepcopy code is wired in
// via `zz_generated.deepcopy.go`.
localSchemeBuilder.Register(addKnownTypes)
}
// addKnownTypes is called from init().
func addKnownTypes(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
// Resource turns an unqualified resource name into a fully-qualified one.
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
// -----------------------------------------------------------------------------
// Public helpers consumed by the apiserver wiring
// -----------------------------------------------------------------------------
// RegisterStaticTypes adds *compile-time* resources such as TenantNamespace.
func RegisterStaticTypes(scheme *runtime.Scheme) {
scheme.AddKnownTypes(SchemeGroupVersion,
&TenantNamespace{},
&TenantNamespaceList{},
&TenantSecret{},
&TenantSecretList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret")
}

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,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,166 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantNamespace) DeepCopyInto(out *TenantNamespace) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantNamespace.
func (in *TenantNamespace) DeepCopy() *TenantNamespace {
if in == nil {
return nil
}
out := new(TenantNamespace)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantNamespace) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantNamespaceList) DeepCopyInto(out *TenantNamespaceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantNamespace, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantNamespaceList.
func (in *TenantNamespaceList) DeepCopy() *TenantNamespaceList {
if in == nil {
return nil
}
out := new(TenantNamespaceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantNamespaceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecret) DeepCopyInto(out *TenantSecret) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make(map[string][]byte, len(*in))
for key, val := range *in {
var outVal []byte
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]byte, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
if in.StringData != nil {
in, out := &in.StringData, &out.StringData
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecret.
func (in *TenantSecret) DeepCopy() *TenantSecret {
if in == nil {
return nil
}
out := new(TenantSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantSecret) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecretList) DeepCopyInto(out *TenantSecretList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantSecret, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretList.
func (in *TenantSecretList) DeepCopy() *TenantSecretList {
if in == nil {
return nil
}
out := new(TenantSecretList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantSecretList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

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,18 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"github.com/cozystack/cozystack/pkg/apis/apps"
"github.com/cozystack/cozystack/pkg/apis/apps/install"
appsinstall "github.com/cozystack/cozystack/pkg/apis/apps/install"
coreinstall "github.com/cozystack/cozystack/pkg/apis/apps/install"
"github.com/cozystack/cozystack/pkg/apis/core"
"github.com/cozystack/cozystack/pkg/config"
appsregistry "github.com/cozystack/cozystack/pkg/registry"
cozyregistry "github.com/cozystack/cozystack/pkg/registry"
applicationstorage "github.com/cozystack/cozystack/pkg/registry/apps/application"
tenantnamespacestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantnamespace"
tenantsecretstorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecret"
)
var (
@@ -42,11 +47,12 @@ var (
// Codecs provides methods for retrieving codecs and serializers for specific
// versions and content types.
Codecs = serializer.NewCodecFactory(Scheme)
AppsComponentName = "apps"
CozyComponentName = "cozy"
)
func init() {
install.Install(Scheme)
appsinstall.Install(Scheme)
coreinstall.Install(Scheme)
// Register HelmRelease types.
if err := helmv2.AddToScheme(Scheme); err != nil {
@@ -73,8 +79,8 @@ type Config struct {
ResourceConfig *config.ResourceConfig
}
// AppsServer holds the state for the Kubernetes master/api server.
type AppsServer struct {
// CozyServer holds the state for the Kubernetes master/api server.
type CozyServer struct {
GenericAPIServer *genericapiserver.GenericAPIServer
}
@@ -98,19 +104,17 @@ func (cfg *Config) Complete() CompletedConfig {
return CompletedConfig{&c}
}
// New returns a new instance of AppsServer from the given configuration.
func (c completedConfig) New() (*AppsServer, error) {
genericServer, err := c.GenericConfig.New("apps-apiserver", genericapiserver.NewEmptyDelegate())
// New returns a new instance of CozyServer from the given configuration.
func (c completedConfig) New() (*CozyServer, error) {
genericServer, err := c.GenericConfig.New("cozy-apiserver", genericapiserver.NewEmptyDelegate())
if err != nil {
return nil, err
}
s := &AppsServer{
s := &CozyServer{
GenericAPIServer: genericServer,
}
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs)
// Create a dynamic client for HelmRelease using InClusterConfig.
inClusterConfig, err := restclient.InClusterConfig()
if err != nil {
@@ -122,16 +126,41 @@ func (c completedConfig) New() (*AppsServer, error) {
return nil, fmt.Errorf("unable to create dynamic client: %v", err)
}
v1alpha1storage := map[string]rest.Storage{}
for _, resConfig := range c.ResourceConfig.Resources {
storage := applicationstorage.NewREST(dynamicClient, &resConfig)
v1alpha1storage[resConfig.Application.Plural] = appsregistry.RESTInPeace(storage)
clientset, err := kubernetes.NewForConfig(inClusterConfig)
if err != nil {
return nil, fmt.Errorf("create kube clientset: %v", err)
}
apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage
// --- static, cluster-scoped resource for core group ---
coreV1alpha1Storage := map[string]rest.Storage{}
coreV1alpha1Storage["tenantnamespaces"] = cozyregistry.RESTInPeace(
tenantnamespacestorage.NewREST(
clientset.CoreV1(),
clientset.AuthorizationV1(),
20,
),
)
coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace(
tenantsecretstorage.NewREST(
clientset.CoreV1(),
),
)
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
coreApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(core.GroupName, Scheme, metav1.ParameterCodec, Codecs)
coreApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = coreV1alpha1Storage
if err := s.GenericAPIServer.InstallAPIGroup(&coreApiGroupInfo); err != nil {
return nil, err
}
// --- dynamically-configured, per-tenant resources ---
appsV1alpha1Storage := map[string]rest.Storage{}
for _, resConfig := range c.ResourceConfig.Resources {
storage := applicationstorage.NewREST(dynamicClient, &resConfig)
appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage)
}
appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs)
appsApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = appsV1alpha1Storage
if err := s.GenericAPIServer.InstallAPIGroup(&appsApiGroupInfo); err != nil {
return nil, err
}

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,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1.Application": schema_pkg_apis_apps_v1alpha1_Application(ref),
"github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1.ApplicationList": schema_pkg_apis_apps_v1alpha1_ApplicationList(ref),
"github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus": schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace": schema_pkg_apis_core_v1alpha1_TenantNamespace(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespaceList": schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret": schema_pkg_apis_core_v1alpha1_TenantSecret(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretList": schema_pkg_apis_core_v1alpha1_TenantSecretList(ref),
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionRequest": schema_pkg_apis_apiextensions_v1_ConversionRequest(ref),
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionResponse": schema_pkg_apis_apiextensions_v1_ConversionResponse(ref),
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionReview": schema_pkg_apis_apiextensions_v1_ConversionReview(ref),
@@ -252,6 +256,208 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref common.ReferenceCallbac
}
}
func schema_pkg_apis_core_v1alpha1_TenantNamespace(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantNamespace is a thin wrapper around ObjectMeta. It has no spec/status because it merely reflects an existing Namespace object.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantNamespaceList is the list variant for TenantNamespace.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecret(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"type": {
SchemaProps: spec.SchemaProps{
Description: "Same semantics as core/v1 Secret.",
Type: []string{"string"},
Format: "",
},
},
"data": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "byte",
},
},
},
},
},
"stringData": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecretList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_apiextensions_v1_ConversionRequest(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

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,456 @@
// SPDX-License-Identifier: Apache-2.0
// TenantSecret registry namespaced view over Secrets labelled
// “cozystack.io/ui=true”. Internal labels/annotations are hidden.
package tenantsecret
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"sort"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
)
// -----------------------------------------------------------------------------
// Constants & helpers
// -----------------------------------------------------------------------------
const (
uiLabelKey = "cozystack.io/ui"
uiLabelValue = "true"
systemLabelPrefix = "internal.cozystack.io/"
systemAnnotPrefix = "internal.cozystack.io/"
singularName = "tenantsecret"
kindTenantSecret = "TenantSecret"
kindTenantSecretList = "TenantSecretList"
)
func stripInternal(m map[string]string) map[string]string {
if m == nil {
return nil
}
out := make(map[string]string, len(m))
for k, v := range m {
if k == uiLabelKey ||
strings.HasPrefix(k, systemLabelPrefix) ||
strings.HasPrefix(k, systemAnnotPrefix) {
continue
}
out[k] = v
}
return out
}
func encodeStringData(sd map[string]string) map[string][]byte {
if len(sd) == 0 {
return nil
}
out := make(map[string][]byte, len(sd))
for k, v := range sd {
out[k] = []byte(v)
}
return out
}
func decodeStringData(d map[string][]byte) map[string]string {
if len(d) == 0 {
return nil
}
out := make(map[string]string, len(d))
for k, v := range d {
out[k] = base64.StdEncoding.EncodeToString(v)
}
return out
}
func secretToTenant(sec *corev1.Secret) *corev1alpha1.TenantSecret {
return &corev1alpha1.TenantSecret{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1alpha1.SchemeGroupVersion.String(),
Kind: kindTenantSecret,
},
ObjectMeta: metav1.ObjectMeta{
Name: sec.Name,
Namespace: sec.Namespace,
UID: sec.UID,
ResourceVersion: sec.ResourceVersion,
CreationTimestamp: sec.CreationTimestamp,
Labels: stripInternal(sec.Labels),
Annotations: stripInternal(sec.Annotations),
},
Type: string(sec.Type),
Data: sec.Data,
StringData: decodeStringData(sec.Data),
}
}
func tenantToSecret(ts *corev1alpha1.TenantSecret, cur *corev1.Secret) *corev1.Secret {
var out corev1.Secret
if cur != nil {
out = *cur.DeepCopy()
}
out.TypeMeta = metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}
out.Name, out.Namespace = ts.Name, ts.Namespace
if out.Labels == nil {
out.Labels = map[string]string{}
}
out.Labels[uiLabelKey] = uiLabelValue
for k, v := range ts.Labels {
out.Labels[k] = v
}
if out.Annotations == nil {
out.Annotations = map[string]string{}
}
for k, v := range ts.Annotations {
out.Annotations[k] = v
}
if len(ts.Data) != 0 {
out.Data = ts.Data
} else if len(ts.StringData) != 0 {
out.Data = encodeStringData(ts.StringData)
}
out.Type = corev1.SecretType(ts.Type)
return &out
}
func nsFrom(ctx context.Context) (string, error) {
ns, ok := request.NamespaceFrom(ctx)
if !ok {
return "", apierrors.NewBadRequest("namespace required")
}
return ns, nil
}
// -----------------------------------------------------------------------------
// REST storage
// -----------------------------------------------------------------------------
var (
_ rest.Creater = &REST{}
_ rest.Getter = &REST{}
_ rest.Lister = &REST{}
_ rest.Updater = &REST{}
_ rest.Patcher = &REST{}
_ rest.GracefulDeleter = &REST{}
_ rest.Watcher = &REST{}
_ rest.TableConvertor = &REST{}
_ rest.Scoper = &REST{}
_ rest.SingularNameProvider = &REST{}
)
type REST struct {
core corev1client.CoreV1Interface
gvr schema.GroupVersionResource
}
func NewREST(coreCli corev1client.CoreV1Interface) *REST {
return &REST{
core: coreCli,
gvr: schema.GroupVersionResource{
Group: corev1alpha1.GroupName,
Version: "v1alpha1",
Resource: "tenantsecrets",
},
}
}
// -----------------------------------------------------------------------------
// Basic meta
// -----------------------------------------------------------------------------
func (*REST) NamespaceScoped() bool { return true }
func (*REST) New() runtime.Object { return &corev1alpha1.TenantSecret{} }
func (*REST) NewList() runtime.Object {
return &corev1alpha1.TenantSecretList{}
}
func (*REST) Kind() string { return kindTenantSecret }
func (r *REST) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
return r.gvr.GroupVersion().WithKind(kindTenantSecret)
}
func (*REST) GetSingularName() string { return singularName }
// -----------------------------------------------------------------------------
// CRUD
// -----------------------------------------------------------------------------
func (r *REST) Create(
ctx context.Context,
obj runtime.Object,
_ rest.ValidateObjectFunc,
opts *metav1.CreateOptions,
) (runtime.Object, error) {
in, ok := obj.(*corev1alpha1.TenantSecret)
if !ok {
return nil, fmt.Errorf("expected TenantSecret, got %T", obj)
}
sec := tenantToSecret(in, nil)
out, err := r.core.Secrets(sec.Namespace).Create(ctx, sec, *opts)
if err != nil {
return nil, err
}
return secretToTenant(out), nil
}
func (r *REST) Get(
ctx context.Context,
name string,
opts *metav1.GetOptions,
) (runtime.Object, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
sec, err := r.core.Secrets(ns).Get(ctx, name, *opts)
if err != nil {
return nil, err
}
return secretToTenant(sec), nil
}
func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtime.Object, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
ls := labels.NewSelector()
req, _ := labels.NewRequirement(uiLabelKey, selection.Equals, []string{uiLabelValue})
ls = ls.Add(*req)
if opts.LabelSelector != nil {
if reqs, _ := opts.LabelSelector.Requirements(); len(reqs) > 0 {
ls = ls.Add(reqs...)
}
}
fieldSel := ""
if opts.FieldSelector != nil {
fieldSel = opts.FieldSelector.String()
}
list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{
LabelSelector: ls.String(),
FieldSelector: fieldSel,
})
if err != nil {
return nil, err
}
out := &corev1alpha1.TenantSecretList{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1alpha1.SchemeGroupVersion.String(),
Kind: kindTenantSecretList,
},
ListMeta: list.ListMeta,
}
for i := range list.Items {
out.Items = append(out.Items, *secretToTenant(&list.Items[i]))
}
sort.Slice(out.Items, func(i, j int) bool { return out.Items[i].Name < out.Items[j].Name })
return out, nil
}
func (r *REST) Update(
ctx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
_ rest.ValidateObjectFunc,
_ rest.ValidateObjectUpdateFunc,
forceCreate bool,
opts *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, false, err
}
cur, err := r.core.Secrets(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return nil, false, err
}
newObj, err := objInfo.UpdatedObject(ctx, nil)
if err != nil {
return nil, false, err
}
in := newObj.(*corev1alpha1.TenantSecret)
newSec := tenantToSecret(in, cur)
if cur == nil {
if !forceCreate && err == nil {
return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
out, err := r.core.Secrets(ns).Create(ctx, newSec, metav1.CreateOptions{})
return secretToTenant(out), true, err
}
newSec.ResourceVersion = cur.ResourceVersion
out, err := r.core.Secrets(ns).Update(ctx, newSec, *opts)
return secretToTenant(out), false, err
}
func (r *REST) Delete(
ctx context.Context,
name string,
_ rest.ValidateObjectFunc,
opts *metav1.DeleteOptions,
) (runtime.Object, bool, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, false, err
}
err = r.core.Secrets(ns).Delete(ctx, name, *opts)
return nil, err == nil, err
}
func (r *REST) Patch(
ctx context.Context,
name string,
pt types.PatchType,
data []byte,
opts *metav1.PatchOptions,
subresources ...string,
) (runtime.Object, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
out, err := r.core.Secrets(ns).
Patch(ctx, name, pt, data, *opts, subresources...)
if err != nil {
return nil, err
}
// Ensure UI label is preserved
if out.Labels[uiLabelKey] != uiLabelValue {
out.Labels[uiLabelKey] = uiLabelValue
out, _ = r.core.Secrets(ns).Update(ctx, out, metav1.UpdateOptions{})
}
return secretToTenant(out), nil
}
// -----------------------------------------------------------------------------
// Watcher
// -----------------------------------------------------------------------------
func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
ls := labels.Set{uiLabelKey: uiLabelValue}.AsSelector().String()
base, err := r.core.Secrets(ns).Watch(ctx, metav1.ListOptions{
Watch: true,
LabelSelector: ls,
ResourceVersion: opts.ResourceVersion,
})
if err != nil {
return nil, err
}
ch := make(chan watch.Event)
proxy := watch.NewProxyWatcher(ch)
go func() {
defer proxy.Stop()
for ev := range base.ResultChan() {
sec, ok := ev.Object.(*corev1.Secret)
if !ok || sec == nil {
continue
}
tenant := secretToTenant(sec)
ch <- watch.Event{
Type: ev.Type,
Object: tenant,
}
}
}()
return proxy, nil
}
// -----------------------------------------------------------------------------
// TableConvertor
// -----------------------------------------------------------------------------
func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) {
now := time.Now()
row := func(o *corev1alpha1.TenantSecret) metav1.TableRow {
return metav1.TableRow{
Cells: []interface{}{o.Name, o.Type, duration.HumanDuration(now.Sub(o.CreationTimestamp.Time))},
Object: runtime.RawExtension{Object: o},
}
}
tbl := &metav1.Table{
TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"},
ColumnDefinitions: []metav1.TableColumnDefinition{
{Name: "NAME", Type: "string"},
{Name: "TYPE", Type: "string"},
{Name: "AGE", Type: "string"},
},
}
switch v := obj.(type) {
case *corev1alpha1.TenantSecretList:
for i := range v.Items {
tbl.Rows = append(tbl.Rows, row(&v.Items[i]))
}
tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion
case *corev1alpha1.TenantSecret:
tbl.Rows = append(tbl.Rows, row(v))
tbl.ListMeta.ResourceVersion = v.ResourceVersion
default:
return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)}
}
return tbl, nil
}
// -----------------------------------------------------------------------------
// Boiler-plate
// -----------------------------------------------------------------------------
func (*REST) Destroy() {}
type notAcceptable struct {
resource schema.GroupResource
message string
}
func (e notAcceptable) Error() string { return e.message }
func (e notAcceptable) Status() metav1.Status {
return metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotAcceptable,
Reason: metav1.StatusReason("NotAcceptable"),
Message: e.message,
}
}

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 }