mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-10 06:24:09 +09:00
- Add resourceVersion handling for Watch requests by filtering ADDED events based on the resourceVersion provided by the client - Forward bookmark events from underlying HelmRelease watchers to clients for proper resourceVersion synchronization - Extract MaxResourceVersion helper using meta.EachListItem for cleaner code - This ensures clients don't receive duplicate objects they already have from List+Watch patterns Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
814 lines
25 KiB
Go
814 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"
|
|
"strconv"
|
|
"strings"
|
|
"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"
|
|
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"
|
|
"github.com/cozystack/cozystack/pkg/registry"
|
|
fieldfilter "github.com/cozystack/cozystack/pkg/registry/fields"
|
|
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
|
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"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s", name, r.gvr.Resource)
|
|
return &convertedModule, 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 helmLabelSelector labels.Selector
|
|
|
|
// Parse field selector for manual filtering
|
|
// controller-runtime cache doesn't support field selectors
|
|
// See: https://github.com/kubernetes-sigs/controller-runtime/issues/612
|
|
fieldFilter, err := fieldfilter.ParseFieldSelector(options.FieldSelector)
|
|
if err != nil {
|
|
klog.Errorf("Error parsing field selector: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// If field selector specifies namespace different from context, return empty list
|
|
if fieldFilter.Namespace != "" && namespace != "" && namespace != fieldFilter.Namespace {
|
|
klog.V(6).Infof("Field selector namespace %s doesn't match context namespace %s, returning empty list", fieldFilter.Namespace, namespace)
|
|
return &corev1alpha1.TenantModuleList{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: corev1alpha1.SchemeGroupVersion.String(),
|
|
Kind: "TenantModuleList",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// 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...)
|
|
|
|
// List HelmReleases with label selector only
|
|
// Field selectors are not supported by controller-runtime cache
|
|
// See: https://github.com/kubernetes-sigs/controller-runtime/issues/612
|
|
hrList := &helmv2.HelmReleaseList{}
|
|
err = r.c.List(ctx, hrList, &client.ListOptions{
|
|
Namespace: namespace,
|
|
LabelSelector: helmLabelSelector,
|
|
})
|
|
if err != nil {
|
|
klog.Errorf("Error listing HelmReleases: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize TenantModule items array
|
|
items := make([]corev1alpha1.TenantModule, 0, len(hrList.Items))
|
|
|
|
// Iterate over HelmReleases and convert to TenantModules
|
|
for i := range hrList.Items {
|
|
// Apply manual field selector filtering (metadata.name and metadata.namespace)
|
|
// controller-runtime cache doesn't support field selectors
|
|
// See: https://github.com/kubernetes-sigs/controller-runtime/issues/612
|
|
if !fieldFilter.MatchesName(hrList.Items[i].Name) {
|
|
continue
|
|
}
|
|
if !fieldFilter.MatchesNamespace(hrList.Items[i].Namespace) {
|
|
continue
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
items = append(items, module)
|
|
}
|
|
|
|
// Create TenantModuleList with proper kind
|
|
moduleList := &corev1alpha1.TenantModuleList{}
|
|
moduleList.TypeMeta = metav1.TypeMeta{
|
|
APIVersion: "core.cozystack.io/v1alpha1",
|
|
Kind: r.kindName + "List",
|
|
}
|
|
|
|
// Get ResourceVersion from list or compute from items
|
|
// controller-runtime cached client may not set ResourceVersion on the list itself
|
|
listRV := hrList.GetResourceVersion()
|
|
if listRV == "" {
|
|
listRV, _ = registry.MaxResourceVersion(hrList)
|
|
}
|
|
moduleList.SetResourceVersion(listRV)
|
|
moduleList.Items = items
|
|
|
|
sorting.ByNamespacedName[corev1alpha1.TenantModule, *corev1alpha1.TenantModule](moduleList.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 helmLabelSelector labels.Selector
|
|
|
|
// Parse field selector for manual filtering
|
|
// controller-runtime cache doesn't support field selectors
|
|
// See: https://github.com/kubernetes-sigs/controller-runtime/issues/612
|
|
fieldFilter, err := fieldfilter.ParseFieldSelector(options.FieldSelector)
|
|
if err != nil {
|
|
klog.Errorf("Error parsing field selector: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// 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...)
|
|
|
|
// Get starting resourceVersion from options
|
|
var startingRV uint64
|
|
if options.ResourceVersion != "" {
|
|
if rv, err := strconv.ParseUint(options.ResourceVersion, 10, 64); err == nil {
|
|
startingRV = rv
|
|
}
|
|
}
|
|
|
|
// Start watch on HelmRelease with label selector only
|
|
// Field selectors are not supported by controller-runtime cache
|
|
// See: https://github.com/kubernetes-sigs/controller-runtime/issues/612
|
|
hrList := &helmv2.HelmReleaseList{}
|
|
helmWatcher, err := r.w.Watch(ctx, hrList, &client.ListOptions{
|
|
Namespace: namespace,
|
|
LabelSelector: helmLabelSelector,
|
|
})
|
|
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 {
|
|
klog.Warning("HelmRelease watcher closed")
|
|
return
|
|
}
|
|
|
|
// Handle bookmark events
|
|
if event.Type == watch.Bookmark {
|
|
if hr, ok := event.Object.(*helmv2.HelmRelease); ok {
|
|
bookmarkModule := &corev1alpha1.TenantModule{}
|
|
bookmarkModule.SetResourceVersion(hr.GetResourceVersion())
|
|
bookmarkModule.TypeMeta = metav1.TypeMeta{
|
|
APIVersion: corev1alpha1.SchemeGroupVersion.String(),
|
|
Kind: r.kindName,
|
|
}
|
|
bookmarkEvent := watch.Event{
|
|
Type: watch.Bookmark,
|
|
Object: bookmarkModule,
|
|
}
|
|
select {
|
|
case customW.resultChan <- bookmarkEvent:
|
|
case <-customW.stopChan:
|
|
return
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Apply manual field selector filtering
|
|
if !fieldFilter.MatchesName(hr.Name) {
|
|
continue
|
|
}
|
|
if !fieldFilter.MatchesNamespace(hr.Namespace) {
|
|
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
|
|
}
|
|
|
|
// Skip ADDED events based on resourceVersion comparison
|
|
// Only skip when client provided resourceVersion (they already have objects from List)
|
|
if event.Type == watch.Added && startingRV > 0 {
|
|
objRV, parseErr := strconv.ParseUint(module.ResourceVersion, 10, 64)
|
|
// Skip objects client already has (objRV <= startingRV)
|
|
if parseErr == nil && objRV <= startingRV {
|
|
continue
|
|
}
|
|
}
|
|
// When startingRV == 0, always send ADDED events (client wants full state)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Create watch event with TenantModule object
|
|
moduleEvent := watch.Event{
|
|
Type: event.Type,
|
|
Object: &module,
|
|
}
|
|
|
|
// 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.Error(err)
|
|
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 *corev1alpha1.TenantModuleList:
|
|
table = r.buildTableFromTenantModules(obj.Items)
|
|
table.ResourceVersion = obj.ResourceVersion
|
|
case *corev1alpha1.TenantModule:
|
|
table = r.buildTableFromTenantModule(*obj)
|
|
table.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 i := range modules {
|
|
module := &modules[i]
|
|
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()
|
|
|
|
m := module
|
|
row := metav1.TableRow{
|
|
Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)},
|
|
Object: runtime.RawExtension{Object: &m},
|
|
}
|
|
table.Rows = append(table.Rows, row)
|
|
|
|
return table
|
|
}
|
|
|
|
// getVersion extracts and returns only the revision from the version string
|
|
// If version is in format "0.1.4+abcdef", returns "abcdef"
|
|
// Otherwise returns the original string or "<unknown>" if empty
|
|
func getVersion(version string) string {
|
|
if version == "" {
|
|
return "<unknown>"
|
|
}
|
|
// Check if version contains "+" separator
|
|
if idx := strings.LastIndex(version, "+"); idx >= 0 && idx < len(version)-1 {
|
|
// Return only the part after "+"
|
|
return version[idx+1:]
|
|
}
|
|
// If no "+" found, return original version
|
|
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 {
|
|
obj := &corev1alpha1.TenantModule{}
|
|
obj.TypeMeta = metav1.TypeMeta{
|
|
APIVersion: r.gvk.GroupVersion().String(),
|
|
Kind: r.kindName,
|
|
}
|
|
return obj
|
|
}
|
|
|
|
// NewList returns an empty list of TenantModule objects
|
|
func (r *REST) NewList() runtime.Object {
|
|
obj := &corev1alpha1.TenantModuleList{}
|
|
obj.TypeMeta = metav1.TypeMeta{
|
|
APIVersion: r.gvk.GroupVersion().String(),
|
|
Kind: r.kindName + "List",
|
|
}
|
|
return obj
|
|
}
|
|
|
|
// 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(),
|
|
}
|
|
}
|