mirror of
https://github.com/outbackdingo/cozystack.git
synced 2026-02-05 00:15:51 +00:00
This patch changes all clients in the Cozystack API server to typed ones from the controller runtime. This should improve the performance of the API server and simplifies the code by removing work with unstructured objects and dynamic clients. ```release-note [api] Use typed and cache-backed k8s clients in the Cozystack API to improve performance. Get rid of operations on unstructured objects and use of dynamic clients. ``` Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
790 lines
25 KiB
Go
790 lines
25 KiB
Go
/*
|
|
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/types"
|
|
"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/klog/v2"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
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 = "internal.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 {
|
|
c client.Client
|
|
w client.WithWatch
|
|
gvr schema.GroupVersionResource
|
|
gvk schema.GroupVersionKind
|
|
kindName string
|
|
singularName string
|
|
}
|
|
|
|
// NewREST creates a new REST storage for TenantModule
|
|
func NewREST(c client.Client, w client.WithWatch) *REST {
|
|
return &REST{
|
|
c: c,
|
|
w: w,
|
|
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 := &helmv2.HelmRelease{}
|
|
err = r.c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, hr, &client.GetOptions{Raw: 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 := &helmv2.HelmReleaseList{}
|
|
err = r.c.List(ctx, hrList, &client.ListOptions{
|
|
Namespace: namespace,
|
|
Raw: &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 i := range hrList.Items {
|
|
// Double-check the label requirement
|
|
if !r.hasTenantModuleLabel(&hrList.Items[i]) {
|
|
continue
|
|
}
|
|
|
|
module, err := r.ConvertHelmReleaseToTenantModule(&hrList.Items[i])
|
|
if err != nil {
|
|
klog.Errorf("Error converting HelmRelease %s to TenantModule: %v", hrList.Items[i].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
|
|
hrList := &helmv2.HelmReleaseList{}
|
|
helmWatcher, err := r.w.Watch(ctx, hrList, &client.ListOptions{
|
|
Namespace: namespace,
|
|
Raw: &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{}),
|
|
underlying: helmWatcher,
|
|
}
|
|
|
|
go func() {
|
|
defer close(customW.resultChan)
|
|
defer customW.underlying.Stop()
|
|
for {
|
|
select {
|
|
case event, ok := <-customW.underlying.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 HelmRelease objects
|
|
hr, ok := event.Object.(*helmv2.HelmRelease)
|
|
if !ok {
|
|
klog.V(4).Infof("Expected HelmRelease object, got %T", event.Object)
|
|
continue
|
|
}
|
|
|
|
if !r.hasTenantModuleLabel(hr) {
|
|
continue
|
|
}
|
|
|
|
// Convert HelmRelease to TenantModule
|
|
module, err := r.ConvertHelmReleaseToTenantModule(hr)
|
|
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
|
|
underlying watch.Interface
|
|
}
|
|
|
|
// Stop terminates the watch
|
|
func (cw *customWatcher) Stop() {
|
|
cw.stopOnce.Do(func() {
|
|
close(cw.stopChan)
|
|
if cw.underlying != nil {
|
|
cw.underlying.Stop()
|
|
}
|
|
})
|
|
}
|
|
|
|
// ResultChan returns the event channel
|
|
func (cw *customWatcher) ResultChan() <-chan watch.Event {
|
|
return cw.resultChan
|
|
}
|
|
|
|
// hasTenantModuleLabel checks if a HelmRelease has the required tenant module label
|
|
func (r *REST) hasTenantModuleLabel(hr *helmv2.HelmRelease) 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 *helmv2.HelmRelease) (corev1alpha1.TenantModule, error) {
|
|
klog.V(6).Infof("Converting HelmRelease to TenantModule for resource %s", hr.GetName())
|
|
|
|
// Convert HelmRelease struct to TenantModule struct
|
|
module, err := r.convertHelmReleaseToTenantModule(hr)
|
|
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(),
|
|
}
|
|
}
|