diff --git a/Makefile b/Makefile index c941a9ff..bb5eb40e 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ build: build-deps make -C packages/extra/monitoring image make -C packages/system/cozystack-api image make -C packages/system/cozystack-controller image + make -C packages/system/lineage-controller-webhook image make -C packages/system/cilium image make -C packages/system/kubeovn image make -C packages/system/kubeovn-webhook image diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index f774284a..fc18a778 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -39,7 +39,6 @@ import ( cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" "github.com/cozystack/cozystack/internal/controller" "github.com/cozystack/cozystack/internal/controller/dashboard" - lcw "github.com/cozystack/cozystack/internal/lineagecontrollerwebhook" "github.com/cozystack/cozystack/internal/telemetry" helmv2 "github.com/fluxcd/helm-controller/api/v2" @@ -70,6 +69,7 @@ func main() { var telemetryEndpoint string var telemetryInterval string var cozystackVersion string + var reconcileDeployment bool var tlsOpts []func(*tls.Config) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") @@ -89,6 +89,8 @@ func main() { "Interval between telemetry data collection (e.g. 15m, 1h)") flag.StringVar(&cozystackVersion, "cozystack-version", "unknown", "Version of Cozystack") + flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false, + "If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.") opts := zap.Options{ Development: false, } @@ -214,25 +216,25 @@ func main() { os.Exit(1) } + cozyAPIKind := "DaemonSet" + if reconcileDeployment { + cozyAPIKind = "Deployment" + } if err = (&controller.CozystackResourceDefinitionReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CozystackAPIKind: cozyAPIKind, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler") os.Exit(1) } - // special one that's both a webhook and a reconciler - lineageControllerWebhook := &lcw.LineageControllerWebhook{ + dashboardManager := &dashboard.Manager{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), } - if err := lineageControllerWebhook.SetupWithManagerAsController(mgr); err != nil { - setupLog.Error(err, "unable to setup controller", "controller", "LineageController") - os.Exit(1) - } - if err := lineageControllerWebhook.SetupWithManagerAsWebhook(mgr); err != nil { - setupLog.Error(err, "unable to setup webhook", "webhook", "LineageWebhook") + if err = dashboardManager.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DashboardReconciler") os.Exit(1) } @@ -261,7 +263,9 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + ctx := ctrl.SetupSignalHandler() + dashboardManager.InitializeStaticResources(ctx) + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } diff --git a/cmd/lineage-controller-webhook/main.go b/cmd/lineage-controller-webhook/main.go new file mode 100644 index 00000000..ffe0942b --- /dev/null +++ b/cmd/lineage-controller-webhook/main.go @@ -0,0 +1,179 @@ +/* +Copyright 2025. + +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 main + +import ( + "crypto/tls" + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + lcw "github.com/cozystack/cozystack/internal/lineagecontrollerwebhook" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(cozystackiov1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + opts := zap.Options{ + Development: false, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: tlsOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + + // TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + } + + // Configure rate limiting for the Kubernetes client + config := ctrl.GetConfigOrDie() + config.QPS = 50.0 // Increased from default 5.0 + config.Burst = 100 // Increased from default 10 + + mgr, err := ctrl.NewManager(config, ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "8796f12d.cozystack.io", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + lineageControllerWebhook := &lcw.LineageControllerWebhook{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + if err := lineageControllerWebhook.SetupWithManagerAsController(mgr); err != nil { + setupLog.Error(err, "unable to setup controller", "controller", "LineageController") + os.Exit(1) + } + if err := lineageControllerWebhook.SetupWithManagerAsWebhook(mgr); err != nil { + setupLog.Error(err, "unable to setup webhook", "webhook", "LineageWebhook") + os.Exit(1) + } + + // +kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/docs/changelogs/unreleased.md b/docs/changelogs/unreleased.md new file mode 100644 index 00000000..1ea38313 --- /dev/null +++ b/docs/changelogs/unreleased.md @@ -0,0 +1,3 @@ +# Changes after v0.37.0 + +* [lineage] Break webhook out into a separate daemonset. Reduce unnecessary webhook calls by marking handled resources and excluding them from consideration by the webhook's object selector (@lllamnyp in #1515). diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index 7c7f347a..c9a54232 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -64,19 +64,19 @@ spec: EOF # Wait for the tenant-test namespace to be active kubectl wait namespace tenant-test --timeout=20s --for=jsonpath='{.status.phase}'=Active - + # Wait for the Kamaji control plane to be created (retry for up to 10 seconds) timeout 10 sh -ec 'until kubectl get kamajicontrolplane -n tenant-test kubernetes-'"${test_name}"'; do sleep 1; done' # Wait for the tenant control plane to be fully created (timeout after 4 minutes) kubectl wait --for=condition=TenantControlPlaneCreated kamajicontrolplane -n tenant-test kubernetes-${test_name} --timeout=4m - + # Wait for Kubernetes resources to be ready (timeout after 2 minutes) - kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=2m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready - + kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=5m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready + # Wait for all required deployments to be available (timeout after 4 minutes) kubectl wait deploy --timeout=4m --for=condition=available -n tenant-test kubernetes-${test_name} kubernetes-${test_name}-cluster-autoscaler kubernetes-${test_name}-kccm kubernetes-${test_name}-kcsi-controller - + # Wait for the machine deployment to scale to 2 replicas (timeout after 1 minute) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=1m --for=jsonpath='{.status.replicas}'=2 # Get the admin kubeconfig and save it to a file @@ -87,14 +87,14 @@ EOF # Set up port forwarding to the Kubernetes API server for a 200 second timeout - bash -c 'timeout 200s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &' + bash -c 'timeout 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &' # Verify the Kubernetes version matches what we expect (retry for up to 20 seconds) timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done' # Wait for the nodes to be ready (timeout after 2 minutes) - timeout 2m bash -c ' + timeout 3m bash -c ' until [ "$(kubectl --kubeconfig tenantkubeconfig get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do - sleep 3 + sleep 2 done ' # Verify the nodes are ready @@ -105,9 +105,11 @@ EOF versions=$(kubectl --kubeconfig tenantkubeconfig get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}') node_ok=true - if [[ "$k8s_version" == v1.32* ]]; then - echo "⚠️ TODO: Temporary stub — allowing nodes with v1.33 while k8s_version is v1.32" - fi + case "$k8s_version" in + v1.32*) + echo "⚠️ TODO: Temporary stub — allowing nodes with v1.33 while k8s_version is v1.32" + ;; + esac for v in $versions; do case "$k8s_version" in @@ -134,11 +136,105 @@ EOF esac done - if ! $node_ok; then + if [ "$node_ok" != true ]; then echo "Kubelet versions did not match expected ${k8s_version}" >&2 exit 1 fi + + kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <&2 + exit 1 +fi + + for i in $(seq 1 20); do + echo "Attempt $i" + curl --silent --fail "http://${LB_ADDR}" && break + sleep 3 + done + + if [ "$i" -eq 20 ]; then + echo "LoadBalancer not reachable" >&2 + exit 1 + fi + + # Cleanup + kubectl delete deployment --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test + kubectl delete service --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test + # Wait for all machine deployment replicas to be ready (timeout after 10 minutes) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2 diff --git a/hack/e2e-install-cozystack.bats b/hack/e2e-install-cozystack.bats index 50b6a071..da132132 100644 --- a/hack/e2e-install-cozystack.bats +++ b/hack/e2e-install-cozystack.bats @@ -118,7 +118,7 @@ EOF } @test "Check Cozystack API service" { - kubectl wait --for=condition=Available apiservices/v1alpha1.apps.cozystack.io --timeout=2m + kubectl wait --for=condition=Available apiservices/v1alpha1.apps.cozystack.io apiservices/v1alpha1.core.cozystack.io --timeout=2m } @test "Configure Tenant and wait for applications" { diff --git a/hack/e2e-prepare-cluster.bats b/hack/e2e-prepare-cluster.bats index 38620532..650ca643 100644 --- a/hack/e2e-prepare-cluster.bats +++ b/hack/e2e-prepare-cluster.bats @@ -132,7 +132,6 @@ machine: - usermode_helper=disabled - name: zfs - name: spl - - name: lldpd registries: mirrors: docker.io: diff --git a/hack/e2e-test-openapi.bats b/hack/e2e-test-openapi.bats index 7e3cc992..aaddbe84 100644 --- a/hack/e2e-test-openapi.bats +++ b/hack/e2e-test-openapi.bats @@ -9,6 +9,7 @@ @test "Test OpenAPI v3 endpoint" { kubectl get -v7 --raw '/openapi/v3/apis/apps.cozystack.io/v1alpha1' > /dev/null + kubectl get -v7 --raw '/openapi/v3/apis/core.cozystack.io/v1alpha1' > /dev/null } @test "Test OpenAPI v2 endpoint (protobuf)" { diff --git a/internal/controller/cozystackresource_controller.go b/internal/controller/cozystackresource_controller.go index 0f02b610..46884418 100644 --- a/internal/controller/cozystackresource_controller.go +++ b/internal/controller/cozystackresource_controller.go @@ -5,28 +5,21 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "sort" + "slices" "sync" "time" - "github.com/cozystack/cozystack/internal/controller/dashboard" - "github.com/cozystack/cozystack/internal/shared/crdmem" - cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" - "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -40,128 +33,20 @@ type CozystackResourceDefinitionReconciler struct { lastEvent time.Time lastHandled time.Time - mem *crdmem.Memory - - // Track static resources initialization - staticResourcesInitialized bool - staticResourcesMutex sync.Mutex + CozystackAPIKind string } func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - - crd := &cozyv1alpha1.CozystackResourceDefinition{} - err := r.Get(ctx, types.NamespacedName{Name: req.Name}, crd) - if err == nil { - if r.mem != nil { - r.mem.Upsert(crd) - } - - mgr := dashboard.NewManager( - r.Client, - r.Scheme, - dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) { - if r.mem != nil { - return r.mem.ListFromCacheOrAPI(c, r.Client) - } - var list cozyv1alpha1.CozystackResourceDefinitionList - if err := r.Client.List(c, &list); err != nil { - return nil, err - } - return list.Items, nil - }), - ) - - if res, derr := mgr.EnsureForCRD(ctx, crd); derr != nil || res.Requeue || res.RequeueAfter > 0 { - return res, derr - } - - // After processing CRD, perform cleanup of orphaned resources - // This should be done after cache warming to ensure all current resources are known - if cleanupErr := mgr.CleanupOrphanedResources(ctx); cleanupErr != nil { - logger.Error(cleanupErr, "Failed to cleanup orphaned dashboard resources") - // Don't fail the reconciliation, just log the error - } - - r.mu.Lock() - r.lastEvent = time.Now() - r.mu.Unlock() - return ctrl.Result{}, nil - } - - // Handle error cases (err is guaranteed to be non-nil here) - if !apierrors.IsNotFound(err) { - return ctrl.Result{}, err - } - // If resource is not found, clean up from memory - if r.mem != nil { - r.mem.Delete(req.Name) - } - if req.Namespace == "cozy-system" && req.Name == "cozystack-api" { - return r.debouncedRestart(ctx, logger) - } - return ctrl.Result{}, nil -} - -// initializeStaticResourcesOnce ensures static resources are created only once -func (r *CozystackResourceDefinitionReconciler) initializeStaticResourcesOnce(ctx context.Context) error { - r.staticResourcesMutex.Lock() - defer r.staticResourcesMutex.Unlock() - - if r.staticResourcesInitialized { - return nil // Already initialized - } - - // Create dashboard manager and initialize static resources - mgr := dashboard.NewManager( - r.Client, - r.Scheme, - dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) { - if r.mem != nil { - return r.mem.ListFromCacheOrAPI(c, r.Client) - } - var list cozyv1alpha1.CozystackResourceDefinitionList - if err := r.Client.List(c, &list); err != nil { - return nil, err - } - return list.Items, nil - }), - ) - - if err := mgr.InitializeStaticResources(ctx); err != nil { - return err - } - - r.staticResourcesInitialized = true - log.FromContext(ctx).Info("Static dashboard resources initialized successfully") - return nil + return r.debouncedRestart(ctx) } func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error { if r.Debounce == 0 { r.Debounce = 5 * time.Second } - if r.mem == nil { - r.mem = crdmem.Global() - } - if err := r.mem.EnsurePrimingWithManager(mgr); err != nil { - return err - } - - // Initialize static resources once during controller startup using manager.Runnable - if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { - if err := r.initializeStaticResourcesOnce(ctx); err != nil { - log.FromContext(ctx).Error(err, "Failed to initialize static resources") - return err - } - return nil - })); err != nil { - return err - } return ctrl.NewControllerManagedBy(mgr). Named("cozystackresource-controller"). - For(&cozyv1alpha1.CozystackResourceDefinition{}, builder.WithPredicates()). Watches( &cozyv1alpha1.CozystackResourceDefinition{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { @@ -176,9 +61,6 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage }} }), ). - WithOptions(controller.Options{ - MaxConcurrentReconciles: 5, // Allow more concurrent reconciles with proper rate limiting - }). Complete(r) } @@ -188,22 +70,18 @@ type crdHashView struct { } func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) { - var items []cozyv1alpha1.CozystackResourceDefinition - if r.mem != nil { - list, err := r.mem.ListFromCacheOrAPI(ctx, r.Client) - if err != nil { - return "", err - } - items = list + list := &cozyv1alpha1.CozystackResourceDefinitionList{} + if err := r.List(ctx, list); err != nil { + return "", err } - sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) + slices.SortFunc(list.Items, sortCozyRDs) - views := make([]crdHashView, 0, len(items)) - for i := range items { + views := make([]crdHashView, 0, len(list.Items)) + for i := range list.Items { views = append(views, crdHashView{ - Name: items[i].Name, - Spec: items[i].Spec, + Name: list.Items[i].Name, + Spec: list.Items[i].Spec, }) } b, err := json.Marshal(views) @@ -214,7 +92,9 @@ func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Co return hex.EncodeToString(sum[:]), nil } -func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context, logger logr.Logger) (ctrl.Result, error) { +func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context) (ctrl.Result, error) { + logger := log.FromContext(ctx) + r.mu.Lock() le := r.lastEvent lh := r.lastHandled @@ -239,15 +119,12 @@ func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Con return ctrl.Result{}, err } - deploy := &appsv1.Deployment{} - if err := r.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}, deploy); err != nil { + tpl, obj, patch, err := r.getWorkload(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}) + if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - if deploy.Spec.Template.Annotations == nil { - deploy.Spec.Template.Annotations = map[string]string{} - } - oldHash := deploy.Spec.Template.Annotations["cozystack.io/config-hash"] + oldHash := tpl.Annotations["cozystack.io/config-hash"] if oldHash == newHash && oldHash != "" { r.mu.Lock() @@ -257,10 +134,9 @@ func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Con return ctrl.Result{}, nil } - patch := client.MergeFrom(deploy.DeepCopy()) - deploy.Spec.Template.Annotations["cozystack.io/config-hash"] = newHash + tpl.Annotations["cozystack.io/config-hash"] = newHash - if err := r.Patch(ctx, deploy, patch); err != nil { + if err := r.Patch(ctx, obj, patch); err != nil { return ctrl.Result{}, err } @@ -272,3 +148,40 @@ func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Con "old", oldHash, "new", newHash) return ctrl.Result{}, nil } + +func (r *CozystackResourceDefinitionReconciler) getWorkload( + ctx context.Context, + key types.NamespacedName, +) (tpl *corev1.PodTemplateSpec, obj client.Object, patch client.Patch, err error) { + if r.CozystackAPIKind == "Deployment" { + dep := &appsv1.Deployment{} + if err := r.Get(ctx, key, dep); err != nil { + return nil, nil, nil, err + } + obj = dep + tpl = &dep.Spec.Template + patch = client.MergeFrom(dep.DeepCopy()) + } else { + ds := &appsv1.DaemonSet{} + if err := r.Get(ctx, key, ds); err != nil { + return nil, nil, nil, err + } + obj = ds + tpl = &ds.Spec.Template + patch = client.MergeFrom(ds.DeepCopy()) + } + if tpl.Annotations == nil { + tpl.Annotations = make(map[string]string) + } + return tpl, obj, patch, nil +} + +func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int { + if a.Name == b.Name { + return 0 + } + if a.Name < b.Name { + return -1 + } + return 1 +} diff --git a/internal/controller/dashboard/breadcrumb.go b/internal/controller/dashboard/breadcrumb.go index 0ad53ddb..5122f605 100644 --- a/internal/controller/dashboard/breadcrumb.go +++ b/internal/controller/dashboard/breadcrumb.go @@ -58,8 +58,8 @@ func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.Cozyst "breadcrumbItems": items, } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/customcolumns.go b/internal/controller/dashboard/customcolumns.go index 441787fe..6c23d68b 100644 --- a/internal/controller/dashboard/customcolumns.go +++ b/internal/controller/dashboard/customcolumns.go @@ -30,10 +30,6 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al name := fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural) id := fmt.Sprintf("stock-namespace-/%s/%s/%s", g, v, plural) - // Badge content & color derived from kind - badgeText := initialsFromKind(kind) // e.g., "VirtualMachine" -> "VM", "Bucket" -> "B" - badgeColor := hexColorForKind(kind) // deterministic, dark enough for white text - obj := &dashv1alpha1.CustomColumnsOverride{} obj.SetName(name) @@ -62,25 +58,11 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al }, "children": []any{ map[string]any{ - "type": "antdText", + "type": "ResourceBadge", "data": map[string]any{ "id": "header-badge", - "text": badgeText, - "title": strings.ToLower(kind), // optional tooltip - "style": map[string]any{ - "backgroundColor": badgeColor, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": "15px", - "fontWeight": 400, - "lineHeight": "24px", - "minWidth": 24, - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, + "value": kind, + // abbreviation auto-generated by ResourceBadge from value }, }, map[string]any{ @@ -145,8 +127,8 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al }, } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/customformsoverride.go b/internal/controller/dashboard/customformsoverride.go index c06a21f3..2b0daa08 100644 --- a/internal/controller/dashboard/customformsoverride.go +++ b/internal/controller/dashboard/customformsoverride.go @@ -11,6 +11,7 @@ import ( apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" ) // ensureCustomFormsOverride creates or updates a CustomFormsOverride resource for the given CRD @@ -45,16 +46,25 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph } } + // Build schema with multilineString for string fields without enum + l := log.FromContext(ctx) + schema, err := buildMultilineStringSchema(crd.Spec.Application.OpenAPISchema) + if err != nil { + // If schema parsing fails, log the error and use an empty schema + l.Error(err, "failed to build multiline string schema, using empty schema", "crd", crd.Name) + schema = map[string]any{} + } + spec := map[string]any{ "customizationId": customizationID, "hidden": hidden, "sort": sort, - "schema": map[string]any{}, // {} + "schema": schema, "strategy": "merge", } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err = controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources @@ -73,3 +83,112 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph }) return err } + +// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString +// for all string fields inside spec that don't have enum +func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) { + if openAPISchema == "" { + return map[string]any{}, nil + } + + var root map[string]any + if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil { + return nil, fmt.Errorf("cannot parse openAPISchema: %w", err) + } + + props, _ := root["properties"].(map[string]any) + if props == nil { + return map[string]any{}, nil + } + + schema := map[string]any{ + "properties": map[string]any{}, + } + + // Check if there's a spec property + specProp, ok := props["spec"].(map[string]any) + if !ok { + return map[string]any{}, nil + } + + specProps, ok := specProp["properties"].(map[string]any) + if !ok { + return map[string]any{}, nil + } + + // Create spec.properties structure in schema + schemaProps := schema["properties"].(map[string]any) + specSchema := map[string]any{ + "properties": map[string]any{}, + } + schemaProps["spec"] = specSchema + + // Process spec properties recursively + processSpecProperties(specProps, specSchema["properties"].(map[string]any)) + + return schema, nil +} + +// processSpecProperties recursively processes spec properties and adds multilineString type +// for string fields without enum +func processSpecProperties(props map[string]any, schemaProps map[string]any) { + for pname, raw := range props { + sub, ok := raw.(map[string]any) + if !ok { + continue + } + + typ, _ := sub["type"].(string) + + switch typ { + case "string": + // Check if this string field has enum + if !hasEnum(sub) { + // Add multilineString type for this field + if schemaProps[pname] == nil { + schemaProps[pname] = map[string]any{} + } + fieldSchema := schemaProps[pname].(map[string]any) + fieldSchema["type"] = "multilineString" + } + case "object": + // Recursively process nested objects + if childProps, ok := sub["properties"].(map[string]any); ok { + fieldSchema, ok := schemaProps[pname].(map[string]any) + if !ok { + fieldSchema = map[string]any{} + schemaProps[pname] = fieldSchema + } + nestedSchemaProps, ok := fieldSchema["properties"].(map[string]any) + if !ok { + nestedSchemaProps = map[string]any{} + fieldSchema["properties"] = nestedSchemaProps + } + processSpecProperties(childProps, nestedSchemaProps) + } + case "array": + // Check if array items are objects with properties + if items, ok := sub["items"].(map[string]any); ok { + if itemProps, ok := items["properties"].(map[string]any); ok { + // Create array item schema + fieldSchema, ok := schemaProps[pname].(map[string]any) + if !ok { + fieldSchema = map[string]any{} + schemaProps[pname] = fieldSchema + } + itemSchema, ok := fieldSchema["items"].(map[string]any) + if !ok { + itemSchema = map[string]any{} + fieldSchema["items"] = itemSchema + } + itemSchemaProps, ok := itemSchema["properties"].(map[string]any) + if !ok { + itemSchemaProps = map[string]any{} + itemSchema["properties"] = itemSchemaProps + } + processSpecProperties(itemProps, itemSchemaProps) + } + } + } + } +} diff --git a/internal/controller/dashboard/customformsoverride_test.go b/internal/controller/dashboard/customformsoverride_test.go new file mode 100644 index 00000000..9f7babe9 --- /dev/null +++ b/internal/controller/dashboard/customformsoverride_test.go @@ -0,0 +1,171 @@ +package dashboard + +import ( + "encoding/json" + "testing" +) + +func TestBuildMultilineStringSchema(t *testing.T) { + // Test OpenAPI schema with various field types + openAPISchema := `{ + "properties": { + "spec": { + "type": "object", + "properties": { + "simpleString": { + "type": "string", + "description": "A simple string field" + }, + "stringWithEnum": { + "type": "string", + "enum": ["option1", "option2"], + "description": "String with enum should be skipped" + }, + "numberField": { + "type": "number", + "description": "Number field should be skipped" + }, + "nestedObject": { + "type": "object", + "properties": { + "nestedString": { + "type": "string", + "description": "Nested string should get multilineString" + }, + "nestedStringWithEnum": { + "type": "string", + "enum": ["a", "b"], + "description": "Nested string with enum should be skipped" + } + } + }, + "arrayOfObjects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "itemString": { + "type": "string", + "description": "String in array item" + } + } + } + } + } + } + } + }` + + schema, err := buildMultilineStringSchema(openAPISchema) + if err != nil { + t.Fatalf("buildMultilineStringSchema failed: %v", err) + } + + // Marshal to JSON for easier inspection + schemaJSON, err := json.MarshalIndent(schema, "", " ") + if err != nil { + t.Fatalf("Failed to marshal schema: %v", err) + } + + t.Logf("Generated schema:\n%s", schemaJSON) + + // Verify that simpleString has multilineString type + props, ok := schema["properties"].(map[string]any) + if !ok { + t.Fatal("schema.properties is not a map") + } + + // Check spec property exists + spec, ok := props["spec"].(map[string]any) + if !ok { + t.Fatal("spec not found in properties") + } + + specProps, ok := spec["properties"].(map[string]any) + if !ok { + t.Fatal("spec.properties is not a map") + } + + // Check simpleString + simpleString, ok := specProps["simpleString"].(map[string]any) + if !ok { + t.Fatal("simpleString not found in spec.properties") + } + if simpleString["type"] != "multilineString" { + t.Errorf("simpleString should have type multilineString, got %v", simpleString["type"]) + } + + // Check stringWithEnum should not be present (or should not have multilineString) + if stringWithEnum, ok := specProps["stringWithEnum"].(map[string]any); ok { + if stringWithEnum["type"] == "multilineString" { + t.Error("stringWithEnum should not have multilineString type") + } + } + + // Check numberField should not be present + if numberField, ok := specProps["numberField"].(map[string]any); ok { + if numberField["type"] != nil { + t.Error("numberField should not have any type override") + } + } + + // Check nested object + nestedObject, ok := specProps["nestedObject"].(map[string]any) + if !ok { + t.Fatal("nestedObject not found in spec.properties") + } + nestedProps, ok := nestedObject["properties"].(map[string]any) + if !ok { + t.Fatal("nestedObject.properties is not a map") + } + + // Check nestedString + nestedString, ok := nestedProps["nestedString"].(map[string]any) + if !ok { + t.Fatal("nestedString not found in nestedObject.properties") + } + if nestedString["type"] != "multilineString" { + t.Errorf("nestedString should have type multilineString, got %v", nestedString["type"]) + } + + // Check array of objects + arrayOfObjects, ok := specProps["arrayOfObjects"].(map[string]any) + if !ok { + t.Fatal("arrayOfObjects not found in spec.properties") + } + items, ok := arrayOfObjects["items"].(map[string]any) + if !ok { + t.Fatal("arrayOfObjects.items is not a map") + } + itemProps, ok := items["properties"].(map[string]any) + if !ok { + t.Fatal("arrayOfObjects.items.properties is not a map") + } + itemString, ok := itemProps["itemString"].(map[string]any) + if !ok { + t.Fatal("itemString not found in arrayOfObjects.items.properties") + } + if itemString["type"] != "multilineString" { + t.Errorf("itemString should have type multilineString, got %v", itemString["type"]) + } +} + +func TestBuildMultilineStringSchemaEmpty(t *testing.T) { + schema, err := buildMultilineStringSchema("") + if err != nil { + t.Fatalf("buildMultilineStringSchema failed on empty string: %v", err) + } + if len(schema) != 0 { + t.Errorf("Expected empty schema for empty input, got %v", schema) + } +} + +func TestBuildMultilineStringSchemaInvalidJSON(t *testing.T) { + schema, err := buildMultilineStringSchema("{invalid json") + if err == nil { + t.Error("Expected error for invalid JSON") + } + if schema != nil { + t.Errorf("Expected nil schema for invalid JSON, got %v", schema) + } +} diff --git a/internal/controller/dashboard/customformsprefill.go b/internal/controller/dashboard/customformsprefill.go index b4dd2491..d2761873 100644 --- a/internal/controller/dashboard/customformsprefill.go +++ b/internal/controller/dashboard/customformsprefill.go @@ -56,8 +56,8 @@ func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha return reconcile.Result{}, err } - _, err = controllerutil.CreateOrUpdate(ctx, m.client, cfp, func() error { - if err := controllerutil.SetOwnerReference(crd, cfp, m.scheme); err != nil { + _, err = controllerutil.CreateOrUpdate(ctx, m.Client, cfp, func() error { + if err := controllerutil.SetOwnerReference(crd, cfp, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/factory.go b/internal/controller/dashboard/factory.go index 798d338c..4e18ff09 100644 --- a/internal/controller/dashboard/factory.go +++ b/internal/controller/dashboard/factory.go @@ -53,7 +53,6 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack Kind: kind, Plural: plural, Title: strings.ToLower(plural), - Size: BadgeSizeLarge, } spec := createUnifiedFactory(config, tabs, []any{resourceFetch}) @@ -61,8 +60,8 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack obj := &dashv1alpha1.Factory{} obj.SetName(factoryName) - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources @@ -115,7 +114,7 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str "gap": float64(6), }, "children": []any{ - createUnifiedBadgeFromKind("ns-badge", "Namespace", "namespace", BadgeSizeMedium), + createUnifiedBadgeFromKind("ns-badge", "Namespace"), antdLink("namespace-link", "{reqsJsonPath[0]['.metadata.namespace']['-']}", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace", @@ -222,7 +221,7 @@ func workloadsTab(kind string) map[string]any { "baseprefix": "/openapi-ui", "customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors", "pathToItems": []any{"items"}, - "labelsSelector": map[string]any{ + "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, "apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}", @@ -247,7 +246,7 @@ func servicesTab(kind string) map[string]any { "baseprefix": "/openapi-ui", "customizationId": "factory-details-v1.services", "pathToItems": []any{"items"}, - "labelsSelector": map[string]any{ + "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, "apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}", @@ -273,7 +272,7 @@ func ingressesTab(kind string) map[string]any { "baseprefix": "/openapi-ui", "customizationId": "factory-details-networking.k8s.io.v1.ingresses", "pathToItems": []any{"items"}, - "labelsSelector": map[string]any{ + "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, "apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}", @@ -294,12 +293,12 @@ func secretsTab(kind string) map[string]any { "type": "EnrichedTable", "data": map[string]any{ "id": "secrets-table", - "fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecretstables", + "fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets", "clusterNamePartOfUrl": "{2}", "baseprefix": "/openapi-ui", - "customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecretstables", + "customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets", "pathToItems": []any{"items"}, - "labelsSelector": map[string]any{ + "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, "apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}", @@ -324,6 +323,7 @@ func yamlTab(plural string) map[string]any { "type": "builtin", "typeName": plural, "prefillValuesRequestIndex": float64(0), + "readOnly": true, "substractHeight": float64(400), }, }, diff --git a/internal/controller/dashboard/helpers.go b/internal/controller/dashboard/helpers.go index e5361c53..a0023a05 100644 --- a/internal/controller/dashboard/helpers.go +++ b/internal/controller/dashboard/helpers.go @@ -1,12 +1,9 @@ package dashboard import ( - "crypto/sha1" - "encoding/hex" "encoding/json" "fmt" "reflect" - "regexp" "sort" "strings" @@ -57,97 +54,6 @@ func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) stri return k + "s" } -// initialsFromKind splits CamelCase and returns the first letters in upper case. -// "VirtualMachine" -> "VM"; "Bucket" -> "B". -func initialsFromKind(kind string) string { - parts := splitCamel(kind) - if len(parts) == 0 { - return strings.ToUpper(kind) - } - var b strings.Builder - for _, p := range parts { - if p == "" { - continue - } - b.WriteString(strings.ToUpper(string(p[0]))) - // Limit to 3 chars to keep the badge compact (VM, PVC, etc.) - if b.Len() >= 3 { - break - } - } - return b.String() -} - -// hexColorForKind returns a dark, saturated color (hex) derived from a stable hash of the kind. -// We map the hash to an HSL hue; fix S/L for consistent readability with white text. -func hexColorForKind(kind string) string { - // Stable short hash (sha1 → bytes → hue) - sum := sha1.Sum([]byte(kind)) - // Use first two bytes for hue [0..359] - hue := int(sum[0])<<8 | int(sum[1]) - hue = hue % 360 - - // Fixed S/L chosen to contrast with white text: - // S = 80%, L = 35% (dark enough so #fff is readable) - r, g, b := hslToRGB(float64(hue), 0.80, 0.35) - - return fmt.Sprintf("#%02x%02x%02x", r, g, b) -} - -// hslToRGB converts HSL (0..360, 0..1, 0..1) to sRGB (0..255). -func hslToRGB(h float64, s float64, l float64) (uint8, uint8, uint8) { - c := (1 - absFloat(2*l-1)) * s - hp := h / 60.0 - x := c * (1 - absFloat(modFloat(hp, 2)-1)) - var r1, g1, b1 float64 - switch { - case 0 <= hp && hp < 1: - r1, g1, b1 = c, x, 0 - case 1 <= hp && hp < 2: - r1, g1, b1 = x, c, 0 - case 2 <= hp && hp < 3: - r1, g1, b1 = 0, c, x - case 3 <= hp && hp < 4: - r1, g1, b1 = 0, x, c - case 4 <= hp && hp < 5: - r1, g1, b1 = x, 0, c - default: - r1, g1, b1 = c, 0, x - } - m := l - c/2 - r := uint8(clamp01(r1+m) * 255.0) - g := uint8(clamp01(g1+m) * 255.0) - b := uint8(clamp01(b1+m) * 255.0) - return r, g, b -} - -func absFloat(v float64) float64 { - if v < 0 { - return -v - } - return v -} - -func modFloat(a, b float64) float64 { - return a - b*float64(int(a/b)) -} - -func clamp01(v float64) float64 { - if v < 0 { - return 0 - } - if v > 1 { - return 1 - } - return v -} - -// optional: tiny helper to expose the compact color hash (useful for debugging) -func shortHashHex(s string) string { - sum := sha1.Sum([]byte(s)) - return hex.EncodeToString(sum[:4]) -} - // ----------------------- Helpers (OpenAPI → values) ----------------------- // defaultOrZero returns the schema default if present; otherwise a reasonable zero value. @@ -295,12 +201,6 @@ func normalizeJSON(v any) any { } } -var camelSplitter = regexp.MustCompile(`(?m)([A-Z]+[a-z0-9]*|[a-z0-9]+)`) - -func splitCamel(s string) []string { - return camelSplitter.FindAllString(s, -1) -} - // --- helpers for schema inspection --- func isScalarType(n map[string]any) bool { diff --git a/internal/controller/dashboard/manager.go b/internal/controller/dashboard/manager.go index 4b5c2fb3..12e50fbc 100644 --- a/internal/controller/dashboard/manager.go +++ b/internal/controller/dashboard/manager.go @@ -10,9 +10,12 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + managerpkg "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -40,28 +43,51 @@ func AddToScheme(s *runtime.Scheme) error { // Manager owns logic for creating/updating dashboard resources derived from CRDs. // It’s easy to extend: add new ensure* methods and wire them into EnsureForCRD. type Manager struct { - client client.Client - scheme *runtime.Scheme - crdListFn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) -} - -// Option pattern so callers can inject a custom lister. -type Option func(*Manager) - -// WithCRDListFunc overrides how Manager lists all CozystackResourceDefinitions. -func WithCRDListFunc(fn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error)) Option { - return func(m *Manager) { m.crdListFn = fn } + client.Client + Scheme *runtime.Scheme } // NewManager constructs a dashboard Manager. -func NewManager(c client.Client, scheme *runtime.Scheme, opts ...Option) *Manager { - m := &Manager{client: c, scheme: scheme} - for _, o := range opts { - o(m) - } +func NewManager(c client.Client, scheme *runtime.Scheme) *Manager { + m := &Manager{Client: c, Scheme: scheme} return m } +func (m *Manager) SetupWithManager(mgr ctrl.Manager) error { + if err := ctrl.NewControllerManagedBy(mgr). + Named("dashboard-reconciler"). + For(&cozyv1alpha1.CozystackResourceDefinition{}). + Complete(m); err != nil { + return err + } + + return mgr.Add(managerpkg.RunnableFunc(func(ctx context.Context) error { + if !mgr.GetCache().WaitForCacheSync(ctx) { + return fmt.Errorf("dashboard static resources cache sync failed") + } + return m.ensureStaticResources(ctx) + })) +} + +func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + + crd := &cozyv1alpha1.CozystackResourceDefinition{} + + err := m.Get(ctx, types.NamespacedName{Name: req.Name}, crd) + if err != nil { + if apierrors.IsNotFound(err) { + if err := m.CleanupOrphanedResources(ctx); err != nil { + l.Error(err, "Failed to cleanup orphaned dashboard resources") + } + return ctrl.Result{}, nil // no point in requeuing here + } + return ctrl.Result{}, err + } + + return m.EnsureForCRD(ctx, crd) +} + // EnsureForCRD is the single entry-point used by the controller. // Add more ensure* calls here as you implement support for other resources: // @@ -171,21 +197,11 @@ func (m *Manager) getStaticResourceSelector() client.MatchingLabels { // CleanupOrphanedResources removes dashboard resources that are no longer needed // This should be called after cache warming to ensure all current resources are known func (m *Manager) CleanupOrphanedResources(ctx context.Context) error { - // Get all current CRDs to determine which resources should exist - var allCRDs []cozyv1alpha1.CozystackResourceDefinition - if m.crdListFn != nil { - s, err := m.crdListFn(ctx) - if err != nil { - return err - } - allCRDs = s - } else { - var crdList cozyv1alpha1.CozystackResourceDefinitionList - if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil { - return err - } - allCRDs = crdList.Items + var crdList cozyv1alpha1.CozystackResourceDefinitionList + if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil { + return err } + allCRDs := crdList.Items // Build a set of expected resource names for each type expectedResources := m.buildExpectedResourceSet(allCRDs) @@ -349,7 +365,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O } // List with dashboard labels - if err := m.client.List(ctx, list, m.getDashboardResourceSelector()); err != nil { + if err := m.List(ctx, list, m.getDashboardResourceSelector()); err != nil { return err } @@ -358,7 +374,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.CustomColumnsOverrideList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -369,7 +385,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.CustomFormsOverrideList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -380,7 +396,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.CustomFormsPrefillList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -391,7 +407,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.MarketplacePanelList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -402,7 +418,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.SidebarList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -413,7 +429,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.TableUriMappingList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -426,7 +442,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O if !expected[item.Name] { logger := log.FromContext(ctx) logger.Info("Deleting orphaned Breadcrumb resource", "name", item.Name) - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -438,7 +454,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O if !expected[item.Name] { logger := log.FromContext(ctx) logger.Info("Deleting orphaned Factory resource", "name", item.Name) - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } diff --git a/internal/controller/dashboard/marketplacepanel.go b/internal/controller/dashboard/marketplacepanel.go index e58232f5..82a8f336 100644 --- a/internal/controller/dashboard/marketplacepanel.go +++ b/internal/controller/dashboard/marketplacepanel.go @@ -24,14 +24,14 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. // If dashboard is not set, delete the panel if it exists. if crd.Spec.Dashboard == nil { - err := m.client.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) + err := m.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) if apierrors.IsNotFound(err) { return reconcile.Result{}, nil } if err != nil { return reconcile.Result{}, err } - if err := m.client.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { + if err := m.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { return reconcile.Result{}, err } logger.Info("Deleted MarketplacePanel because dashboard is not set", "name", mp.Name) @@ -40,14 +40,14 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. // Skip module and tenant resources (they don't need MarketplacePanel) if crd.Spec.Dashboard.Module || crd.Spec.Application.Kind == "Tenant" { - err := m.client.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) + err := m.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) if apierrors.IsNotFound(err) { return reconcile.Result{}, nil } if err != nil { return reconcile.Result{}, err } - if err := m.client.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { + if err := m.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { return reconcile.Result{}, err } logger.Info("Deleted MarketplacePanel because resource is a module", "name", mp.Name) @@ -86,8 +86,8 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. return reconcile.Result{}, err } - _, err = controllerutil.CreateOrUpdate(ctx, m.client, mp, func() error { - if err := controllerutil.SetOwnerReference(crd, mp, m.scheme); err != nil { + _, err = controllerutil.CreateOrUpdate(ctx, m.Client, mp, func() error { + if err := controllerutil.SetOwnerReference(crd, mp, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/sidebar.go b/internal/controller/dashboard/sidebar.go index 3edd6aca..97937921 100644 --- a/internal/controller/dashboard/sidebar.go +++ b/internal/controller/dashboard/sidebar.go @@ -33,19 +33,11 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack // 1) Fetch all CRDs var all []cozyv1alpha1.CozystackResourceDefinition - if m.crdListFn != nil { - s, err := m.crdListFn(ctx) - if err != nil { - return err - } - all = s - } else { - var crdList cozyv1alpha1.CozystackResourceDefinitionList - if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil { - return err - } - all = crdList.Items + var crdList cozyv1alpha1.CozystackResourceDefinitionList + if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil { + return err } + all = crdList.Items // 2) Build category -> []item map (only for CRDs with spec.dashboard != nil) type item struct { @@ -251,7 +243,7 @@ func (m *Manager) upsertMultipleSidebars( obj := &dashv1alpha1.Sidebar{} obj.SetName(id) - if _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { + if _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { // Only set owner reference for dynamic sidebars (stock-project-factory-{kind}-details) // Static sidebars (stock-instance-*, stock-project-*) should not have owner references if strings.HasPrefix(id, "stock-project-factory-") && strings.HasSuffix(id, "-details") { @@ -260,7 +252,7 @@ func (m *Manager) upsertMultipleSidebars( lowerKind := strings.ToLower(kind) expectedID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind) if id == expectedID { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/static_helpers.go b/internal/controller/dashboard/static_helpers.go index 469e9f8f..4c8aae30 100644 --- a/internal/controller/dashboard/static_helpers.go +++ b/internal/controller/dashboard/static_helpers.go @@ -122,7 +122,7 @@ func createCustomColumnsOverride(id string, additionalPrinterColumns []any) *das } } - if name == "factory-details-v1alpha1.core.cozystack.io.tenantsecretstables" { + if name == "factory-details-v1alpha1.core.cozystack.io.tenantsecrets" { data["additionalPrinterColumnsTrimLengths"] = []any{ map[string]any{ "key": "Name", @@ -531,7 +531,6 @@ func createBreadcrumbItem(key, label string, link ...string) map[string]any { // createCustomColumn creates a custom column with factory type and badge func createCustomColumn(name, kind, plural, href string) map[string]any { - badge := createUnifiedBadgeFromKind("header-badge", kind, plural, BadgeSizeMedium) link := antdLink("name-link", "{reqsJsonPath[0]['.metadata.name']['-']}", href) return map[string]any{ @@ -541,8 +540,18 @@ func createCustomColumn(name, kind, plural, href string) map[string]any { "disableEventBubbling": true, "items": []any{ map[string]any{ - "children": []any{badge, link}, - "type": "antdFlex", + "children": []any{ + map[string]any{ + "type": "ResourceBadge", + "data": map[string]any{ + "id": "header-badge", + "value": kind, + // abbreviation auto-generated by ResourceBadge from value + }, + }, + link, + }, + "type": "antdFlex", "data": map[string]any{ "align": "center", "gap": float64(6), @@ -554,16 +563,16 @@ func createCustomColumn(name, kind, plural, href string) map[string]any { } // createCustomColumnWithBadge creates a custom column with a specific badge -func createCustomColumnWithBadge(name, badgeText, badgeColor, title, href string) map[string]any { - config := BadgeConfig{ - Text: badgeText, - Color: badgeColor, - Title: title, - Size: BadgeSizeMedium, - } - badge := createUnifiedBadge("header-badge", config) +// badgeValue should be the kind in PascalCase (e.g., "Service", "Pod") +// abbreviation is auto-generated by ResourceBadge from badgeValue +func createCustomColumnWithBadge(name, badgeValue, href string) map[string]any { link := antdLink("name-link", "{reqsJsonPath[0]['.metadata.name']['-']}", href) + badgeData := map[string]any{ + "id": "header-badge", + "value": badgeValue, + } + return map[string]any{ "name": name, "type": "factory", @@ -571,8 +580,14 @@ func createCustomColumnWithBadge(name, badgeText, badgeColor, title, href string "disableEventBubbling": true, "items": []any{ map[string]any{ - "children": []any{badge, link}, - "type": "antdFlex", + "children": []any{ + map[string]any{ + "type": "ResourceBadge", + "data": badgeData, + }, + link, + }, + "type": "antdFlex", "data": map[string]any{ "align": "center", "gap": float64(6), @@ -583,17 +598,22 @@ func createCustomColumnWithBadge(name, badgeText, badgeColor, title, href string } } -// createCustomColumnWithSpecificColor creates a custom column with a specific color -func createCustomColumnWithSpecificColor(name, kind, title, color, href string) map[string]any { - config := BadgeConfig{ - Text: initialsFromKind(kind), - Color: color, - Title: title, - Size: BadgeSizeMedium, - } - badge := createUnifiedBadge("header-badge", config) +// createCustomColumnWithSpecificColor creates a custom column with a specific kind and optional color +// badgeValue should be the kind in PascalCase (e.g., "Service", "Pod") +func createCustomColumnWithSpecificColor(name, kind, color, href string) map[string]any { link := antdLink("name-link", "{reqsJsonPath[0]['.metadata.name']['-']}", href) + badgeData := map[string]any{ + "id": "header-badge", + "value": kind, + } + // Add custom color if specified + if color != "" { + badgeData["style"] = map[string]any{ + "backgroundColor": color, + } + } + return map[string]any{ "name": name, "type": "factory", @@ -602,8 +622,14 @@ func createCustomColumnWithSpecificColor(name, kind, title, color, href string) "disableEventBubbling": true, "items": []any{ map[string]any{ - "children": []any{badge, link}, - "type": "antdFlex", + "children": []any{ + map[string]any{ + "type": "ResourceBadge", + "data": badgeData, + }, + link, + }, + "type": "antdFlex", "data": map[string]any{ "align": "center", "gap": float64(6), @@ -668,7 +694,7 @@ func createTimestampColumn(name, jsonPath string) map[string]any { // createFactoryHeader creates a header for factory resources func createFactoryHeader(kind, plural string) map[string]any { lowerKind := strings.ToLower(kind) - badge := createUnifiedBadgeFromKind("badge-"+lowerKind, kind, plural, BadgeSizeLarge) + badge := createUnifiedBadgeFromKind("badge-"+lowerKind, kind) nameText := parsedText(lowerKind+"-name", "{reqsJsonPath[0]['.metadata.name']['-']}", map[string]any{ "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", "fontSize": float64(20), @@ -718,13 +744,26 @@ func createFactorySpec(key string, sidebarTags []any, urlsToFetch []any, header } // createCustomColumnWithJsonPath creates a column with a custom badge and link using jsonPath -func createCustomColumnWithJsonPath(name, jsonPath, badgeText, badgeTitle, badgeColor, linkHref string) map[string]any { +// badgeValue should be the kind in PascalCase (e.g., "Service", "VirtualMachine") +// abbreviation is auto-generated by ResourceBadge from badgeValue +func createCustomColumnWithJsonPath(name, jsonPath, badgeValue, badgeColor, linkHref string) map[string]any { // Determine link ID based on jsonPath linkId := "name-link" if jsonPath == ".metadata.namespace" { linkId = "namespace-link" } + badgeData := map[string]any{ + "id": "header-badge", + "value": badgeValue, + } + // Add custom color if specified + if badgeColor != "" { + badgeData["style"] = map[string]any{ + "backgroundColor": badgeColor, + } + } + return map[string]any{ "name": name, "type": "factory", @@ -741,26 +780,8 @@ func createCustomColumnWithJsonPath(name, jsonPath, badgeText, badgeTitle, badge }, "children": []any{ map[string]any{ - "type": "antdText", - "data": map[string]any{ - "id": "header-badge", - "text": badgeText, - "title": badgeTitle, - "style": map[string]any{ - "backgroundColor": badgeColor, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": "15px", - "fontWeight": 400, - "lineHeight": "24px", - "minWidth": 24, - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, - }, + "type": "ResourceBadge", + "data": badgeData, }, map[string]any{ "type": "antdLink", @@ -778,7 +799,20 @@ func createCustomColumnWithJsonPath(name, jsonPath, badgeText, badgeTitle, badge } // createCustomColumnWithoutJsonPath creates a column with a custom badge and link without jsonPath -func createCustomColumnWithoutJsonPath(name, badgeText, badgeTitle, badgeColor, linkHref string) map[string]any { +// badgeValue should be the kind in PascalCase (e.g., "Node", "Pod") +// abbreviation is auto-generated by ResourceBadge from badgeValue +func createCustomColumnWithoutJsonPath(name, badgeValue, badgeColor, linkHref string) map[string]any { + badgeData := map[string]any{ + "id": "header-badge", + "value": badgeValue, + } + // Add custom color if specified + if badgeColor != "" { + badgeData["style"] = map[string]any{ + "backgroundColor": badgeColor, + } + } + return map[string]any{ "name": name, "type": "factory", @@ -794,26 +828,8 @@ func createCustomColumnWithoutJsonPath(name, badgeText, badgeTitle, badgeColor, }, "children": []any{ map[string]any{ - "type": "antdText", - "data": map[string]any{ - "id": "header-badge", - "text": badgeText, - "title": badgeTitle, - "style": map[string]any{ - "backgroundColor": badgeColor, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": "15px", - "fontWeight": 400, - "lineHeight": "24px", - "minWidth": 24, - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, - }, + "type": "ResourceBadge", + "data": badgeData, }, map[string]any{ "type": "antdLink", @@ -1030,6 +1046,15 @@ func createConverterBytesColumn(name, jsonPath string) map[string]any { } } +// createFlatMapColumn creates a flatMap column that expands a map into separate rows +func createFlatMapColumn(name, jsonPath string) map[string]any { + return map[string]any{ + "name": name, + "type": "flatMap", + "jsonPath": jsonPath, + } +} + // ---------------- Factory UI helper functions ---------------- // labelsEditor creates a Labels editor component diff --git a/internal/controller/dashboard/static_processor.go b/internal/controller/dashboard/static_processor.go index ef2e0341..902e3818 100644 --- a/internal/controller/dashboard/static_processor.go +++ b/internal/controller/dashboard/static_processor.go @@ -32,7 +32,7 @@ func (m *Manager) ensureStaticResource(ctx context.Context, obj client.Object) e // Add dashboard labels to static resources m.addDashboardLabels(resource, nil, ResourceTypeStatic) - _, err := controllerutil.CreateOrUpdate(ctx, m.client, resource, func() error { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, resource, func() error { // For static resources, we don't need to set owner references // as they are meant to be persistent across CRD changes // Copy Spec from the original object to the live object diff --git a/internal/controller/dashboard/static_refactored.go b/internal/controller/dashboard/static_refactored.go index 24a602d3..266cb6d5 100644 --- a/internal/controller/dashboard/static_refactored.go +++ b/internal/controller/dashboard/static_refactored.go @@ -132,7 +132,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid return []*dashboardv1alpha1.CustomColumnsOverride{ // Factory details v1 services createCustomColumnsOverride("factory-details-v1.services", []any{ - createCustomColumnWithSpecificColor("Name", "Service", "service", getColorForType("service"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithSpecificColor("Name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("ClusterIP", ".spec.clusterIP"), createStringColumn("LoadbalancerIP", ".spec.loadBalancerIP"), createTimestampColumn("Created", ".metadata.creationTimestamp"), @@ -140,15 +140,15 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace v1 services createCustomColumnsOverride("stock-namespace-/v1/services", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "service", getColorForType("service"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("ClusterIP", ".spec.clusterIP"), - createStringColumn("LoadbalancerIP", ".spec.loadBalancerIP"), + createStringColumn("LoadbalancerIP", ".status.loadBalancer.ingress[0].ip"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock namespace core cozystack io v1alpha1 tenantmodules createCustomColumnsOverride("stock-namespace-/core.cozystack.io/v1alpha1/tenantmodules", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "M", "module", getColorForType("module"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/{reqsJsonPath[0]['.metadata.name']['-']}-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Module", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/{reqsJsonPath[0]['.metadata.name']['-']}-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createReadyColumn(), createTimestampColumn("Created", ".metadata.creationTimestamp"), createStringColumn("Version", ".status.version"), @@ -164,7 +164,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory details v1alpha1 cozystack io workloadmonitors createCustomColumnsOverride("factory-details-v1alpha1.cozystack.io.workloadmonitors", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "W", "workloadmonitor", getColorForType("workloadmonitor"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/workloadmonitor-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "WorkloadMonitor", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/workloadmonitor-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("TYPE", ".spec.type"), createStringColumn("VERSION", ".spec.version"), createStringColumn("REPLICAS", ".spec.replicas"), @@ -173,18 +173,19 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid createStringColumn("OBSERVED", ".status.observedReplicas"), }), - // Factory details v1alpha1 core cozystack io tenantsecretstables - createCustomColumnsOverride("factory-details-v1alpha1.core.cozystack.io.tenantsecretstables", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "secret", getColorForType("secret"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createStringColumn("Key", ".data.key"), - createSecretBase64Column("Value", ".data.value"), + // Factory details v1alpha1 core cozystack io tenantsecrets + createCustomColumnsOverride("factory-details-v1alpha1.core.cozystack.io.tenantsecrets", []any{ + createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createFlatMapColumn("Data", ".data"), + createStringColumn("Key", "_flatMapData_Key"), + createSecretBase64Column("Value", "._flatMapData_Value"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Factory ingress details rules createCustomColumnsOverride("factory-kube-ingress-details-rules", []any{ createStringColumn("Host", ".host"), - createCustomColumnWithJsonPath("Service", ".http.paths[0].backend.service.name", "S", "service", getColorForType("service"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.http.paths[0].backend.service.name']['-']}"), + createCustomColumnWithJsonPath("Service", ".http.paths[0].backend.service.name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.http.paths[0].backend.service.name']['-']}"), createStringColumn("Port", ".http.paths[0].backend.service.port.number"), createStringColumn("Path", ".http.paths[0].path"), }), @@ -250,7 +251,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory details networking k8s io v1 ingresses createCustomColumnsOverride("factory-details-networking.k8s.io.v1.ingresses", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "I", "ingress", getColorForType("ingress"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Ingress", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Hosts", ".spec.rules[*].host"), createStringColumn("Address", ".status.loadBalancer.ingress[0].ip"), createStringColumn("Port", ".spec.defaultBackend.service.port.number"), @@ -259,7 +260,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace networking k8s io v1 ingresses createCustomColumnsOverride("stock-namespace-/networking.k8s.io/v1/ingresses", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "I", "ingress", getColorForType("ingress"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Ingress", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Hosts", ".spec.rules[*].host"), createStringColumn("Address", ".status.loadBalancer.ingress[0].ip"), createStringColumn("Port", ".spec.defaultBackend.service.port.number"), @@ -268,34 +269,34 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster v1 configmaps createCustomColumnsOverride("stock-cluster-/v1/configmaps", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "CM", "configmap", getColorForType("configmap"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", getColorForType("namespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "ConfigMap", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock namespace v1 configmaps createCustomColumnsOverride("stock-namespace-/v1/configmaps", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "CM", "configmap", getColorForType("configmap"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "ConfigMap", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Cluster v1 configmaps createCustomColumnsOverride("cluster-/v1/configmaps", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "CM", "configmap", getColorForType("configmap"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", getColorForType("namespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "ConfigMap", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock cluster v1 nodes createCustomColumnsOverride("stock-cluster-/v1/nodes", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "N", "node", getColorForType("node"), "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Node", "", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createSimpleStatusColumn("Status", "node-status"), }), // Factory node details v1 pods createCustomColumnsOverride("factory-node-details-v1.pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", getColorForType("pod"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", getColorForType("namespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -304,8 +305,8 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory v1 pods createCustomColumnsOverride("factory-v1.pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", getColorForType("pod"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithoutJsonPath("Node", "N", "node", getColorForType("node"), "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithoutJsonPath("Node", "Node", "", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -314,9 +315,9 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster v1 pods createCustomColumnsOverride("stock-cluster-/v1/pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), - createCustomColumnWithJsonPath("Node", ".spec.nodeName", "N", "node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), + createCustomColumnWithJsonPath("Node", ".spec.nodeName", "Node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -325,8 +326,8 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace v1 pods createCustomColumnsOverride("stock-namespace-/v1/pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithoutJsonPath("Node", "N", "node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithoutJsonPath("Node", "Node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -335,15 +336,15 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster v1 secrets createCustomColumnsOverride("stock-cluster-/v1/secrets", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), createStringColumn("Type", ".type"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock namespace v1 secrets createCustomColumnsOverride("stock-namespace-/v1/secrets", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Type", ".type"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), @@ -360,7 +361,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster core cozystack io v1alpha1 tenantnamespaces createCustomColumnsOverride("stock-cluster-/core.cozystack.io/v1alpha1/tenantnamespaces", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "TN", "tenantnamespace", getColorForType("tenantnamespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.name']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "TenantNamespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.name']['-']}/factory/marketplace"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), } @@ -496,7 +497,6 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { Kind: "Namespace", Plural: "namespaces", Title: "namespace", - Size: BadgeSizeLarge, } namespaceSpec := createUnifiedFactory(namespaceConfig, nil, []any{"/api/clusters/{2}/k8s/api/v1/namespaces/{5}"}) @@ -796,6 +796,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "substractHeight": float64(400), "type": "builtin", "typeName": "secrets", + "readOnly": true, }, }, }, @@ -1055,7 +1056,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "clusterNamePartOfUrl": "{2}", "customizationId": "factory-kube-service-details-endpointslice", "fetchUrl": "/api/clusters/{2}/k8s/apis/discovery.k8s.io/v1/namespaces/{3}/endpointslices", - "labelsSelector": map[string]any{ + "labelSelector": map[string]any{ "kubernetes.io/service-name": "{reqsJsonPath[0]['.metadata.name']['-']}", }, "pathToItems": ".items[*].endpoints", @@ -1201,7 +1202,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "gap": 6, }, "children": []any{ - createUnifiedBadgeFromKind("ns-badge", "Namespace", "namespace", BadgeSizeMedium), + createUnifiedBadgeFromKind("ns-badge", "Namespace"), antdLink("namespace-link", "{reqsJsonPath[0]['.metadata.namespace']['-']}", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace", @@ -1396,7 +1397,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "clusterNamePartOfUrl": "{2}", "customizationId": "factory-details-v1alpha1.cozystack.io.workloads", "fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloads", - "labelsSelector": map[string]any{ + "labelSelector": map[string]any{ "workloads.cozystack.io/monitor": "{reqs[0]['metadata','name']}", }, "pathToItems": []any{"items"}, diff --git a/internal/controller/dashboard/ui_helpers.go b/internal/controller/dashboard/ui_helpers.go index 4066fd36..fb29a608 100644 --- a/internal/controller/dashboard/ui_helpers.go +++ b/internal/controller/dashboard/ui_helpers.go @@ -1,7 +1,5 @@ package dashboard -import "strings" - // ---------------- UI helpers (use float64 for numeric fields) ---------------- func contentCard(id string, style map[string]any, children []any) map[string]any { @@ -200,10 +198,10 @@ func createBadge(id, text, color, title string) map[string]any { // createBadgeFromKind creates a badge using the existing badge generation functions func createBadgeFromKind(id, kind, title string) map[string]any { - return createUnifiedBadgeFromKind(id, kind, title, BadgeSizeMedium) + return createUnifiedBadgeFromKind(id, kind) } // createHeaderBadge creates a badge specifically for headers with consistent styling func createHeaderBadge(id, kind, plural string) map[string]any { - return createUnifiedBadgeFromKind(id, kind, strings.ToLower(plural), BadgeSizeLarge) + return createUnifiedBadgeFromKind(id, kind) } diff --git a/internal/controller/dashboard/unified_helpers.go b/internal/controller/dashboard/unified_helpers.go index 7bdea2b5..b25d5b65 100644 --- a/internal/controller/dashboard/unified_helpers.go +++ b/internal/controller/dashboard/unified_helpers.go @@ -81,86 +81,47 @@ func isAlphanumeric(c byte) bool { // BadgeConfig holds configuration for badge generation type BadgeConfig struct { - Text string - Color string - Title string - Size BadgeSize + Kind string // Resource kind in PascalCase (e.g., "VirtualMachine") - used for value and auto-generation + Text string // Optional abbreviation override (if empty, ResourceBadge auto-generates from Kind) + Color string // Optional custom backgroundColor override } -// BadgeSize represents the size of the badge -type BadgeSize int - -const ( - BadgeSizeSmall BadgeSize = iota - BadgeSizeMedium - BadgeSizeLarge -) - -// generateBadgeConfig creates a BadgeConfig from kind and optional custom values -func generateBadgeConfig(kind string, customText, customColor, customTitle string) BadgeConfig { - config := BadgeConfig{ - Text: initialsFromKind(kind), - Color: hexColorForKind(kind), - Title: strings.ToLower(kind), - Size: BadgeSizeMedium, - } - - // Override with custom values if provided - if customText != "" { - config.Text = customText - } - if customColor != "" { - config.Color = customColor - } - if customTitle != "" { - config.Title = customTitle - } - - return config -} - -// createUnifiedBadge creates a badge using the unified BadgeConfig +// createUnifiedBadge creates a badge using the unified BadgeConfig with ResourceBadge component func createUnifiedBadge(id string, config BadgeConfig) map[string]any { - fontSize := "15px" - if config.Size == BadgeSizeLarge { - fontSize = "20px" - } else if config.Size == BadgeSizeSmall { - fontSize = "12px" + data := map[string]any{ + "id": id, + "value": config.Kind, + } + + // Add abbreviation override if specified (otherwise ResourceBadge auto-generates from Kind) + if config.Text != "" { + data["abbreviation"] = config.Text + } + + // Add custom color if specified + if config.Color != "" { + data["style"] = map[string]any{ + "backgroundColor": config.Color, + } } return map[string]any{ - "type": "antdText", - "data": map[string]any{ - "id": id, - "text": config.Text, - "title": config.Title, - "style": map[string]any{ - "backgroundColor": config.Color, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": fontSize, - "fontWeight": float64(400), - "lineHeight": "24px", - "minWidth": float64(24), - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, - }, + "type": "ResourceBadge", + "data": data, } } -// createUnifiedBadgeFromKind creates a badge from kind with automatic color generation -func createUnifiedBadgeFromKind(id, kind, title string, size BadgeSize) map[string]any { - config := BadgeConfig{ - Text: initialsFromKind(kind), - Color: hexColorForKind(kind), - Title: title, - Size: size, +// createUnifiedBadgeFromKind creates a badge from kind with ResourceBadge component +// Abbreviation is auto-generated by ResourceBadge from kind, but can be customized if needed +func createUnifiedBadgeFromKind(id, kind string) map[string]any { + return map[string]any{ + "type": "ResourceBadge", + "data": map[string]any{ + "id": id, + "value": kind, + // abbreviation is optional - ResourceBadge auto-generates from value + }, } - return createUnifiedBadge(id, config) } // ---------------- Resource creation helpers with unified approach ---------------- @@ -183,7 +144,9 @@ func createResourceConfig(components []string, kind, title string) ResourceConfi metadataName := generateMetadataName(specID) // Generate badge config - badgeConfig := generateBadgeConfig(kind, "", "", title) + badgeConfig := BadgeConfig{ + Kind: kind, + } return ResourceConfig{ SpecID: specID, @@ -196,35 +159,6 @@ func createResourceConfig(components []string, kind, title string) ResourceConfi // ---------------- Enhanced color generation ---------------- -// getColorForKind returns a color for a specific kind with improved distribution -func getColorForKind(kind string) string { - // Use existing hexColorForKind function - return hexColorForKind(kind) -} - -// getColorForType returns a color for a specific type (like "namespace", "service", etc.) -func getColorForType(typeName string) string { - // Map common types to specific colors for consistency - colorMap := map[string]string{ - "namespace": "#a25792ff", - "service": "#6ca100", - "pod": "#009596", - "node": "#8476d1", - "secret": "#c46100", - "configmap": "#b48c78ff", - "ingress": "#2e7dff", - "workloadmonitor": "#c46100", - "module": "#8b5cf6", - } - - if color, exists := colorMap[strings.ToLower(typeName)]; exists { - return color - } - - // Fall back to hash-based color generation - return hexColorForKind(typeName) -} - // ---------------- Automatic ID generation for UI elements ---------------- // generateElementID creates an ID for UI elements based on context and type @@ -282,7 +216,6 @@ type UnifiedResourceConfig struct { Title string Color string BadgeText string - Size BadgeSize } // createUnifiedFactory creates a factory using unified approach @@ -292,16 +225,9 @@ func createUnifiedFactory(config UnifiedResourceConfig, tabs []any, urlsToFetch // Create header with unified badge badgeConfig := BadgeConfig{ + Kind: config.Kind, Text: config.BadgeText, Color: config.Color, - Title: config.Title, - Size: config.Size, - } - if badgeConfig.Text == "" { - badgeConfig.Text = initialsFromKind(config.Kind) - } - if badgeConfig.Color == "" { - badgeConfig.Color = getColorForKind(config.Kind) } badge := createUnifiedBadge(generateBadgeID("header", config.Kind), badgeConfig) @@ -348,7 +274,9 @@ func createUnifiedFactory(config UnifiedResourceConfig, tabs []any, urlsToFetch // createUnifiedCustomColumn creates a custom column using unified approach func createUnifiedCustomColumn(name, jsonPath, kind, title, href string) map[string]any { - badgeConfig := generateBadgeConfig(kind, "", "", title) + badgeConfig := BadgeConfig{ + Kind: kind, + } badge := createUnifiedBadge(generateBadgeID("column", kind), badgeConfig) linkID := generateLinkID("column", "name") diff --git a/internal/lineagecontrollerwebhook/config.go b/internal/lineagecontrollerwebhook/config.go index c10c5a77..4ca45c13 100644 --- a/internal/lineagecontrollerwebhook/config.go +++ b/internal/lineagecontrollerwebhook/config.go @@ -38,6 +38,9 @@ func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, if !ok { return "", "", "", fmt.Errorf("failed to load chart-app mapping from config") } + if hr.Spec.Chart == nil { + return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name) + } s := hr.Spec.Chart.Spec val, ok := cfg.chartAppMap[chartRef{s.SourceRef.Name, s.Chart}] if !ok { diff --git a/internal/lineagecontrollerwebhook/webhook.go b/internal/lineagecontrollerwebhook/webhook.go index 1e7739b7..0841c891 100644 --- a/internal/lineagecontrollerwebhook/webhook.go +++ b/internal/lineagecontrollerwebhook/webhook.go @@ -26,6 +26,13 @@ var ( AncestryAmbiguous = fmt.Errorf("object ancestry is ambiguous") ) +const ( + ManagedObjectKey = "internal.cozystack.io/managed-by-cozystack" + ManagerGroupKey = "apps.cozystack.io/application.group" + ManagerKindKey = "apps.cozystack.io/application.kind" + ManagerNameKey = "apps.cozystack.io/application.name" +) + // getResourceSelectors returns the appropriate CozystackResourceDefinitionResources for a given GroupKind func (h *LineageControllerWebhook) getResourceSelectors(gk schema.GroupKind, crd *cozyv1alpha1.CozystackResourceDefinition) *cozyv1alpha1.CozystackResourceDefinitionResources { switch { @@ -91,7 +98,7 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req labels, err := h.computeLabels(ctx, obj) for { if err != nil && errors.Is(err, NoAncestors) { - return admission.Allowed("object not managed by app") + break // not a problem, mark object as unmanaged } if err != nil && errors.Is(err, AncestryAmbiguous) { warn = append(warn, "object ancestry ambiguous, using first ancestor found") @@ -119,7 +126,7 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstructured.Unstructured) (map[string]string, error) { owners := lineage.WalkOwnershipGraph(ctx, h.dynClient, h.mapper, h, o) if len(owners) == 0 { - return nil, NoAncestors + return map[string]string{ManagedObjectKey: "false"}, NoAncestors } obj, err := owners[0].GetUnstructured(ctx, h.dynClient, h.mapper) if err != nil { @@ -135,7 +142,8 @@ func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstruc } labels := map[string]string{ // truncate apigroup to first 63 chars - "apps.cozystack.io/application.group": func(s string) string { + ManagedObjectKey: "true", + ManagerGroupKey: func(s string) string { if len(s) < 63 { return s } @@ -145,8 +153,8 @@ func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstruc } return s }(gv.Group), - "apps.cozystack.io/application.kind": obj.GetKind(), - "apps.cozystack.io/application.name": obj.GetName(), + ManagerKindKey: obj.GetKind(), + ManagerNameKey: obj.GetName(), } templateLabels := map[string]string{ "kind": strings.ToLower(obj.GetKind()), diff --git a/packages/apps/kubernetes/images/cluster-autoscaler.tag b/packages/apps/kubernetes/images/cluster-autoscaler.tag index d5ab33d1..56fcb024 100644 --- a/packages/apps/kubernetes/images/cluster-autoscaler.tag +++ b/packages/apps/kubernetes/images/cluster-autoscaler.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d +ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:e1c0eb54d0ddadc8c7d72cee4f4282d56c957bf4ff0f62ef37db6b38dd0e8a01 diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag index d0e37aee..61e9f2ba 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:c8b08084a86251cdd18e237de89b695bca0e4f7eb1f1f6ddc2b903b4d74ea5ff +ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:47396dac04a24f824f82892c90efa160539e1e0f8ac697f5dc8ecf397e1ac413 diff --git a/packages/apps/kubernetes/templates/cluster.yaml b/packages/apps/kubernetes/templates/cluster.yaml index f275c979..2343a90a 100644 --- a/packages/apps/kubernetes/templates/cluster.yaml +++ b/packages/apps/kubernetes/templates/cluster.yaml @@ -147,7 +147,7 @@ spec: podAdditionalMetadata: labels: policy.cozystack.io/allow-to-etcd: "true" - replicas: 2 + replicas: {{ .Values.controlPlane.replicas }} version: {{ include "kubernetes.versionMap" $ }} --- apiVersion: cozystack.io/v1alpha1 diff --git a/packages/apps/kubernetes/templates/delete.yaml b/packages/apps/kubernetes/templates/delete.yaml new file mode 100644 index 00000000..ae581226 --- /dev/null +++ b/packages/apps/kubernetes/templates/delete.yaml @@ -0,0 +1,150 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "10" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed + name: {{ .Release.Name }}-cleanup +spec: + template: + metadata: + labels: + policy.cozystack.io/allow-to-apiserver: "true" + spec: + serviceAccountName: {{ .Release.Name }}-cleanup + restartPolicy: Never + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: "NoSchedule" + containers: + - name: kubectl + image: docker.io/clastix/kubectl:v1.32 + command: + - /bin/sh + - -c + - | + set -e + echo "Step 1: Suspending all HelmReleases with label cozystack.io/target-cluster-name={{ .Release.Name }}" + for hr in $(kubectl -n {{ .Release.Namespace }} get helmreleases.helm.toolkit.fluxcd.io -l "cozystack.io/target-cluster-name={{ .Release.Name }}" -o name 2>/dev/null || true); do + if [ -n "$hr" ]; then + echo " Suspending $hr" + kubectl -n {{ .Release.Namespace }} patch "$hr" \ + -p '{"spec": {"suspend": true}}' \ + --type=merge --field-manager=flux-client-side-apply + fi + done + + echo "Step 2: Deleting HelmReleases with label cozystack.io/target-cluster-name={{ .Release.Name }}" + kubectl -n {{ .Release.Namespace }} delete helmreleases.helm.toolkit.fluxcd.io \ + -l "cozystack.io/target-cluster-name={{ .Release.Name }}" \ + --ignore-not-found=true --wait=true + + echo "Step 3: Deleting KamajiControlPlane {{ .Release.Name }}" + kubectl -n {{ .Release.Namespace }} delete kamajicontrolplanes.controlplane.cluster.x-k8s.io {{ .Release.Name }} \ + --ignore-not-found=true + + echo "Step 4: Deleting TenantControlPlane {{ .Release.Name }}" + kubectl -n {{ .Release.Namespace }} delete tenantcontrolplanes.kamaji.clastix.io {{ .Release.Name }} \ + --ignore-not-found=true + + echo "Step 5: Cleaning up DataVolumes" + kubectl -n {{ .Release.Namespace }} delete datavolumes \ + -l "cluster.x-k8s.io/cluster-name={{ .Release.Name }}" \ + --ignore-not-found=true + + echo "Step 6: Cleaning up LoadBalancer Services" + kubectl -n {{ .Release.Namespace }} delete services \ + -l "cluster.x-k8s.io/cluster-name={{ .Release.Name }}" \ + --field-selector spec.type=LoadBalancer \ + --ignore-not-found=true + + echo "Cleanup completed successfully" + + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }}-cleanup + annotations: + helm.sh/hook: pre-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-failed,hook-succeeded + helm.sh/hook-weight: "0" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed + "helm.sh/hook-weight": "5" + name: {{ .Release.Name }}-cleanup +rules: + - apiGroups: + - "helm.toolkit.fluxcd.io" + resources: + - helmreleases + verbs: + - get + - list + - watch + - patch + - delete + - apiGroups: + - "controlplane.cluster.x-k8s.io" + resources: + - kamajicontrolplanes + verbs: + - get + - list + - watch + - delete + - apiGroups: + - "kamaji.clastix.io" + resources: + - tenantcontrolplanes + verbs: + - get + - list + - watch + - delete + - apiGroups: + - "cdi.kubevirt.io" + resources: + - datavolumes + verbs: + - get + - list + - watch + - delete + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed + "helm.sh/hook-weight": "5" + name: {{ .Release.Name }}-cleanup +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Release.Name }}-cleanup +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-cleanup + namespace: {{ .Release.Namespace }} diff --git a/packages/apps/kubernetes/templates/helmreleases/delete.yaml b/packages/apps/kubernetes/templates/helmreleases/delete.yaml deleted file mode 100644 index 5db40503..00000000 --- a/packages/apps/kubernetes/templates/helmreleases/delete.yaml +++ /dev/null @@ -1,103 +0,0 @@ ---- -apiVersion: batch/v1 -kind: Job -metadata: - annotations: - "helm.sh/hook": pre-delete - "helm.sh/hook-weight": "10" - "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed - name: {{ .Release.Name }}-flux-teardown -spec: - template: - spec: - serviceAccountName: {{ .Release.Name }}-flux-teardown - restartPolicy: Never - tolerations: - - key: CriticalAddonsOnly - operator: Exists - - key: node-role.kubernetes.io/control-plane - operator: Exists - effect: "NoSchedule" - containers: - - name: kubectl - image: docker.io/clastix/kubectl:v1.32 - command: - - /bin/sh - - -c - - | - kubectl - --namespace={{ .Release.Namespace }} - patch - helmrelease - {{ .Release.Name }}-cilium - {{ .Release.Name }}-gateway-api-crds - {{ .Release.Name }}-csi - {{ .Release.Name }}-cert-manager - {{ .Release.Name }}-cert-manager-crds - {{ .Release.Name }}-vertical-pod-autoscaler - {{ .Release.Name }}-vertical-pod-autoscaler-crds - {{ .Release.Name }}-ingress-nginx - {{ .Release.Name }}-fluxcd-operator - {{ .Release.Name }}-fluxcd - {{ .Release.Name }}-gpu-operator - {{ .Release.Name }}-velero - {{ .Release.Name }}-coredns - -p '{"spec": {"suspend": true}}' - --type=merge --field-manager=flux-client-side-apply || true ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ .Release.Name }}-flux-teardown - annotations: - helm.sh/hook: pre-delete - helm.sh/hook-delete-policy: before-hook-creation,hook-failed - helm.sh/hook-weight: "0" ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - annotations: - "helm.sh/hook": pre-install,post-install,pre-delete - "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed - "helm.sh/hook-weight": "5" - name: {{ .Release.Name }}-flux-teardown -rules: - - apiGroups: - - "helm.toolkit.fluxcd.io" - resources: - - helmreleases - verbs: - - get - - patch - resourceNames: - - {{ .Release.Name }}-cilium - - {{ .Release.Name }}-csi - - {{ .Release.Name }}-cert-manager - - {{ .Release.Name }}-cert-manager-crds - - {{ .Release.Name }}-vertical-pod-autoscaler - - {{ .Release.Name }}-vertical-pod-autoscaler-crds - - {{ .Release.Name }}-ingress-nginx - - {{ .Release.Name }}-fluxcd-operator - - {{ .Release.Name }}-fluxcd - - {{ .Release.Name }}-gpu-operator - - {{ .Release.Name }}-velero - - {{ .Release.Name }}-coredns - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - annotations: - helm.sh/hook: pre-delete - helm.sh/hook-delete-policy: hook-succeeded,before-hook-creation,hook-failed - helm.sh/hook-weight: "5" - name: {{ .Release.Name }}-flux-teardown -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ .Release.Name }}-flux-teardown -subjects: - - kind: ServiceAccount - name: {{ .Release.Name }}-flux-teardown - namespace: {{ .Release.Namespace }} diff --git a/packages/apps/mysql/templates/hooks/cleanup-pvc.yaml b/packages/apps/mysql/templates/hooks/cleanup-pvc.yaml new file mode 100644 index 00000000..e3fd5c32 --- /dev/null +++ b/packages/apps/mysql/templates/hooks/cleanup-pvc.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-cleanup + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + "helm.sh/hook": post-delete + "helm.sh/hook-weight": "10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + template: + metadata: + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + policy.cozystack.io/allow-to-apiserver: "true" + spec: + serviceAccountName: {{ .Release.Name }}-cleanup + restartPolicy: Never + containers: + - name: cleanup + image: docker.io/clastix/kubectl:v1.32 + command: + - /bin/sh + - -c + - | + echo "Deleting orphaned PVCs for {{ .Release.Name }}..." + kubectl delete pvc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} || true + echo "PVC cleanup complete." +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }}-cleanup + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + "helm.sh/hook": post-delete + helm.sh/hook-weight: "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-cleanup + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + "helm.sh/hook": post-delete + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +rules: + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Release.Name }}-cleanup + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + "helm.sh/hook": post-delete + helm.sh/hook-weight: "5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Release.Name }}-cleanup +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-cleanup diff --git a/packages/apps/nats/templates/nats.yaml b/packages/apps/nats/templates/nats.yaml index 9994332f..b05c87b5 100644 --- a/packages/apps/nats/templates/nats.yaml +++ b/packages/apps/nats/templates/nats.yaml @@ -1,6 +1,14 @@ {{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }} {{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }} +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }} {{- $passwords := dict }} + +{{- with (dig "data" (dict) $existingSecret) }} + {{- range $k, $v := . }} + {{- $_ := set $passwords $k (b64dec $v) }} + {{- end }} +{{- end }} + {{- range $user, $u := .Values.users }} {{- if $u.password }} {{- $_ := set $passwords $user $u.password }} @@ -47,18 +55,14 @@ spec: retries: -1 values: nats: - podTemplate: + container: merge: - spec: - containers: - - name: nats - image: nats:2.10.17-alpine - resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 22 }} + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 12 }} fullnameOverride: {{ .Release.Name }} config: - {{- if or (gt (len $passwords) 0) (gt (len .Values.config.merge) 0) }} + {{- if or $passwords .Values.config.merge }} merge: - {{- if gt (len $passwords) 0 }} + {{- if $passwords }} accounts: A: users: @@ -67,13 +71,13 @@ spec: password: "{{ $password }}" {{- end }} {{- end }} - {{- if and .Values.config (hasKey .Values.config "merge") }} - {{ toYaml .Values.config.merge | nindent 12 }} + {{- with .Values.config.merge }} + {{- toYaml . | nindent 10 }} {{- end }} {{- end }} - {{- if and .Values.config (hasKey .Values.config "resolver") }} + {{- with .Values.config.resolver }} resolver: - {{ toYaml .Values.config.resolver | nindent 12 }} + {{- toYaml . | nindent 10 }} {{- end }} cluster: enabled: true diff --git a/packages/apps/redis/templates/redisfailover.yaml b/packages/apps/redis/templates/redisfailover.yaml index f6d1ae05..20e91646 100644 --- a/packages/apps/redis/templates/redisfailover.yaml +++ b/packages/apps/redis/templates/redisfailover.yaml @@ -27,6 +27,7 @@ spec: replicas: 3 resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 6 }} redis: + image: "redis:8.2.0" resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 6 }} replicas: {{ .Values.replicas }} {{- with .Values.size }} diff --git a/packages/apps/tenant/templates/cleanup-job.yaml b/packages/apps/tenant/templates/cleanup-job.yaml new file mode 100644 index 00000000..9a1bcdd5 --- /dev/null +++ b/packages/apps/tenant/templates/cleanup-job.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "tenant.name" . }}-cleanup + namespace: {{ include "tenant.name" . }} + annotations: + helm.sh/hook: pre-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + helm.sh/hook-weight: "-5" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "tenant.name" . }}-cleanup + namespace: {{ include "tenant.name" . }} + annotations: + helm.sh/hook: pre-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + helm.sh/hook-weight: "-5" +rules: +- apiGroups: ["helm.toolkit.fluxcd.io"] + resources: ["helmreleases"] + verbs: ["get", "list", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "tenant.name" . }}-cleanup + namespace: {{ include "tenant.name" . }} + annotations: + helm.sh/hook: pre-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + helm.sh/hook-weight: "-5" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "tenant.name" . }}-cleanup +subjects: +- kind: ServiceAccount + name: {{ include "tenant.name" . }}-cleanup + namespace: {{ include "tenant.name" . }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "tenant.name" . }}-cleanup + namespace: {{ include "tenant.name" . }} + annotations: + helm.sh/hook: pre-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + helm.sh/hook-weight: "0" +spec: + ttlSecondsAfterFinished: 300 + template: + metadata: + name: {{ include "tenant.name" . }}-cleanup + labels: + policy.cozystack.io/allow-to-apiserver: "true" + spec: + serviceAccountName: {{ include "tenant.name" . }}-cleanup + restartPolicy: OnFailure + containers: + - name: cleanup + image: bitnami/kubectl:latest + command: + - /bin/bash + - -c + - | + set -e + NAMESPACE="{{ include "tenant.name" . }}" + + echo "Cleaning up HelmReleases in namespace: $NAMESPACE" + + echo "Deleting Applications" + kubectl delete helmreleases.helm.toolkit.fluxcd.io -n "$NAMESPACE" \ + -l 'cozystack.io/ui=true,internal.cozystack.io/tenantmodule!=true' \ + --ignore-not-found=true --wait=true + + echo "Deleting Tenant Modules" + kubectl delete helmreleases.helm.toolkit.fluxcd.io -n "$NAMESPACE" \ + -l 'cozystack.io/ui=true,internal.cozystack.io/tenantmodule=true' \ + --ignore-not-found=true --wait=true + + echo "Cleanup completed successfully" diff --git a/packages/apps/tenant/templates/networkpolicy.yaml b/packages/apps/tenant/templates/networkpolicy.yaml index 84df6d11..b66e85ff 100644 --- a/packages/apps/tenant/templates/networkpolicy.yaml +++ b/packages/apps/tenant/templates/networkpolicy.yaml @@ -20,11 +20,7 @@ metadata: name: allow-external-communication namespace: {{ include "tenant.name" . }} spec: - endpointSelector: - matchExpressions: - - key: policy.cozystack.io/allow-external-communication - operator: NotIn - values: ["false"] + endpointSelector: {} ingress: - fromEntities: - world diff --git a/packages/apps/tenant/templates/tenant.yaml b/packages/apps/tenant/templates/tenant.yaml index d58c37d4..54864baa 100644 --- a/packages/apps/tenant/templates/tenant.yaml +++ b/packages/apps/tenant/templates/tenant.yaml @@ -28,13 +28,13 @@ rules: - cozystack.io resources: - workloadmonitors + - workloads verbs: ["get", "list", "watch"] - apiGroups: - core.cozystack.io resources: - tenantmodules - tenantsecrets - - tenantsecretstables verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 @@ -113,6 +113,7 @@ rules: - cozystack.io resources: - workloadmonitors + - workloads verbs: ["get", "list", "watch"] --- kind: RoleBinding @@ -121,7 +122,7 @@ metadata: name: {{ include "tenant.name" . }}-view namespace: {{ include "tenant.name" . }} subjects: -{{ include "cozy-lib.rbac.subjectsForTenant" (list "view" (include "tenant.name" .)) | nindent 2 }} +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "view" (include "tenant.name" .)) | nindent 2 }} roleRef: kind: Role name: {{ include "tenant.name" . }}-view @@ -184,13 +185,13 @@ rules: - cozystack.io resources: - workloadmonitors + - workloads verbs: ["get", "list", "watch"] - apiGroups: - core.cozystack.io resources: - tenantmodules - tenantsecrets - - tenantsecretstables verbs: ["get", "list", "watch"] --- kind: RoleBinding @@ -199,7 +200,7 @@ metadata: name: {{ include "tenant.name" . }}-use namespace: {{ include "tenant.name" . }} subjects: -{{ include "cozy-lib.rbac.subjectsForTenant" (list "use" (include "tenant.name" .)) | nindent 2 }} +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" (include "tenant.name" .)) | nindent 2 }} roleRef: kind: Role name: {{ include "tenant.name" . }}-use @@ -282,13 +283,13 @@ rules: - cozystack.io resources: - workloadmonitors + - workloads verbs: ["get", "list", "watch"] - apiGroups: - core.cozystack.io resources: - tenantmodules - tenantsecrets - - tenantsecretstables verbs: ["get", "list", "watch"] --- kind: RoleBinding @@ -297,7 +298,7 @@ metadata: name: {{ include "tenant.name" . }}-admin namespace: {{ include "tenant.name" . }} subjects: -{{ include "cozy-lib.rbac.subjectsForTenant" (list "admin" (include "tenant.name" .)) | nindent 2 }} +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "admin" (include "tenant.name" .)) | nindent 2 }} roleRef: kind: Role name: {{ include "tenant.name" . }}-admin @@ -356,13 +357,13 @@ rules: - cozystack.io resources: - workloadmonitors + - workloads verbs: ["get", "list", "watch"] - apiGroups: - core.cozystack.io resources: - tenantmodules - tenantsecrets - - tenantsecretstables verbs: ["get", "list", "watch"] --- kind: RoleBinding @@ -371,7 +372,7 @@ metadata: name: {{ include "tenant.name" . }}-super-admin namespace: {{ include "tenant.name" . }} subjects: -{{ include "cozy-lib.rbac.subjectsForTenant" (list "super-admin" (include "tenant.name" .) ) | nindent 2 }} +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "super-admin" (include "tenant.name" .) ) | nindent 2 }} roleRef: kind: Role name: {{ include "tenant.name" . }}-super-admin diff --git a/packages/apps/virtual-machine/templates/service.yaml b/packages/apps/virtual-machine/templates/service.yaml index d9d77825..f212db62 100644 --- a/packages/apps/virtual-machine/templates/service.yaml +++ b/packages/apps/virtual-machine/templates/service.yaml @@ -28,27 +28,3 @@ spec: {{- end }} {{- end }} {{- end }} ---- -apiVersion: cilium.io/v2 -kind: CiliumNetworkPolicy -metadata: - name: {{ include "virtual-machine.fullname" . }} -spec: - endpointSelector: - matchLabels: - {{- include "virtual-machine.selectorLabels" . | nindent 6 }} - ingress: - - fromEntities: - - cluster - - fromEntities: - - world - {{- if eq .Values.externalMethod "PortList" }} - toPorts: - - ports: - {{- range .Values.externalPorts }} - - port: {{ quote . }} - {{- end }} - {{- end }} - egress: - - toEntities: - - world diff --git a/packages/apps/virtual-machine/templates/vm-update-hook.yaml b/packages/apps/virtual-machine/templates/vm-update-hook.yaml index 74f76cc5..1197c027 100644 --- a/packages/apps/virtual-machine/templates/vm-update-hook.yaml +++ b/packages/apps/virtual-machine/templates/vm-update-hook.yaml @@ -27,7 +27,11 @@ {{- if and $existingPVC $desiredStorage -}} {{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}} {{- if not (eq $currentStorage $desiredStorage) -}} - {{- $needResizePVC = true -}} + {{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}} + {{- $newSize := (include "cozy-lib.resources.toFloat" $desiredStorage) | float64 -}} + {{- if gt $newSize $oldSize -}} + {{- $needResizePVC = true -}} + {{- end -}} {{- end -}} {{- end -}} diff --git a/packages/apps/virtual-machine/templates/vm.yaml b/packages/apps/virtual-machine/templates/vm.yaml index 684e48c4..744ec220 100644 --- a/packages/apps/virtual-machine/templates/vm.yaml +++ b/packages/apps/virtual-machine/templates/vm.yaml @@ -62,7 +62,6 @@ spec: template: metadata: annotations: - policy.cozystack.io/allow-external-communication: "false" kubevirt.io/allow-pod-bridge-network-live-migration: "true" labels: {{- include "virtual-machine.labels" . | nindent 8 }} diff --git a/packages/apps/vm-disk/templates/pvc-resize-hook.yaml b/packages/apps/vm-disk/templates/pvc-resize-hook.yaml index 2619ed15..2454c599 100644 --- a/packages/apps/vm-disk/templates/pvc-resize-hook.yaml +++ b/packages/apps/vm-disk/templates/pvc-resize-hook.yaml @@ -1,5 +1,17 @@ {{- $existingPVC := lookup "v1" "PersistentVolumeClaim" .Release.Namespace .Release.Name }} -{{- if and $existingPVC (ne ($existingPVC.spec.resources.requests.storage | toString) .Values.storage) -}} +{{- $shouldResize := false -}} +{{- if and $existingPVC .Values.storage -}} + {{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}} + {{- if ne $currentStorage .Values.storage -}} + {{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}} + {{- $newSize := (include "cozy-lib.resources.toFloat" .Values.storage) | float64 -}} + {{- if gt $newSize $oldSize -}} + {{- $shouldResize = true -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{- if $shouldResize -}} apiVersion: batch/v1 kind: Job metadata: @@ -23,6 +35,7 @@ spec: command: ["sh", "-xec"] args: - | + echo "Resizing PVC to {{ .Values.storage }}..." kubectl patch pvc {{ .Release.Name }} -p '{"spec":{"resources":{"requests":{"storage":"{{ .Values.storage }}"}}}}' --- apiVersion: v1 diff --git a/packages/apps/vm-instance/templates/service.yaml b/packages/apps/vm-instance/templates/service.yaml index d1ef4df9..f212db62 100644 --- a/packages/apps/vm-instance/templates/service.yaml +++ b/packages/apps/vm-instance/templates/service.yaml @@ -28,27 +28,3 @@ spec: {{- end }} {{- end }} {{- end }} ---- -apiVersion: cilium.io/v2 -kind: CiliumNetworkPolicy -metadata: - name: {{ include "virtual-machine.fullname" . }} -spec: - endpointSelector: - matchLabels: - {{- include "virtual-machine.selectorLabels" . | nindent 6 }} - ingress: - - fromEntities: - - cluster - - fromEntities: - - world - {{- if eq .Values.externalMethod "PortList" }} - toPorts: - - ports: - {{- range .Values.externalPorts }} - - port: {{ quote . }} - {{- end }} - {{- end }} - egress: - - toEntities: - - world diff --git a/packages/apps/vm-instance/templates/vm.yaml b/packages/apps/vm-instance/templates/vm.yaml index c6082a93..1674337f 100644 --- a/packages/apps/vm-instance/templates/vm.yaml +++ b/packages/apps/vm-instance/templates/vm.yaml @@ -26,7 +26,6 @@ spec: template: metadata: annotations: - policy.cozystack.io/allow-external-communication: "false" kubevirt.io/allow-pod-bridge-network-live-migration: "true" labels: {{- include "virtual-machine.labels" . | nindent 8 }} diff --git a/packages/core/installer/hack/gen-profiles.sh b/packages/core/installer/hack/gen-profiles.sh index 2325b45f..bbe932d7 100755 --- a/packages/core/installer/hack/gen-profiles.sh +++ b/packages/core/installer/hack/gen-profiles.sh @@ -5,7 +5,7 @@ set -u TMPDIR=$(mktemp -d) PROFILES="initramfs kernel iso installer nocloud metal" FIRMWARES="amd-ucode amdgpu bnx2-bnx2x i915 intel-ice-firmware intel-ucode qlogic-firmware" -EXTENSIONS="drbd zfs lldpd" +EXTENSIONS="drbd zfs" mkdir -p images/talos/profiles @@ -90,7 +90,6 @@ input: - imageRef: ${QLOGIC_FIRMWARE_IMAGE} - imageRef: ${DRBD_IMAGE} - imageRef: ${ZFS_IMAGE} - - imageRef: ${LLDPD_IMAGE} output: kind: ${kind} imageOptions: ${image_options} diff --git a/packages/core/installer/images/talos/profiles/initramfs.yaml b/packages/core/installer/images/talos/profiles/initramfs.yaml index 7e2bdf1d..aec0e7e2 100644 --- a/packages/core/installer/images/talos/profiles/initramfs.yaml +++ b/packages/core/installer/images/talos/profiles/initramfs.yaml @@ -21,7 +21,6 @@ input: - imageRef: ghcr.io/siderolabs/qlogic-firmware:20250708@sha256:97124ee3594ab1529c8153b633f85f2d2de1252ee8222a77f81904dcabd76815 - imageRef: ghcr.io/siderolabs/drbd:9.2.14-v1.10.6@sha256:ca7fba878c5acb8fdfe130a39472a6c0a5c9dd74d65ba7507c09780a873b29c7 - imageRef: ghcr.io/siderolabs/zfs:2.3.3-v1.10.6@sha256:4952ef7306cf014823b6a66cf6d29840f4c6b7b362e36f9d6e853846c7dd0025 - - imageRef: ghcr.io/siderolabs/lldpd:1.0.19@sha256:73caa3c3a6c325970d0f527963f982698154d5f39c8c045b0fc2eb51d7da7b85 output: kind: initramfs imageOptions: {} diff --git a/packages/core/installer/images/talos/profiles/installer.yaml b/packages/core/installer/images/talos/profiles/installer.yaml index 7ad604a2..848224ee 100644 --- a/packages/core/installer/images/talos/profiles/installer.yaml +++ b/packages/core/installer/images/talos/profiles/installer.yaml @@ -21,7 +21,6 @@ input: - imageRef: ghcr.io/siderolabs/qlogic-firmware:20250708@sha256:97124ee3594ab1529c8153b633f85f2d2de1252ee8222a77f81904dcabd76815 - imageRef: ghcr.io/siderolabs/drbd:9.2.14-v1.10.6@sha256:ca7fba878c5acb8fdfe130a39472a6c0a5c9dd74d65ba7507c09780a873b29c7 - imageRef: ghcr.io/siderolabs/zfs:2.3.3-v1.10.6@sha256:4952ef7306cf014823b6a66cf6d29840f4c6b7b362e36f9d6e853846c7dd0025 - - imageRef: ghcr.io/siderolabs/lldpd:1.0.19@sha256:73caa3c3a6c325970d0f527963f982698154d5f39c8c045b0fc2eb51d7da7b85 output: kind: installer imageOptions: {} diff --git a/packages/core/installer/images/talos/profiles/iso.yaml b/packages/core/installer/images/talos/profiles/iso.yaml index 326917e4..7c1aaf9a 100644 --- a/packages/core/installer/images/talos/profiles/iso.yaml +++ b/packages/core/installer/images/talos/profiles/iso.yaml @@ -21,7 +21,6 @@ input: - imageRef: ghcr.io/siderolabs/qlogic-firmware:20250708@sha256:97124ee3594ab1529c8153b633f85f2d2de1252ee8222a77f81904dcabd76815 - imageRef: ghcr.io/siderolabs/drbd:9.2.14-v1.10.6@sha256:ca7fba878c5acb8fdfe130a39472a6c0a5c9dd74d65ba7507c09780a873b29c7 - imageRef: ghcr.io/siderolabs/zfs:2.3.3-v1.10.6@sha256:4952ef7306cf014823b6a66cf6d29840f4c6b7b362e36f9d6e853846c7dd0025 - - imageRef: ghcr.io/siderolabs/lldpd:1.0.19@sha256:73caa3c3a6c325970d0f527963f982698154d5f39c8c045b0fc2eb51d7da7b85 output: kind: iso imageOptions: {} diff --git a/packages/core/installer/images/talos/profiles/kernel.yaml b/packages/core/installer/images/talos/profiles/kernel.yaml index 89cf5371..86079632 100644 --- a/packages/core/installer/images/talos/profiles/kernel.yaml +++ b/packages/core/installer/images/talos/profiles/kernel.yaml @@ -21,7 +21,6 @@ input: - imageRef: ghcr.io/siderolabs/qlogic-firmware:20250708@sha256:97124ee3594ab1529c8153b633f85f2d2de1252ee8222a77f81904dcabd76815 - imageRef: ghcr.io/siderolabs/drbd:9.2.14-v1.10.6@sha256:ca7fba878c5acb8fdfe130a39472a6c0a5c9dd74d65ba7507c09780a873b29c7 - imageRef: ghcr.io/siderolabs/zfs:2.3.3-v1.10.6@sha256:4952ef7306cf014823b6a66cf6d29840f4c6b7b362e36f9d6e853846c7dd0025 - - imageRef: ghcr.io/siderolabs/lldpd:1.0.19@sha256:73caa3c3a6c325970d0f527963f982698154d5f39c8c045b0fc2eb51d7da7b85 output: kind: kernel imageOptions: {} diff --git a/packages/core/installer/images/talos/profiles/metal.yaml b/packages/core/installer/images/talos/profiles/metal.yaml index 8e1f1ebb..2619fd63 100644 --- a/packages/core/installer/images/talos/profiles/metal.yaml +++ b/packages/core/installer/images/talos/profiles/metal.yaml @@ -21,7 +21,6 @@ input: - imageRef: ghcr.io/siderolabs/qlogic-firmware:20250708@sha256:97124ee3594ab1529c8153b633f85f2d2de1252ee8222a77f81904dcabd76815 - imageRef: ghcr.io/siderolabs/drbd:9.2.14-v1.10.6@sha256:ca7fba878c5acb8fdfe130a39472a6c0a5c9dd74d65ba7507c09780a873b29c7 - imageRef: ghcr.io/siderolabs/zfs:2.3.3-v1.10.6@sha256:4952ef7306cf014823b6a66cf6d29840f4c6b7b362e36f9d6e853846c7dd0025 - - imageRef: ghcr.io/siderolabs/lldpd:1.0.19@sha256:73caa3c3a6c325970d0f527963f982698154d5f39c8c045b0fc2eb51d7da7b85 output: kind: image imageOptions: { diskSize: 1306525696, diskFormat: raw } diff --git a/packages/core/installer/images/talos/profiles/nocloud.yaml b/packages/core/installer/images/talos/profiles/nocloud.yaml index c9604759..8db68208 100644 --- a/packages/core/installer/images/talos/profiles/nocloud.yaml +++ b/packages/core/installer/images/talos/profiles/nocloud.yaml @@ -21,7 +21,6 @@ input: - imageRef: ghcr.io/siderolabs/qlogic-firmware:20250708@sha256:97124ee3594ab1529c8153b633f85f2d2de1252ee8222a77f81904dcabd76815 - imageRef: ghcr.io/siderolabs/drbd:9.2.14-v1.10.6@sha256:ca7fba878c5acb8fdfe130a39472a6c0a5c9dd74d65ba7507c09780a873b29c7 - imageRef: ghcr.io/siderolabs/zfs:2.3.3-v1.10.6@sha256:4952ef7306cf014823b6a66cf6d29840f4c6b7b362e36f9d6e853846c7dd0025 - - imageRef: ghcr.io/siderolabs/lldpd:1.0.19@sha256:73caa3c3a6c325970d0f527963f982698154d5f39c8c045b0fc2eb51d7da7b85 output: kind: image imageOptions: { diskSize: 1306525696, diskFormat: raw } diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index fc7c81fa..5d1caa27 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -1,2 +1,2 @@ cozystack: - image: ghcr.io/cozystack/cozystack/installer:v0.37.0@sha256:256c5a0f0ae2fc3ad6865b9fda74c42945b38a5384240fa29554617185b60556 + image: ghcr.io/cozystack/cozystack/installer:v0.37.10@sha256:df2e6d4cf91636f8b6e8553fe61fdfd48945fe97f22086f5d9c9fc5bac89fd71 diff --git a/packages/core/platform/bundles/distro-full.yaml b/packages/core/platform/bundles/distro-full.yaml index fbcf2ba9..be35482a 100644 --- a/packages/core/platform/bundles/distro-full.yaml +++ b/packages/core/platform/bundles/distro-full.yaml @@ -68,6 +68,12 @@ releases: disableTelemetry: true {{- end }} +- name: lineage-controller-webhook + releaseName: lineage-controller-webhook + chart: cozy-lineage-controller-webhook + namespace: cozy-system + dependsOn: [cozystack-controller,cilium,cert-manager] + - name: cert-manager releaseName: cert-manager chart: cozy-cert-manager diff --git a/packages/core/platform/bundles/distro-hosted.yaml b/packages/core/platform/bundles/distro-hosted.yaml index c24232c6..d486399d 100644 --- a/packages/core/platform/bundles/distro-hosted.yaml +++ b/packages/core/platform/bundles/distro-hosted.yaml @@ -36,6 +36,12 @@ releases: disableTelemetry: true {{- end }} +- name: lineage-controller-webhook + releaseName: lineage-controller-webhook + chart: cozy-lineage-controller-webhook + namespace: cozy-system + dependsOn: [cozystack-controller,cert-manager] + - name: cert-manager releaseName: cert-manager chart: cozy-cert-manager diff --git a/packages/core/platform/bundles/paas-full.yaml b/packages/core/platform/bundles/paas-full.yaml index f4274eda..f02dbc40 100644 --- a/packages/core/platform/bundles/paas-full.yaml +++ b/packages/core/platform/bundles/paas-full.yaml @@ -105,6 +105,12 @@ releases: disableTelemetry: true {{- end }} +- name: lineage-controller-webhook + releaseName: lineage-controller-webhook + chart: cozy-lineage-controller-webhook + namespace: cozy-system + dependsOn: [cozystack-controller,cilium,kubeovn,cert-manager] + - name: cozystack-resource-definition-crd releaseName: cozystack-resource-definition-crd chart: cozystack-resource-definition-crd diff --git a/packages/core/platform/bundles/paas-hosted.yaml b/packages/core/platform/bundles/paas-hosted.yaml index 5526d2a7..a76b8b59 100644 --- a/packages/core/platform/bundles/paas-hosted.yaml +++ b/packages/core/platform/bundles/paas-hosted.yaml @@ -52,6 +52,12 @@ releases: disableTelemetry: true {{- end }} +- name: lineage-controller-webhook + releaseName: lineage-controller-webhook + chart: cozy-lineage-controller-webhook + namespace: cozy-system + dependsOn: [cozystack-controller,cert-manager] + - name: cozystack-resource-definition-crd releaseName: cozystack-resource-definition-crd chart: cozystack-resource-definition-crd diff --git a/packages/core/testing/values.yaml b/packages/core/testing/values.yaml index bcc94ce3..2d93ec5c 100755 --- a/packages/core/testing/values.yaml +++ b/packages/core/testing/values.yaml @@ -1,2 +1,2 @@ e2e: - image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.37.0@sha256:10afd0a6c39248ec41d0e59ff1bc6c29bd0075b7cc9a512b01cf603ef39c33ea + image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.37.10@sha256:0a452cc4795500b094068330c23bb3e579f0fc3fe230b9371a394c023c415249 diff --git a/packages/extra/bootbox/images/matchbox.tag b/packages/extra/bootbox/images/matchbox.tag index 6060afa4..b02335a2 100644 --- a/packages/extra/bootbox/images/matchbox.tag +++ b/packages/extra/bootbox/images/matchbox.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/matchbox:v0.37.0@sha256:5cca5f56b755285aefa11b1052fe55e1aa83b25bae34aef80cdb77ff63091044 +ghcr.io/cozystack/cozystack/matchbox:v0.37.10@sha256:5ebd5dca8c4adac48362f0c11de9696ce675a7edf184029e0118351dd701c730 diff --git a/packages/extra/ingress/templates/nginx-ingress.yaml b/packages/extra/ingress/templates/nginx-ingress.yaml index faa0b061..e28918e4 100644 --- a/packages/extra/ingress/templates/nginx-ingress.yaml +++ b/packages/extra/ingress/templates/nginx-ingress.yaml @@ -1,6 +1,6 @@ {{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }} {{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }} -{{- $exposeExternalIPs := (index $cozyConfig.data "expose-external-ips") | default "" }} +{{- $exposeExternalIPs := (index $cozyConfig.data "expose-external-ips") | default "" | nospace }} apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: diff --git a/packages/extra/monitoring/README.md b/packages/extra/monitoring/README.md index c97b13de..06dbc97e 100644 --- a/packages/extra/monitoring/README.md +++ b/packages/extra/monitoring/README.md @@ -72,6 +72,8 @@ | `alerta.alerts.telegram.token` | Telegram token for your bot | `string` | `""` | | `alerta.alerts.telegram.chatID` | Specify multiple ID's separated by comma. Get yours in https://t.me/chatid_echo_bot | `string` | `""` | | `alerta.alerts.telegram.disabledSeverity` | List of severity without alerts, separated by comma like: "informational,warning" | `string` | `""` | +| `alerta.alerts.slack` | Configuration for Slack alerts | `*object` | `null` | +| `alerta.alerts.slack.url` | Configuration uri for Slack alerts | `*string` | `""` | ### Grafana configuration diff --git a/packages/extra/monitoring/templates/alerta/alerta.yaml b/packages/extra/monitoring/templates/alerta/alerta.yaml index bec7b825..71336286 100644 --- a/packages/extra/monitoring/templates/alerta/alerta.yaml +++ b/packages/extra/monitoring/templates/alerta/alerta.yaml @@ -109,9 +109,20 @@ spec: - name: AUTH_REQUIRED value: "True" + {{- $plugins := list }} {{- if and .Values.alerta.alerts.telegram.chatID .Values.alerta.alerts.telegram.token }} + {{- $plugins = append $plugins "telegram" }} + {{- end }} + {{- if .Values.alerta.alerts.slack.url }} + {{- $plugins = append $plugins "slack" }} + {{- end }} + + {{- if gt (len $plugins) 0 }} - name: "PLUGINS" - value: "telegram" + value: "{{ default "" (join "," $plugins) }}" + {{- end }} + + {{- if and .Values.alerta.alerts.telegram.chatID .Values.alerta.alerts.telegram.token }} - name: TELEGRAM_CHAT_ID value: "{{ .Values.alerta.alerts.telegram.chatID }}" - name: TELEGRAM_TOKEN @@ -122,6 +133,11 @@ spec: value: "{{ .Values.alerta.alerts.telegram.disabledSeverity }}" {{- end }} + {{- if .Values.alerta.alerts.slack.url }} + - name: "SLACK_WEBHOOK_URL" + value: "{{ .Values.alerta.alerts.slack.url }}" + {{- end }} + ports: - name: http containerPort: 8080 diff --git a/packages/extra/monitoring/values.schema.json b/packages/extra/monitoring/values.schema.json index 36f1c0c3..505b5bef 100644 --- a/packages/extra/monitoring/values.schema.json +++ b/packages/extra/monitoring/values.schema.json @@ -12,6 +12,17 @@ "type": "object", "default": {}, "properties": { + "slack": { + "description": "Configuration for Slack alerts", + "type": "object", + "default": {}, + "properties": { + "url": { + "description": "Configuration uri for Slack alerts", + "type": "string" + } + } + }, "telegram": { "description": "Configuration for Telegram alerts", "type": "object", diff --git a/packages/extra/monitoring/values.yaml b/packages/extra/monitoring/values.yaml index 02155ce0..72aab9a9 100644 --- a/packages/extra/monitoring/values.yaml +++ b/packages/extra/monitoring/values.yaml @@ -90,6 +90,8 @@ logsStorages: ## @field telegramAlerts.token {string} Telegram token for your bot ## @field telegramAlerts.chatID {string} Specify multiple ID's separated by comma. Get yours in https://t.me/chatid_echo_bot ## @field telegramAlerts.disabledSeverity {string} List of severity without alerts, separated by comma like: "informational,warning" +## @field alerts.slack {*slackAlerts} Configuration for Slack alerts +## @field slackAlerts.url {*string} Configuration uri for Slack alerts alerta: storage: 10Gi storageClassName: "" @@ -112,6 +114,9 @@ alerta: chatID: "" disabledSeverity: "" + slack: + url: "" + ## @section Grafana configuration ## @param grafana {grafana} Configuration for Grafana diff --git a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag index e16a832d..6fa6e9d3 100644 --- a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag +++ b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.37.0@sha256:f166f09cdc9cdbb758209883819ab8261a3793bc1d7a6b6685efd5a2b2930847 +ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.37.10@sha256:233a1177f309aa159a8c4a08212dab3f992f5a2b626f86a15c8ffd44f10bf2ad diff --git a/packages/library/cozy-lib/templates/_resources.tpl b/packages/library/cozy-lib/templates/_resources.tpl index 637d04f6..3976f442 100644 --- a/packages/library/cozy-lib/templates/_resources.tpl +++ b/packages/library/cozy-lib/templates/_resources.tpl @@ -154,7 +154,7 @@ {{- $resources := index . 1 }} {{- $global := index . 2 }} {{- $presetMap := include "cozy-lib.resources.unsanitizedPreset" $preset | fromYaml }} -{{- $mergedMap := deepCopy $resources | mergeOverwrite $presetMap }} +{{- $mergedMap := deepCopy (default (dict) $resources) | mergeOverwrite $presetMap }} {{- include "cozy-lib.resources.sanitize" (list $mergedMap $global) }} {{- end }} @@ -174,15 +174,68 @@ {{- end }} {{- define "cozy-lib.resources.flatten" -}} +{{- /* +This helper either outputs raw ResourceQuota fields (e.g., services.loadbalancers) +as-is, or flattens sanitized resource maps into limits.* / requests.* keys. + +If ALL keys in the input are recognized quota keys (pods, services.*, etc.), +the input is output directly as YAML. Otherwise, the input is treated as a +resource map and processed through sanitize + flatten. + +Do not mix quota keys and resource keys in a single call. +*/ -}} +{{- $input := index . 0 -}} +{{- $ctx := index . 1 -}} + +{{- $rawQuotaKeys := list +"pods" +"services" +"services.loadbalancers" +"services.nodeports" +"services.clusterip" +"configmaps" +"secrets" +"persistentvolumeclaims" +"replicationcontrollers" +"resourcequotas" +-}} + +{{- $computeKeys := list +"cpu" +"memory" +"ephemeral-storage" +-}} + {{- $out := dict -}} -{{- $res := include "cozy-lib.resources.sanitize" . | fromYaml -}} +{{- $computeResources := dict -}} +{{- $quotaResources := dict -}} + +{{- range $k, $v := $input }} +{{- if or (has $k $computeKeys) (hasPrefix "limits." $k) (hasPrefix "requests." $k) }} +{{- $_ := set $computeResources $k $v }} +{{- else if has $k $rawQuotaKeys }} +{{- $_ := set $quotaResources $k $v }} +{{- else }} +{{- $_ := set $computeResources $k $v }} +{{- end }} +{{- end }} + +{{- if $computeResources }} +{{- $res := include "cozy-lib.resources.sanitize" (list $computeResources (index . 1)) | fromYaml -}} {{- range $section, $values := $res }} - {{- range $k, $v := $values }} - {{- $key := printf "%s.%s" $section $k }} - {{- if ne $key "limits.storage" }} - {{- $_ := set $out $key $v }} - {{- end }} - {{- end }} +{{- range $k, $v := $values }} +{{- $key := printf "%s.%s" $section $k }} +{{- if ne $key "limits.storage" }} +{{- $_ := set $out $key $v }} {{- end }} +{{- end }} +{{- end }} +{{- end }} + +{{- range $k, $v := $quotaResources }} +{{- $_ := set $out $k $v }} +{{- end }} + {{- $out | toYaml }} + {{- end }} diff --git a/packages/system/bucket/images/s3manager.tag b/packages/system/bucket/images/s3manager.tag index 96054223..ec7741c2 100644 --- a/packages/system/bucket/images/s3manager.tag +++ b/packages/system/bucket/images/s3manager.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:7348bec610f08bd902c88c9a9f28fdd644727e2728a1e4103f88f0c99febd5e7 +ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:b58f5b192d1f47026c19ad51a857e31c256a9ff46c24a5fbbfcfe4f61981b8eb diff --git a/packages/system/cozystack-api/templates/api-ingress.yaml b/packages/system/cozystack-api/templates/api-ingress.yaml index d7670e71..54fdf54b 100644 --- a/packages/system/cozystack-api/templates/api-ingress.yaml +++ b/packages/system/cozystack-api/templates/api-ingress.yaml @@ -10,6 +10,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/backend-protocol: HTTPS nginx.ingress.kubernetes.io/ssl-passthrough: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" name: kubernetes namespace: default spec: diff --git a/packages/system/cozystack-api/templates/apiservice.yaml b/packages/system/cozystack-api/templates/apiservice.yaml index d0ab1185..3cd3665b 100644 --- a/packages/system/cozystack-api/templates/apiservice.yaml +++ b/packages/system/cozystack-api/templates/apiservice.yaml @@ -1,9 +1,10 @@ apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: + annotations: + cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/cozystack-api" name: v1alpha1.apps.cozystack.io spec: - insecureSkipTLSVerify: true group: apps.cozystack.io groupPriorityMinimum: 1000 versionPriority: 15 @@ -15,9 +16,10 @@ spec: apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: + annotations: + cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/cozystack-api" name: v1alpha1.core.cozystack.io spec: - insecureSkipTLSVerify: true group: core.cozystack.io groupPriorityMinimum: 1000 versionPriority: 15 diff --git a/packages/system/cozystack-api/templates/certmanager.yaml b/packages/system/cozystack-api/templates/certmanager.yaml new file mode 100644 index 00000000..def27bd1 --- /dev/null +++ b/packages/system/cozystack-api/templates/certmanager.yaml @@ -0,0 +1,45 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: cozystack-api-selfsigned + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: cozystack-api-ca + namespace: {{ .Release.Namespace }} +spec: + secretName: cozystack-api-ca + duration: 43800h # 5 years + commonName: cozystack-api-ca + issuerRef: + name: cozystack-api-selfsigned + isCA: true +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: cozystack-api-ca + namespace: {{ .Release.Namespace }} +spec: + ca: + secretName: cozystack-api-ca +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: cozystack-api + namespace: {{ .Release.Namespace }} +spec: + secretName: cozystack-api-cert + duration: 8760h + renewBefore: 720h + issuerRef: + name: cozystack-api-ca + commonName: cozystack-api + dnsNames: + - cozystack-api + - cozystack-api.{{ .Release.Namespace }}.svc diff --git a/packages/system/cozystack-api/templates/deployment.yaml b/packages/system/cozystack-api/templates/deployment.yaml index 46779d2b..1a63a0e0 100644 --- a/packages/system/cozystack-api/templates/deployment.yaml +++ b/packages/system/cozystack-api/templates/deployment.yaml @@ -1,12 +1,18 @@ apiVersion: apps/v1 +{{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} +kind: DaemonSet +{{- else }} kind: Deployment +{{- end }} metadata: name: cozystack-api namespace: cozy-system labels: app: cozystack-api spec: - replicas: 2 + {{- if not .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} + replicas: {{ .Values.cozystackAPI.replicas }} + {{- end }} selector: matchLabels: app: cozystack-api @@ -16,6 +22,35 @@ spec: app: cozystack-api spec: serviceAccountName: cozystack-api + {{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} + nodeSelector: + node-role.kubernetes.io/control-plane: "" + {{- end }} containers: - name: cozystack-api + args: + - --tls-cert-file=/tmp/cozystack-api-certs/tls.crt + - --tls-private-key-file=/tmp/cozystack-api-certs/tls.key + {{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} + env: + - name: KUBERNETES_SERVICE_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: KUBERNETES_SERVICE_PORT + value: "6443" + {{- end }} image: "{{ .Values.cozystackAPI.image }}" + ports: + - containerPort: 443 + name: https + volumeMounts: + - name: cozystack-api-certs + mountPath: /tmp/cozystack-api-certs + readOnly: true + volumes: + - name: cozystack-api-certs + secret: + secretName: cozystack-api-cert + defaultMode: 0400 diff --git a/packages/system/cozystack-api/templates/rbac.yaml b/packages/system/cozystack-api/templates/rbac.yaml index 1a169d86..0429b9ef 100644 --- a/packages/system/cozystack-api/templates/rbac.yaml +++ b/packages/system/cozystack-api/templates/rbac.yaml @@ -4,7 +4,10 @@ metadata: name: cozystack-api rules: - apiGroups: [""] - resources: ["namespaces", "secrets"] + resources: ["namespaces", "secrets", "services"] + verbs: ["get", "watch", "list"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["rolebindings"] verbs: ["get", "watch", "list"] - apiGroups: [""] resources: ["secrets"] diff --git a/packages/system/cozystack-api/templates/service.yaml b/packages/system/cozystack-api/templates/service.yaml index 2dcd618b..abe67abc 100644 --- a/packages/system/cozystack-api/templates/service.yaml +++ b/packages/system/cozystack-api/templates/service.yaml @@ -4,9 +4,12 @@ metadata: name: cozystack-api namespace: cozy-system spec: + {{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} + internalTrafficPolicy: Local + {{- end }} ports: - port: 443 protocol: TCP - targetPort: 443 + targetPort: https selector: app: cozystack-api diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index 1b68eff4..a6e2d052 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,2 +1,5 @@ cozystackAPI: - image: ghcr.io/cozystack/cozystack/cozystack-api:v0.37.0@sha256:19d89e8afb90ce38ab7e42ecedfc28402f7c0b56f30957db957c5415132ff6ca + image: ghcr.io/cozystack/cozystack/cozystack-api:v0.37.10@sha256:04772e898edbab0613ff2beb0b7d62196b47d986488ef0c67b86946b69b30f24 + localK8sAPIEndpoint: + enabled: true + replicas: 2 diff --git a/packages/system/cozystack-controller/templates/certmanager.yaml b/packages/system/cozystack-controller/templates/certmanager.yaml deleted file mode 100644 index 6d9bce7d..00000000 --- a/packages/system/cozystack-controller/templates/certmanager.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: cozystack-controller-webhook-selfsigned - namespace: {{ .Release.Namespace }} -spec: - selfSigned: {} ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: cozystack-controller-webhook-ca - namespace: {{ .Release.Namespace }} -spec: - secretName: cozystack-controller-webhook-ca - duration: 43800h # 5 years - commonName: cozystack-controller-webhook-ca - issuerRef: - name: cozystack-controller-webhook-selfsigned - isCA: true ---- -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: cozystack-controller-webhook-ca - namespace: {{ .Release.Namespace }} -spec: - ca: - secretName: cozystack-controller-webhook-ca ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: cozystack-controller-webhook - namespace: {{ .Release.Namespace }} -spec: - secretName: cozystack-controller-webhook-cert - duration: 8760h - renewBefore: 720h - issuerRef: - name: cozystack-controller-webhook-ca - commonName: cozystack-controller - dnsNames: - - cozystack-controller - - cozystack-controller.{{ .Release.Namespace }}.svc diff --git a/packages/system/cozystack-controller/templates/deployment.yaml b/packages/system/cozystack-controller/templates/deployment.yaml index 318ec3b2..6dc21b1c 100644 --- a/packages/system/cozystack-controller/templates/deployment.yaml +++ b/packages/system/cozystack-controller/templates/deployment.yaml @@ -28,15 +28,6 @@ spec: {{- if .Values.cozystackController.disableTelemetry }} - --disable-telemetry {{- end }} - ports: - - name: webhook - containerPort: 9443 - volumeMounts: - - name: webhook-certs - mountPath: /tmp/k8s-webhook-server/serving-certs - readOnly: true - volumes: - - name: webhook-certs - secret: - secretName: cozystack-controller-webhook-cert - defaultMode: 0400 + {{- if eq .Values.cozystackController.cozystackAPIKind "Deployment" }} + - --reconcile-deployment + {{- end }} diff --git a/packages/system/cozystack-controller/templates/role.yaml b/packages/system/cozystack-controller/templates/role.yaml index 96bfc9a5..734ba95b 100644 --- a/packages/system/cozystack-controller/templates/role.yaml +++ b/packages/system/cozystack-controller/templates/role.yaml @@ -5,7 +5,7 @@ metadata: namespace: cozy-system rules: - apiGroups: ["apps"] - resources: ["deployments"] + resources: ["deployments", "daemonsets"] resourceNames: ["cozystack-api"] verbs: ["patch", "update"] diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index f651807e..1b222742 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -1,5 +1,6 @@ cozystackController: - image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.37.0@sha256:845b8e68cbc277c2303080bcd55597e4334610d396dad258ad56fd906530acc3 + image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.37.10@sha256:224871a6e8f7a80d2b6476b57b6176c472afe112b503a5c5bf151b945d78a407 debug: false disableTelemetry: false - cozystackVersion: "v0.37.0" + cozystackVersion: "v0.37.10" + cozystackAPIKind: "DaemonSet" diff --git a/packages/system/cozystack-resource-definitions/cozyrds/monitoring.yaml b/packages/system/cozystack-resource-definitions/cozyrds/monitoring.yaml index af99f73c..82271a35 100644 --- a/packages/system/cozystack-resource-definitions/cozyrds/monitoring.yaml +++ b/packages/system/cozystack-resource-definitions/cozyrds/monitoring.yaml @@ -8,7 +8,7 @@ spec: singular: monitoring plural: monitorings openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"alerta":{"description":"Configuration for Alerta service","type":"object","default":{},"properties":{"alerts":{"description":"Configuration for alerts","type":"object","default":{},"properties":{"telegram":{"description":"Configuration for Telegram alerts","type":"object","default":{},"required":["chatID","disabledSeverity","token"],"properties":{"chatID":{"description":"Specify multiple ID's separated by comma. Get yours in https://t.me/chatid_echo_bot","type":"string"},"disabledSeverity":{"description":"List of severity without alerts, separated by comma like: \"informational,warning\"","type":"string"},"token":{"description":"Telegram token for your bot","type":"string"}}}}},"resources":{"description":"Resources configuration","type":"object","default":{},"properties":{"limits":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"requests":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}},"storage":{"description":"Persistent Volume size for the database","type":"string","default":"10Gi"},"storageClassName":{"description":"StorageClass used to store the data","type":"string"}}},"grafana":{"description":"Configuration for Grafana","type":"object","default":{},"properties":{"db":{"description":"Database configuration","type":"object","default":{},"properties":{"size":{"description":"Persistent Volume size for the database","type":"string","default":"10Gi"}}},"resources":{"description":"Resources configuration","type":"object","default":{},"properties":{"limits":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"requests":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}}}},"host":{"description":"The hostname used to access the grafana externally (defaults to 'grafana' subdomain for the tenant host).","type":"string"},"logsStorages":{"description":"Configuration of logs storage instances","type":"array","default":[{"name":"generic","retentionPeriod":"1","storage":"10Gi","storageClassName":"replicated"}],"items":{"type":"object","required":["name","retentionPeriod","storage"],"properties":{"name":{"description":"Name of the storage instance","type":"string"},"retentionPeriod":{"description":"Retention period for the logs in the storage instance","type":"string","default":"1"},"storage":{"description":"Persistent Volume size for the storage instance","type":"string","default":"10Gi"},"storageClassName":{"description":"StorageClass used to store the data","type":"string","default":"replicated"}}}},"metricsStorages":{"description":"Configuration of metrics storage instances","type":"array","default":[{"deduplicationInterval":"15s","name":"shortterm","retentionPeriod":"3d","storage":"10Gi","storageClassName":""},{"deduplicationInterval":"5m","name":"longterm","retentionPeriod":"14d","storage":"10Gi","storageClassName":""}],"items":{"type":"object","required":["deduplicationInterval","name","retentionPeriod","storage"],"properties":{"deduplicationInterval":{"description":"Deduplication interval for the metrics in the storage instance","type":"string"},"name":{"description":"Name of the storage instance","type":"string"},"retentionPeriod":{"description":"Retention period for the metrics in the storage instance","type":"string"},"storage":{"description":"Persistent Volume size for the storage instance","type":"string","default":"10Gi"},"storageClassName":{"description":"StorageClass used to store the data","type":"string"},"vminsert":{"description":"Configuration for vminsert component of the storage instance","type":"object","properties":{"maxAllowed":{"description":"Limits (maximum allowed/available resources )","type":"object","properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"minAllowed":{"description":"Requests (minimum allowed/available resources)","type":"object","properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}},"vmselect":{"description":"Configuration for vmselect component of the storage instance","type":"object","properties":{"maxAllowed":{"description":"Limits (maximum allowed/available resources )","type":"object","properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"minAllowed":{"description":"Requests (minimum allowed/available resources)","type":"object","properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}},"vmstorage":{"description":"Configuration for vmstorage component of the storage instance","type":"object","properties":{"maxAllowed":{"description":"Limits (maximum allowed/available resources )","type":"object","properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"minAllowed":{"description":"Requests (minimum allowed/available resources)","type":"object","properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}}}}}}} + {"title":"Chart Values","type":"object","properties":{"alerta":{"description":"Configuration for Alerta service","type":"object","default":{},"properties":{"alerts":{"description":"Configuration for alerts","type":"object","default":{},"properties":{"slack":{"description":"Configuration for Slack alerts","type":"object","default":{},"properties":{"url":{"description":"Configuration uri for Slack alerts","type":"string"}}},"telegram":{"description":"Configuration for Telegram alerts","type":"object","default":{},"required":["chatID","disabledSeverity","token"],"properties":{"chatID":{"description":"Specify multiple ID's separated by comma. Get yours in https://t.me/chatid_echo_bot","type":"string"},"disabledSeverity":{"description":"List of severity without alerts, separated by comma like: \"informational,warning\"","type":"string"},"token":{"description":"Telegram token for your bot","type":"string"}}}}},"resources":{"description":"Resources configuration","type":"object","default":{},"properties":{"limits":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"requests":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}},"storage":{"description":"Persistent Volume size for the database","type":"string","default":"10Gi"},"storageClassName":{"description":"StorageClass used to store the data","type":"string"}}},"grafana":{"description":"Configuration for Grafana","type":"object","default":{},"properties":{"db":{"description":"Database configuration","type":"object","default":{},"properties":{"size":{"description":"Persistent Volume size for the database","type":"string","default":"10Gi"}}},"resources":{"description":"Resources configuration","type":"object","default":{},"properties":{"limits":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"requests":{"type":"object","default":{},"properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}}}},"host":{"description":"The hostname used to access the grafana externally (defaults to 'grafana' subdomain for the tenant host).","type":"string"},"logsStorages":{"description":"Configuration of logs storage instances","type":"array","default":[{"name":"generic","retentionPeriod":"1","storage":"10Gi","storageClassName":"replicated"}],"items":{"type":"object","required":["name","retentionPeriod","storage"],"properties":{"name":{"description":"Name of the storage instance","type":"string"},"retentionPeriod":{"description":"Retention period for the logs in the storage instance","type":"string","default":"1"},"storage":{"description":"Persistent Volume size for the storage instance","type":"string","default":"10Gi"},"storageClassName":{"description":"StorageClass used to store the data","type":"string","default":"replicated"}}}},"metricsStorages":{"description":"Configuration of metrics storage instances","type":"array","default":[{"deduplicationInterval":"15s","name":"shortterm","retentionPeriod":"3d","storage":"10Gi","storageClassName":""},{"deduplicationInterval":"5m","name":"longterm","retentionPeriod":"14d","storage":"10Gi","storageClassName":""}],"items":{"type":"object","required":["deduplicationInterval","name","retentionPeriod","storage"],"properties":{"deduplicationInterval":{"description":"Deduplication interval for the metrics in the storage instance","type":"string"},"name":{"description":"Name of the storage instance","type":"string"},"retentionPeriod":{"description":"Retention period for the metrics in the storage instance","type":"string"},"storage":{"description":"Persistent Volume size for the storage instance","type":"string","default":"10Gi"},"storageClassName":{"description":"StorageClass used to store the data","type":"string"},"vminsert":{"description":"Configuration for vminsert component of the storage instance","type":"object","properties":{"maxAllowed":{"description":"Limits (maximum allowed/available resources )","type":"object","properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"minAllowed":{"description":"Requests (minimum allowed/available resources)","type":"object","properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}},"vmselect":{"description":"Configuration for vmselect component of the storage instance","type":"object","properties":{"maxAllowed":{"description":"Limits (maximum allowed/available resources )","type":"object","properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"minAllowed":{"description":"Requests (minimum allowed/available resources)","type":"object","properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}},"vmstorage":{"description":"Configuration for vmstorage component of the storage instance","type":"object","properties":{"maxAllowed":{"description":"Limits (maximum allowed/available resources )","type":"object","properties":{"cpu":{"description":"CPU limit (maximum available CPU)","default":1,"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory limit (maximum available memory)","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"minAllowed":{"description":"Requests (minimum allowed/available resources)","type":"object","properties":{"cpu":{"description":"CPU request (minimum available CPU)","default":"100m","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory request (minimum available memory)","default":"256Mi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}}}}}}} release: prefix: "" labels: @@ -28,7 +28,7 @@ spec: description: Monitoring and observability stack module: true icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODdfMzI2OCkiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY4N18zMjY4KSI+CjxwYXRoIGQ9Ik04OS41MDM5IDExMS43MDdINTQuNDk3QzU0LjE3MjcgMTExLjcwNyA1NC4wMTA4IDExMS4yMjEgNTQuMzM0OSAxMTEuMDU5TDU3LjI1MjIgMTA4Ljk1MkM2MC4zMzE0IDEwNi42ODMgNjEuOTUyMiAxMDIuNjMxIDYwLjk3OTcgOTguNzQxMkg4My4wMjFDODIuMDQ4NSAxMDIuNjMxIDgzLjY2OTMgMTA2LjY4MyA4Ni43NDg1IDEwOC45NTJMODkuNjY1OCAxMTEuMDU5Qzg5Ljk5IDExMS4yMjEgODkuODI3OSAxMTEuNzA3IDg5LjUwMzkgMTExLjcwN1oiIGZpbGw9IiNCMEI2QkIiLz4KPHBhdGggZD0iTTExMy4zMjggOTguNzQxSDMwLjY3MjVDMjcuNTkzMSA5OC43NDEgMjUgOTYuMTQ4IDI1IDkzLjA2ODdWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJWOTMuMDY4N0MxMTkgOTYuMTQ4IDExNi40MDcgOTguNzQxIDExMy4zMjggOTguNzQxWiIgZmlsbD0iI0U4RURFRSIvPgo8cGF0aCBkPSJNMTE5IDg0LjE1NDlIMjVWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJMMTE5IDg0LjE1NDlaIiBmaWxsPSIjMzg0NTRGIi8+CjxwYXRoIGQ9Ik05MC42Mzc0IDExNi41NjlINTMuMzYxNkM1Mi4wNjUxIDExNi41NjkgNTAuOTMwNyAxMTUuNDM1IDUwLjkzMDcgMTE0LjEzOEM1MC45MzA3IDExMi44NDEgNTIuMDY1MSAxMTEuNzA3IDUzLjM2MTYgMTExLjcwN0g5MC42Mzc0QzkxLjkzMzkgMTExLjcwNyA5My4wNjg0IDExMi44NDEgOTMuMDY4NCAxMTQuMTM4QzkzLjA2ODQgMTE1LjQzNSA5MS45MzM5IDExNi41NjkgOTAuNjM3NCAxMTYuNTY5WiIgZmlsbD0iI0U4RURFRSIvPgo8cGF0aCBkPSJNNTQuMTcyMiAzOC43NzU3SDMzLjEwMzJDMzIuMTMwNyAzOC43NzU3IDMxLjQ4MjQgMzguMTI3NCAzMS40ODI0IDM3LjE1NDlDMzEuNDgyNCAzNi4xODI0IDMyLjEzMDcgMzUuNTM0MiAzMy4xMDMyIDM1LjUzNDJINTQuMTcyMkM1NS4xNDQ3IDM1LjUzNDIgNTUuNzkzIDM2LjE4MjQgNTUuNzkzIDM3LjE1NDlDNTUuNzkyOCAzOC4xMjc0IDU1LjE0NDUgMzguNzc1NyA1NC4xNzIyIDM4Ljc3NTdaIiBmaWxsPSIjREQzNDJFIi8+CjxwYXRoIGQ9Ik02My44OTYzIDQ1LjI1OTFINDEuMjA2N0M0MC4yMzQyIDQ1LjI1OTEgMzkuNTg1OSA0NC42MTA4IDM5LjU4NTkgNDMuNjM4M0MzOS41ODU5IDQyLjY2NTggNDAuMjM0MiA0Mi4wMTc2IDQxLjIwNjcgNDIuMDE3Nkg2My44OTYzQzY0Ljg2ODggNDIuMDE3NiA2NS41MTcxIDQyLjY2NTggNjUuNTE3MSA0My42MzgzQzY1LjUxNzEgNDQuNjEwOCA2NC44Njg4IDQ1LjI1OTEgNjMuODk2MyA0NS4yNTkxWiIgZmlsbD0iIzczODNCRiIvPgo8cGF0aCBkPSJNMzQuNzI0IDQ1LjI1OTFIMzMuMTAzMkMzMi4xMzA3IDQ1LjI1OTEgMzEuNDgyNCA0NC42MTA4IDMxLjQ4MjQgNDMuNjM4M0MzMS40ODI0IDQyLjY2NTggMzIuMTMwNyA0Mi4wMTc2IDMzLjEwMzIgNDIuMDE3NkgzNC43MjRDMzUuNjk2NCA0Mi4wMTc2IDM2LjM0NDcgNDIuNjY1OCAzNi4zNDQ3IDQzLjYzODNDMzYuMzQ0NyA0NC42MTA4IDM1LjY5NjMgNDUuMjU5MSAzNC43MjQgNDUuMjU5MVoiIGZpbGw9IiM0MkIwNUMiLz4KPHBhdGggZD0iTTYzLjg5NjMgMzguNzc1N0g2MC42NTQ5QzU5LjY4MjQgMzguNzc1NyA1OS4wMzQyIDM4LjEyNzQgNTkuMDM0MiAzNy4xNTQ5QzU5LjAzNDIgMzYuMTgyNCA1OS42ODI0IDM1LjUzNDIgNjAuNjU0OSAzNS41MzQySDYzLjg5NjNDNjQuODY4OCAzNS41MzQyIDY1LjUxNzEgMzYuMTgyNCA2NS41MTcxIDM3LjE1NDlDNjUuNTE3MSAzOC4xMjc0IDY0Ljg2ODggMzguNzc1NyA2My44OTYzIDM4Ljc3NTdaIiBmaWxsPSIjRUNCQTE2Ii8+CjxwYXRoIGQ9Ik00Ny42ODkzIDUxLjc0MTNIMzMuMTAzMkMzMi4xMzA3IDUxLjc0MTMgMzEuNDgyNCA1MS4wOTMxIDMxLjQ4MjQgNTAuMTIwNkMzMS40ODI0IDQ5LjE0ODEgMzIuMTMwNyA0OC41IDMzLjEwMzIgNDguNUg0Ny42ODkzQzQ4LjY2MTggNDguNSA0OS4zMTAxIDQ5LjE0ODMgNDkuMzEwMSA1MC4xMjA4QzQ5LjMxMDEgNTEuMDkzMyA0OC42NjE4IDUxLjc0MTMgNDcuNjg5MyA1MS43NDEzWiIgZmlsbD0iI0REMzQyRSIvPgo8cGF0aCBkPSJNNjMuODk2OCA1MS43NDEzSDU0LjE3MjVDNTMuMiA1MS43NDEzIDUyLjU1MTggNTEuMDkzMSA1Mi41NTE4IDUwLjEyMDZDNTIuNTUxOCA0OS4xNDgxIDUzLjIwMDIgNDguNSA1NC4xNzI3IDQ4LjVINjMuODk2OUM2NC44Njk0IDQ4LjUgNjUuNTE3NyA0OS4xNDgzIDY1LjUxNzcgNTAuMTIwOEM2NS41MTc3IDUxLjA5MzMgNjQuODY5MiA1MS43NDEzIDYzLjg5NjggNTEuNzQxM1oiIGZpbGw9IiNFQ0JBMTYiLz4KPHBhdGggZD0iTTU0LjE3MjIgNTguMjI0SDMzLjEwMzJDMzIuMTMwNyA1OC4yMjQgMzEuNDgyNCA1Ny41NzU3IDMxLjQ4MjQgNTYuNjAzMkMzMS40ODI0IDU1LjYzMDcgMzIuMTMwNyA1NC45ODI0IDMzLjEwMzIgNTQuOTgyNEg1NC4xNzIyQzU1LjE0NDcgNTQuOTgyNCA1NS43OTMgNTUuNjMwNyA1NS43OTMgNTYuNjAzMkM1NS43OTMgNTcuNTc1NyA1NS4xNDQ1IDU4LjIyNCA1NC4xNzIyIDU4LjIyNFoiIGZpbGw9IiM0MkIwNUMiLz4KPHBhdGggZD0iTTYzLjg5NjMgNjQuNzA3NEg0MS4yMDY3QzQwLjIzNDIgNjQuNzA3NCAzOS41ODU5IDY0LjA1OTEgMzkuNTg1OSA2My4wODY2QzM5LjU4NTkgNjIuMTE0MSA0MC4yMzQyIDYxLjQ2NTggNDEuMjA2NyA2MS40NjU4SDYzLjg5NjNDNjQuODY4OCA2MS40NjU4IDY1LjUxNzEgNjIuMTE0MSA2NS41MTcxIDYzLjA4NjZDNjUuNTE3MSA2NC4wNTkxIDY0Ljg2ODggNjQuNzA3NCA2My44OTYzIDY0LjcwNzRaIiBmaWxsPSIjRUNCQTE2Ii8+CjxwYXRoIGQ9Ik0zNC43MjQgNjQuNzA3NEgzMy4xMDMyQzMyLjEzMDcgNjQuNzA3NCAzMS40ODI0IDY0LjA1OTEgMzEuNDgyNCA2My4wODY2QzMxLjQ4MjQgNjIuMTE0MSAzMi4xMzA3IDYxLjQ2NTggMzMuMTAzMiA2MS40NjU4SDM0LjcyNEMzNS42OTY0IDYxLjQ2NTggMzYuMzQ0NyA2Mi4xMTQxIDM2LjM0NDcgNjMuMDg2NkMzNi4zNDQ3IDY0LjA1OTEgMzUuNjk2MyA2NC43MDc0IDM0LjcyNCA2NC43MDc0WiIgZmlsbD0iI0REMzQyRSIvPgo8cGF0aCBkPSJNNDcuNjg5MyA3MS4xODk4SDMzLjEwMzJDMzIuMTMwNyA3MS4xODk4IDMxLjQ4MjQgNzAuNTQxNSAzMS40ODI0IDY5LjU2OUMzMS40ODI0IDY4LjU5NjUgMzIuMTMwNyA2Ny45NDgyIDMzLjEwMzIgNjcuOTQ4Mkg0Ny42ODkzQzQ4LjY2MTggNjcuOTQ4MiA0OS4zMTAxIDY4LjU5NjUgNDkuMzEwMSA2OS41NjlDNDkuMzEwMSA3MC41NDE1IDQ4LjY2MTggNzEuMTg5OCA0Ny42ODkzIDcxLjE4OThaIiBmaWxsPSIjNDJCMDVDIi8+CjxwYXRoIGQ9Ik02My44OTY4IDcxLjE4OThINTQuMTcyNUM1My4yIDcxLjE4OTggNTIuNTUxOCA3MC41NDE1IDUyLjU1MTggNjkuNTY5QzUyLjU1MTggNjguNTk2NSA1My4yIDY3Ljk0ODIgNTQuMTcyNSA2Ny45NDgySDYzLjg5NjhDNjQuODY5MiA2Ny45NDgyIDY1LjUxNzUgNjguNTk2NSA2NS41MTc1IDY5LjU2OUM2NS41MTc1IDcwLjU0MTUgNjQuODY5MiA3MS4xODk4IDYzLjg5NjggNzEuMTg5OFoiIGZpbGw9IiM3MzgzQkYiLz4KPHBhdGggZD0iTTU0LjE3MjIgNzcuNjcyMkgzMy4xMDMyQzMyLjEzMDcgNzcuNjcyMiAzMS40ODI0IDc3LjAyMzkgMzEuNDgyNCA3Ni4wNTE0QzMxLjQ4MjQgNzUuMDc4OSAzMi4xMzA3IDc0LjQzMDcgMzMuMTAzMiA3NC40MzA3SDU0LjE3MjJDNTUuMTQ0NyA3NC40MzA3IDU1Ljc5MyA3NS4wNzg5IDU1Ljc5MyA3Ni4wNTE0QzU1Ljc5MjggNzcuMDIzOSA1NS4xNDQ1IDc3LjY3MjIgNTQuMTcyMiA3Ny42NzIyWiIgZmlsbD0iI0VDQkExNiIvPgo8cGF0aCBkPSJNNjMuODk2MyA3Ny42NzIySDYwLjY1NDlDNTkuNjgyNCA3Ny42NzIyIDU5LjAzNDIgNzcuMDIzOSA1OS4wMzQyIDc2LjA1MTRDNTkuMDM0MiA3NS4wNzg5IDU5LjY4MjQgNzQuNDMwNyA2MC42NTQ5IDc0LjQzMDdINjMuODk2M0M2NC44Njg4IDc0LjQzMDcgNjUuNTE3MSA3NS4wNzg5IDY1LjUxNzEgNzYuMDUxNEM2NS41MTcxIDc3LjAyMzkgNjQuODY4OCA3Ny42NzIyIDYzLjg5NjMgNzcuNjcyMloiIGZpbGw9IiM0MkIwNUMiLz4KPHBhdGggZD0iTTEwMS4xNzIgNzcuNjcyMkg4MC4xMDMyQzc5LjEzMDcgNzcuNjcyMiA3OC40ODI0IDc3LjAyMzkgNzguNDgyNCA3Ni4wNTE0Qzc4LjQ4MjQgNzUuMDc4OSA3OS4xMzA3IDc0LjQzMDcgODAuMTAzMiA3NC40MzA3SDEwMS4xNzJDMTAyLjE0NSA3NC40MzA3IDEwMi43OTMgNzUuMDc4OSAxMDIuNzkzIDc2LjA1MTRDMTAyLjc5MyA3Ny4wMjM5IDEwMi4xNDUgNzcuNjcyMiAxMDEuMTcyIDc3LjY3MjJaIiBmaWxsPSIjREQzNDJFIi8+CjxwYXRoIGQ9Ik0xMTAuODk2IDc3LjY3MjJIMTA3LjY1NUMxMDYuNjgyIDc3LjY3MjIgMTA2LjAzNCA3Ny4wMjM5IDEwNi4wMzQgNzYuMDUxNEMxMDYuMDM0IDc1LjA3ODkgMTA2LjY4MiA3NC40MzA3IDEwNy42NTUgNzQuNDMwN0gxMTAuODk2QzExMS44NjkgNzQuNDMwNyAxMTIuNTE3IDc1LjA3ODkgMTEyLjUxNyA3Ni4wNTE0QzExMi41MTcgNzcuMDIzOSAxMTEuODY5IDc3LjY3MjIgMTEwLjg5NiA3Ny42NzIyWiIgZmlsbD0iIzQyQjA1QyIvPgo8cGF0aCBkPSJNNjMuODk2MyA1OC4yMjRINjAuNjU0OUM1OS42ODI0IDU4LjIyNCA1OS4wMzQyIDU3LjU3NTcgNTkuMDM0MiA1Ni42MDMyQzU5LjAzNDIgNTUuNjMwNyA1OS42ODI0IDU0Ljk4MjQgNjAuNjU0OSA1NC45ODI0SDYzLjg5NjNDNjQuODY4OCA1NC45ODI0IDY1LjUxNzEgNTUuNjMwNyA2NS41MTcxIDU2LjYwMzJDNjUuNTE3MSA1Ny41NzU3IDY0Ljg2ODggNTguMjI0IDYzLjg5NjMgNTguMjI0WiIgZmlsbD0iIzczODNCRiIvPgo8cGF0aCBkPSJNMTEyLjUxNyA1MS43NDExQzExMi41MTcgNjAuNjU0OSAxMDUuMjI0IDY3Ljk0OCA5Ni4zMTA0IDY3Ljk0OEM4Ny4zOTY2IDY3Ljk0OCA4MC4xMDM1IDYwLjY1NDkgODAuMTAzNSA1MS43NDExQzgwLjEwMzUgNDIuODI3MyA4Ny4zOTY2IDM1LjUzNDIgOTYuMzEwNCAzNS41MzQyQzEwNS4yMjQgMzUuNTM0MiAxMTIuNTE3IDQyLjgyNzMgMTEyLjUxNyA1MS43NDExWiIgZmlsbD0iI0VDQkExNiIvPgo8cGF0aCBkPSJNODAuMTAzNSA1MS43NDExQzgwLjEwMzUgNTIuMjI3MyA4MC4xMDM1IDUyLjg3NTUgODAuMTAzNSA1My4zNjE5SDk2LjMxMDRWMzUuNTM0MkM4Ny4zOTY2IDM1LjUzNDIgODAuMTAzNSA0Mi44MjczIDgwLjEwMzUgNTEuNzQxMVoiIGZpbGw9IiM0MkIwNUMiLz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4N18zMjY4IiB4MT0iMS4yMzIzOWUtMDYiIHkxPSItOS41MDAwMSIgeDI9IjE2OCIgeTI9IjE2MiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjOEZEREZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwNzVGRiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzY4N18zMjY4Ij4KPHJlY3Qgd2lkdGg9Ijk0IiBoZWlnaHQ9Ijk0IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjUgMjUpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg== - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "metricsStorages"], ["spec", "logsStorages"], ["spec", "alerta"], ["spec", "alerta", "storage"], ["spec", "alerta", "storageClassName"], ["spec", "alerta", "resources"], ["spec", "alerta", "resources", "limits"], ["spec", "alerta", "resources", "limits", "cpu"], ["spec", "alerta", "resources", "limits", "memory"], ["spec", "alerta", "resources", "requests"], ["spec", "alerta", "resources", "requests", "cpu"], ["spec", "alerta", "resources", "requests", "memory"], ["spec", "alerta", "alerts"], ["spec", "alerta", "alerts", "telegram"], ["spec", "alerta", "alerts", "telegram", "token"], ["spec", "alerta", "alerts", "telegram", "chatID"], ["spec", "alerta", "alerts", "telegram", "disabledSeverity"], ["spec", "grafana"], ["spec", "grafana", "db"], ["spec", "grafana", "db", "size"], ["spec", "grafana", "resources"], ["spec", "grafana", "resources", "limits"], ["spec", "grafana", "resources", "limits", "cpu"], ["spec", "grafana", "resources", "limits", "memory"], ["spec", "grafana", "resources", "requests"], ["spec", "grafana", "resources", "requests", "cpu"], ["spec", "grafana", "resources", "requests", "memory"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "metricsStorages"], ["spec", "logsStorages"], ["spec", "alerta"], ["spec", "alerta", "storage"], ["spec", "alerta", "storageClassName"], ["spec", "alerta", "resources"], ["spec", "alerta", "resources", "limits"], ["spec", "alerta", "resources", "limits", "cpu"], ["spec", "alerta", "resources", "limits", "memory"], ["spec", "alerta", "resources", "requests"], ["spec", "alerta", "resources", "requests", "cpu"], ["spec", "alerta", "resources", "requests", "memory"], ["spec", "alerta", "alerts"], ["spec", "alerta", "alerts", "telegram"], ["spec", "alerta", "alerts", "telegram", "token"], ["spec", "alerta", "alerts", "telegram", "chatID"], ["spec", "alerta", "alerts", "telegram", "disabledSeverity"], ["spec", "alerta", "alerts", "slack"], ["spec", "alerta", "alerts", "slack", "url"], ["spec", "grafana"], ["spec", "grafana", "db"], ["spec", "grafana", "db", "size"], ["spec", "grafana", "resources"], ["spec", "grafana", "resources", "limits"], ["spec", "grafana", "resources", "limits", "cpu"], ["spec", "grafana", "resources", "limits", "memory"], ["spec", "grafana", "resources", "requests"], ["spec", "grafana", "resources", "requests", "cpu"], ["spec", "grafana", "resources", "requests", "memory"]] secrets: exclude: [] include: diff --git a/packages/system/cozystack-resource-definitions/cozyrds/virtual-machine.yaml b/packages/system/cozystack-resource-definitions/cozyrds/virtual-machine.yaml index f786678e..7a2a054a 100644 --- a/packages/system/cozystack-resource-definitions/cozyrds/virtual-machine.yaml +++ b/packages/system/cozystack-resource-definitions/cozyrds/virtual-machine.yaml @@ -32,3 +32,8 @@ spec: secrets: exclude: [] include: [] + services: + exclude: [] + include: + - resourceNames: + - virtual-machine-{{ .name }} diff --git a/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile b/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile index e21829d2..ef698698 100644 --- a/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile +++ b/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile @@ -1,15 +1,11 @@ # imported from https://github.com/cozystack/openapi-ui-k8s-bff ARG NODE_VERSION=20.18.1 FROM node:${NODE_VERSION}-alpine AS builder -RUN apk add git WORKDIR /src -ARG COMMIT_REF=22f9143f5109fb90332651c857d70b51bffccd9b +ARG COMMIT_REF=183dc9dcbb0f8a1833dad642c35faa385c71e58d RUN wget -O- https://github.com/PRO-Robotech/openapi-ui-k8s-bff/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1 -COPY patches /patches -RUN git apply /patches/*.diff - ENV PATH=/src/node_modules/.bin:$PATH RUN npm install RUN npm run build diff --git a/packages/system/dashboard/images/openapi-ui-k8s-bff/patches/namespaces.diff b/packages/system/dashboard/images/openapi-ui-k8s-bff/patches/namespaces.diff index 6a8ecbbe..3ed11a7c 100644 --- a/packages/system/dashboard/images/openapi-ui-k8s-bff/patches/namespaces.diff +++ b/packages/system/dashboard/images/openapi-ui-k8s-bff/patches/namespaces.diff @@ -1,7 +1,7 @@ -diff --git a/src/endpoints/forms/formPrepare/formPrepare.ts b/src/endpoints/forms/formPrepare/formPrepare.ts +diff --git a/src/endpoints/forms/prepareFormProps/prepareFormProps.ts b/src/endpoints/forms/prepareFormProps/prepareFormProps.ts index 7e437db..90c40f6 100644 ---- a/src/endpoints/forms/formPrepare/formPrepare.ts -+++ b/src/endpoints/forms/formPrepare/formPrepare.ts +--- a/src/endpoints/forms/prepareFormProps/prepareFormProps.ts ++++ b/src/endpoints/forms/prepareFormProps/prepareFormProps.ts @@ -15,6 +15,7 @@ export const prepareFormProps: RequestHandler = async (req: TPrepareFormReq, res const filteredHeaders = { ...req.headers } diff --git a/packages/system/dashboard/images/openapi-ui/Dockerfile b/packages/system/dashboard/images/openapi-ui/Dockerfile index ea10ac8a..a33be863 100644 --- a/packages/system/dashboard/images/openapi-ui/Dockerfile +++ b/packages/system/dashboard/images/openapi-ui/Dockerfile @@ -3,9 +3,14 @@ ARG NODE_VERSION=20.18.1 # openapi-k8s-toolkit # imported from https://github.com/cozystack/openapi-k8s-toolkit FROM node:${NODE_VERSION}-alpine AS openapi-k8s-toolkit-builder +RUN apk add git WORKDIR /src -ARG COMMIT=4f57ab295b2a886eb294b0b987554194fbe67dcd +ARG COMMIT=7bd5380c6c4606640dd3bac68bf9dce469470518 RUN wget -O- https://github.com/cozystack/openapi-k8s-toolkit/archive/${COMMIT}.tar.gz | tar -xzvf- --strip-components=1 + +COPY openapi-k8s-toolkit/patches /patches +RUN git apply /patches/*.diff + RUN npm install RUN npm install --build-from-source @swc/core RUN npm run build @@ -14,14 +19,14 @@ RUN npm run build # openapi-ui # imported from https://github.com/cozystack/openapi-ui FROM node:${NODE_VERSION}-alpine AS builder -RUN apk add git +#RUN apk add git WORKDIR /src -ARG COMMIT_REF=65e7fa8b3dc530a36e94c8435622bb09961aef97 +ARG COMMIT_REF=0c3629b2ce8545e81f7ece4d65372a188c802dfc RUN wget -O- https://github.com/PRO-Robotech/openapi-ui/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1 -COPY patches /patches -RUN git apply /patches/*.diff +#COPY openapi-ui/patches /patches +#RUN git apply /patches/*.diff ENV PATH=/src/node_modules/.bin:$PATH diff --git a/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff new file mode 100644 index 00000000..2817a8e0 --- /dev/null +++ b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff @@ -0,0 +1,50 @@ +diff --git a/src/components/molecules/EnrichedTable/organisms/EnrichedTable/utils.tsx b/src/components/molecules/EnrichedTable/organisms/EnrichedTable/utils.tsx +index 8bcef4d..2551e92 100644 +--- a/src/components/molecules/EnrichedTable/organisms/EnrichedTable/utils.tsx ++++ b/src/components/molecules/EnrichedTable/organisms/EnrichedTable/utils.tsx +@@ -22,6 +22,15 @@ import { TableFactory } from '../../molecules' + import { ShortenedTextWithTooltip, FilterDropdown, TrimmedTags, TextAlignContainer, TinyButton } from './atoms' + import { TInternalDataForControls } from './types' + ++const getPluralForm = (singular: string): string => { ++ // If already ends with 's', add 'es' ++ if (singular.endsWith('s')) { ++ return `${singular}es` ++ } ++ // Otherwise just add 's' ++ return `${singular}s` ++} ++ + export const getCellRender = ({ + value, + record, +@@ -255,7 +264,7 @@ export const getEnrichedColumnsWithControls = ({ + key: 'controls', + className: 'controls', + width: 60, +- render: (value: TInternalDataForControls) => { ++ render: (value: TInternalDataForControls, record: unknown) => { + return ( + // + +@@ -279,10 +288,19 @@ export const getEnrichedColumnsWithControls = ({ + domEvent.stopPropagation() + domEvent.preventDefault() + if (key === 'edit') { ++ // Special case: redirect tenantmodules from core.cozystack.io to apps.cozystack.io with plural form ++ let apiGroupAndVersion = value.apiGroupAndVersion ++ let typeName = value.typeName ++ if (apiGroupAndVersion?.startsWith('core.cozystack.io/') && typeName === 'tenantmodules') { ++ const appsApiVersion = apiGroupAndVersion.replace('core.cozystack.io/', 'apps.cozystack.io/') ++ const pluralTypeName = getPluralForm(value.entryName) ++ apiGroupAndVersion = appsApiVersion ++ typeName = pluralTypeName ++ } + navigate( + `${baseprefix}/${value.cluster}${value.namespace ? `/${value.namespace}` : ''}${ + value.syntheticProject ? `/${value.syntheticProject}` : '' +- }/${value.pathPrefix}/${value.apiGroupAndVersion}/${value.typeName}/${value.entryName}?backlink=${ ++ }/${value.pathPrefix}/${apiGroupAndVersion}/${typeName}/${value.entryName}?backlink=${ + value.backlink + }`, + ) diff --git a/packages/system/dashboard/images/openapi-ui/patches/namespaces.diff b/packages/system/dashboard/images/openapi-ui/patches/namespaces.diff deleted file mode 100644 index 66bce93a..00000000 --- a/packages/system/dashboard/images/openapi-ui/patches/namespaces.diff +++ /dev/null @@ -1,89 +0,0 @@ -diff --git a/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx b/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx -index 577ba0f..018df9c 100644 ---- a/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx -+++ b/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx -@@ -1,11 +1,16 @@ - import React, { FC, useState } from 'react' - import { Button, Alert, Spin, Typography } from 'antd' --import { filterSelectOptions, Spacer, useBuiltinResources } from '@prorobotech/openapi-k8s-toolkit' -+import { filterSelectOptions, Spacer, useApiResources } from '@prorobotech/openapi-k8s-toolkit' - import { useNavigate } from 'react-router-dom' - import { useSelector, useDispatch } from 'react-redux' - import { RootState } from 'store/store' - import { setCluster } from 'store/cluster/cluster/cluster' - import { Styled } from './styled' -+import { -+ BASE_PROJECTS_API_GROUP, -+ BASE_PROJECTS_VERSION, -+ BASE_PROJECTS_RESOURCE_NAME, -+} from 'constants/customizationApiGroupAndVersion' - - export const ListInsideClusterAndNs: FC = () => { - const clusterList = useSelector((state: RootState) => state.clusterList.clusterList) -@@ -17,9 +22,11 @@ export const ListInsideClusterAndNs: FC = () => { - const [selectedCluster, setSelectedCluster] = useState() - const [selectedNamespace, setSelectedNamespace] = useState() - -- const namespacesData = useBuiltinResources({ -+ const namespacesData = useApiResources({ - clusterName: cluster, -- typeName: 'namespaces', -+ apiGroup: BASE_PROJECTS_API_GROUP, -+ apiVersion: BASE_PROJECTS_VERSION, -+ typeName: BASE_PROJECTS_RESOURCE_NAME, - limit: null, - }) - -diff --git a/src/hooks/useNavSelectorInside.ts b/src/hooks/useNavSelectorInside.ts -index d69405e..5adbd5d 100644 ---- a/src/hooks/useNavSelectorInside.ts -+++ b/src/hooks/useNavSelectorInside.ts -@@ -1,6 +1,11 @@ --import { TClusterList, TSingleResource, useBuiltinResources } from '@prorobotech/openapi-k8s-toolkit' -+import { TClusterList, TSingleResource, useApiResources } from '@prorobotech/openapi-k8s-toolkit' - import { useSelector } from 'react-redux' - import { RootState } from 'store/store' -+import { -+ BASE_PROJECTS_API_GROUP, -+ BASE_PROJECTS_VERSION, -+ BASE_PROJECTS_RESOURCE_NAME, -+} from 'constants/customizationApiGroupAndVersion' - - const mappedClusterToOptionInSidebar = ({ name }: TClusterList[number]): { value: string; label: string } => ({ - value: name, -@@ -15,9 +20,11 @@ const mappedNamespaceToOptionInSidebar = ({ metadata }: TSingleResource): { valu - export const useNavSelectorInside = (clusterName?: string) => { - const clusterList = useSelector((state: RootState) => state.clusterList.clusterList) - -- const { data: namespaces } = useBuiltinResources({ -+ const { data: namespaces } = useApiResources({ - clusterName: clusterName || '', -- typeName: 'namespaces', -+ apiGroup: BASE_PROJECTS_API_GROUP, -+ apiVersion: BASE_PROJECTS_VERSION, -+ typeName: BASE_PROJECTS_RESOURCE_NAME, - limit: null, - }) - -diff --git a/src/utils/getBacklink.ts b/src/utils/getBacklink.ts -index a862354..f24e2bc 100644 ---- a/src/utils/getBacklink.ts -+++ b/src/utils/getBacklink.ts -@@ -28,7 +28,7 @@ export const getFormsBackLink = ({ - } - - if (namespacesMode) { -- return `${baseprefix}/${clusterName}/builtin-table/namespaces` -+ return `${baseprefix}/${clusterName}/api-table/core.cozystack.io/v1alpha1/tenantnamespaces` - } - - if (possibleProject) { -@@ -64,7 +64,7 @@ export const getTablesBackLink = ({ - } - - if (namespacesMode) { -- return `${baseprefix}/${clusterName}/builtin-table/namespaces` -+ return `${baseprefix}/${clusterName}/api-table/core.cozystack.io/v1alpha1/tenantnamespaces` - } - - if (possibleProject) { diff --git a/packages/system/dashboard/templates/configmap.yaml b/packages/system/dashboard/templates/configmap.yaml index 5c541dff..4bf69678 100644 --- a/packages/system/dashboard/templates/configmap.yaml +++ b/packages/system/dashboard/templates/configmap.yaml @@ -1,9 +1,9 @@ {{- $brandingConfig:= lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }} -{{- $tenantText := "v0.37.0" }} +{{- $tenantText := "v0.37.10" }} {{- $footerText := "Cozystack" }} {{- $titleText := "Cozystack Dashboard" }} -{{- $logoText := "false" }} +{{- $logoText := "" }} {{- $logoSvg := "PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjMwIiB2aWV3Qm94PSIwIDAgMTUwIDMwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Ik0xMzMuMzMxMDkgMjIuMzczOTg3VjQuNzk1Mjc2OWgyLjA0NDUyVjEyLjc3Mzg5aC4wNDk5bDguNTI3NzEtNy45NzkxNjcyaDIuNjE4MjdsLTkuNzc0NjQgOS4xMDExNTgyLjAyNDktMS4wOTcwNTkgMTAuMjk4MjQgOS41NzQ4ODloLTIuNjkyNzlsLTkuMDAxNjktOC4yNzgzNjNoLS4wNDk5djguMjc4MzYzem0tOS4xNzIzNi4yMjQzOTljLTEuNzI4NjkuMC0zLjIwODA1LS4zNjU2ODYtNC40MzgxLTEuMDk3MDU5LTEuMjMwNTktLjczMTM3My0yLjE3ODA0LTEuNzcwMjU0LTIuODQyOTMtMy4xMTY2NDUtLjY0ODI3LTEuMzQ2NjY3LS45NzIzOS0yLjk1MDk3OC0uOTcyMzktNC44MTI2NTQuMC0xLjg2MTY3Ny4zMjQxMi0zLjQ1NzM5OS45NzIzOS00Ljc4NzQ0NS42NjQ4OS0xLjM0NjM5MDYgMS42MTIzNC0yLjM4NTI3MjUgMi44NDI2Ni0zLjExNjkyMjQgMS4yMzAwMy0uNzMxMzcyOSAyLjcwOTQxLTEuMDk3MDU5MiA0LjQzODM3LTEuMDk3MDU5MiAxLjIxMzQyLjAgMi4zMzU0MS4xOTExNTQzIDMuMzY2MjYuNTczNDYyOCAxLjAzMDU3LjM4MjMwODUgMS44OTQ5My45MzkxNDkxIDIuNTkzMDUgMS42NzA1MjE5bC0uNzk3ODYgMS42NzA1MjE5Yy0uNzY0NjItLjcxNDc1LTEuNTYyNDgtMS4yMzAwMzYtMi4zOTM1OS0xLjU0NTg1NjEtLjgxNDQ3LS4zMzI0NDIxLTEuNzIwMzgtLjQ5ODY2MzItMi43MTc3MS0uNDk4NjYzMi0xLjk3ODU5LjAtMy40OTEyLjYyMzMyOS00LjUzODM4IDEuODY5OTg4My0xLjA0NzIxIDEuMjQ2NjU3LTEuNTcwOCAzLjAwMDU2Ni0xLjU3MDggNS4yNjE0NTEuMCAyLjI2MDYwNi41MjM1OSA0LjAyMjU0OSAxLjU3MDggNS4yODYxMDggMS4wNDcxOCAxLjI0NjY1NyAyLjU1OTc5IDEuODY5OTg2IDQuNTM4MDkgMS44Njk5ODYuOTk3MzQuMCAxLjkwMzI0LS4xNTc5MDkgMi43MTc3My0uNDczNzMuODMxMzgtLjMzMjQ0MiAxLjYyOTI0LS44NTYwMzggMi4zOTM4Ni0xLjU3MDc4OWwuNzk3ODYgMS42NzA1MjJjLS42OTgxMi43MTQ3NTEtMS41NjI0OCAxLjI3MTg2OS0yLjU5MzA1IDEuNjcwNzk5LTEuMDMwNTguMzgyMzA5LTIuMTUyNTcuNTczNDYyLTMuMzY1OTcuNTczNDYyek05Ni45ODQ2MzEgMjIuMzczOTg3IDEwNC43Mzk0IDQuNzk0OTk5OWgxLjc0NTMzbDcuNzU0NzYgMTcuNTc4OTg3MWgtMi4xMTkzMmwtMi4xNjkxOS01LjAxMTg0MS45OTczMy41MjM1OTVoLTEwLjcyMjA5bDEuMDIyMjctLjUyMzU5NS0yLjE0NDI1NCA1LjAxMTg0MXptOC42MDI0OTktMTUuMTg1NDAzNC00LjAxNDI0IDkuNDUwNTAwNC0uNTk4NC0uNDczNzNoOS4yMjU1NWwtLjU0ODUzLjQ3MzczLTQuMDE0MjUtOS40NTA1MDA0ek04OS44OTM5MjEgMjIuMzczOTg3VjYuNTY1NTMxNkg4My41MTAyMDJWNC43OTUyNzY5aDE0LjgzNjMzVjYuNTY1NTMxNkg5MS45NjMzNjZWMjIuMzc0MjY1eiIgZmlsbD17dG9rZW4uY29sb3JUZXh0fT48L3BhdGg+CiAgPHBhdGggZD0ibTY3Ljg1NDM4NSA0Ljc3MzExNDJoMTQuMDgwODd2MS43NjAwMDQyaC0xNC4wODA4N3ptMCAxNS44NDA4Njk4aDE0LjA4MDg3djEuNzYwMDAzaC0xNC4wODA4N3ptMTQuMDgwODctNy45MjA0MzVoLTE0LjA4MDg3djEuNzYwMDA1aDE0LjA4MDg3eiIgZmlsbD17dG9rZW4uY29sb3JUZXh0fT48L3BhdGg+CiAgPHBhdGggZD0ibTU3LjY2NDQ3MyAyMi4zNzM5ODd2LTkuMTAxMTU4bC40NDg4MDQgMS40MjExOTEtNy4xODEzMDktOS44OTkwMjAxaDIuMzkzODYxbDUuNjYwMTA5IDcuODI5NTY3MWgtLjUyMzYwNmw1LjY2MDM5MS03LjgyOTU2NzFoMi4zMTg3ODdsLTcuMTU2MzczIDkuODk4NzQyMS40MjQxNC0xLjQyMTE4OXY5LjEwMTE1OHptLTIwLjE4ODEwMi4wVjIwLjg1MzA2NUw0OC4xNDg1OTYgNS43NjczOTMydi43OTc4NjEzSDM3LjQ3NjM3MVY0Ljc5NDk5OTlINTAuMDY4NDVWNi4zMTU5MjI4TDM5LjM5NjUwMSAyMS4zNzY2NjF2LS43NzI5MjdoMTEuMDIxMjl2MS43NzAyNTN6bS0xMC4zOTYyOTguMjI0Mzk5Yy0xLjIxMzQxNC4wLTIuMzE5MDYyLS4yMDc3NzYtMy4zMTYzODktLjYyMzMyOC0uOTk3MzI3LS40MzIxNzUtMS44NDUwNTUtMS4wMzg4ODItMi41NDMxODQtMS44MjAxMjEtLjY5ODQwNi0uNzgxMjM5LTEuMjM4NjI0LTEuNzI4Ny0xLjYyMDkzMy0yLjg0MjY1OC0uMzY1Njg2LTEuMTEzNjgyLS41NDg1MjktMi4zNjAzMzktLjU0ODUyOS0zLjc0MDI1MS4wLTEuMzk2MjU3LjE4Mjg0My0yLjY0MjkxNy41NDg1MjktMy43NDAyNTIuMzgyMzA5LTEuMTEzNjgyLjkyMjUyNy0yLjA1MjgzMDkgMS42MjA2NTYtMi44MTc0NDc1LjY5ODEyOS0uNzgxMjM5MiAxLjUzNzU0NS0xLjM3OTkxMjEgMi41MTgyNS0xLjc5NTQ2NDguOTk3NjA0LS40MzIxNzQ5IDIuMTExMjg3LS42NDgyNjIzIDMuMzQxNi0uNjQ4MjYyMyAxLjI0NjY1Ny4wIDIuMzYwMzM5LjIwNzc3NjMgMy4zNDEwNDQuNjIzMzI5MS45OTczMjcuNDE1NTUyNyAxLjg0NTA1NSAxLjAxMzk0ODYgMi41NDM0NTkgMS43OTUxODc3LjcxNDc1MS43ODEyMzk4IDEuMjU0OTY5IDEuNzI4Njk5OCAxLjYyMDY1NSAyLjg0MjY1NzguMzgyMzEgMS4wOTcwNTkuNTczNDY0IDIuMzM1NDA2LjU3MzQ2NCAzLjcxNTA0Mi4wIDEuMzk2NTMzLS4xOTExNTQgMi42NTE3NzktLjU3MzQ2NCAzLjc2NTQ2MS0uMzgyMzA4IDEuMTEzNjgyLS45MjI1MjYgMi4wNjExNDItMS42MjA2NTUgMi44NDIzODFzLTEuNTQ1ODU2IDEuMzg3OTQ2LTIuNTQzMTgzIDEuODIwMzk4Yy0uOTgwNzA0LjQxNTU1Mi0yLjA5NDY2My42MjMzMjgtMy4zNDEzMi42MjMzMjh6bTAtMS44MjAxMmMxLjI2MzI3OS4wIDIuMzI3MDk0LS4yODI1NzYgMy4xOTE0NDMtLjg0NzcyOC44ODA5NzMtLjU2NTE1MiAxLjU1NDE2OS0xLjM4Nzk0NiAyLjAxOTU4OC0yLjQ2ODY2LjQ2NTY5Ni0xLjA4MDQzNy42OTg0MDYtMi4zNzY5NjEuNjk4NDA2LTMuODg5ODUuMC0xLjUyOTIzNC0uMjMyNzEtMi44MjU3NTgtLjY5ODEyOS0zLjg4OTg1MS0uNDY1NDE5LTEuMDYzODE1LTEuMTM4NjE0LTEuODc4Mjk3OC0yLjAxOTU4Ny0yLjQ0MzQ1LS44NjQzNS0uNTY1MTUxOC0xLjkyODQ0Mi0uODQ3NzI3Ni0zLjE5MTcyMS0uODQ3NzI3Ni0xLjIzMDAzOC4wLTIuMjg1ODE4LjI4MjU3NTgtMy4xNjY3OTEuODQ3NzI3Ni0uODY0MzUuNTY1MTUyMi0xLjUyOTIzNCAxLjM4Nzk0Ni0xLjk5NDY1MyAyLjQ2ODM4My0uNDY1NDE5IDEuMDYzODE1LS42OTgxMjkgMi4zNTIwMjktLjY5ODEyOSAzLjg2NDY0MS4wIDEuNTEyODg5LjIzMjcxIDIuODA5NjkuNjk4MTI5IDMuODkwMTI3LjQ2NTQxOSAxLjA2MzgxNSAxLjEzMDMwMyAxLjg4NjYwOSAxLjk5NDY1MyAyLjQ2ODM4My44ODA5NzMuNTY1MTUxIDEuOTM2NDc4Ljg0NzcyNyAzLjE2NjUxMy44NDc3Mjd6bS0xNi4wNDE3MjQgMS44MjAxMmMtMS43Mjg2OTgxLjAtMy4yMDgzNDE3LS4zNjU2ODYtNC40Mzg2NTYxLTEuMDk3MDU5QzUuMzY5NjU3MSAyMC43Njk5NTQgNC40MjIxOTY2IDE5LjczMTA3MyAzLjc1NzMxMzEgMTguMzg0NjgyYy0uNjQ4MjYzLTEuMzQ2NjY3LS45NzIzOTM2LTIuOTUwOTc4LS45NzIzOTM2LTQuODEyNjU0LjAtMS44NjE2NzcuMzI0MTMwNi0zLjQ1NzM5OS45NzIzOTM2LTQuNzg3NDQ1LjY2NDg4MzUtMS4zNDYzOTA2IDEuNjEyMzQ0LTIuMzg1MjcyNSAyLjg0MjM3OTgtMy4xMTY5MjI0QzcuODI5NzI5OCA0LjkzNjI4NzcgOS4zMDkzNzM0IDQuNTcwNjAxNCAxMS4wMzgzNDkgNC41NzA2MDE0YzEuMjEzNDE0LjAgMi4zMzU0MDguMTkxMTU0MyAzLjM2NTk3OC41NzM0NjI4IDEuMDMwNTcyLjM4MjMwODUgMS44OTQ5MjIuOTM5MTQ5MSAyLjU5MzMyNiAxLjY3MDUyMTlMMTYuMTk5NzkxIDguNDg1MTA4QzE1LjQzNTE3NSA3Ljc3MDM1OCAxNC42MzczMTMgNy4yNTUwNzIgMTMuODA2MjA5IDYuOTM5MjUxOSAxMi45OTE0NDcgNi42MDY4MDk4IDEyLjA4NTU0MyA2LjQ0MDU4ODcgMTEuMDg4MjE2IDYuNDQwNTg4N2MtMS45NzgwMzA0LjAtMy40OTA5MTg4LjYyMzMyOS00LjUzODM4OTIgMS44Njk5ODgzLTEuMDQ3MTkyNyAxLjI0NjY1Ny0xLjU3MDc4ODYgMy4wMDA1NjYtMS41NzA3ODg2IDUuMjYxNDUxLjAgMi4yNjA2MDYuNTIzNTk1OSA0LjAyMjU0OSAxLjU3MDc4ODYgNS4yODYxMDggMS4wNDcxOTI5IDEuMjQ2NjU3IDIuNTYwMDgyMyAxLjg2OTk4NiA0LjUzODM4OTIgMS44Njk5ODYuOTk3MzI3LjAgMS45MDMyMzEtLjE1NzkwOSAyLjcxNzcxNS0uNDczNzMuODMxMTA2LS4zMzI0NDIgMS42Mjg5NjgtLjg1NjAzOCAyLjM5MzU4NC0xLjU3MDc4OWwuNzk4MTM4IDEuNjcwNTIyYy0uNjk4MTI4LjcxNDc1MS0xLjU2MjQ3OCAxLjI3MTg2OS0yLjU5MzA0OCAxLjY3MDc5OS0xLjAzMDU3Mi4zODIzMDktMi4xNTI4NDIuNTczNDYyLTMuMzY2MjU2LjU3MzQ2MnoiIGZpbGw9e3Rva2VuLmNvbG9yVGV4dH0+PC9wYXRoPgo8L3N2Zz4K" }} {{- $iconSvg := "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgd2lkdGg9IjkxIgogICBoZWlnaHQ9IjkxIgogICB2aWV3Qm94PSIwIDAgNjguMjUwMDI0IDY4LjI1MDAyNCIKICAgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgbWVldCIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnODI2IgogICBzb2RpcG9kaTpkb2NuYW1lPSJmYXZpY29uLnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4xLjEgKGMzMDg0ZWYsIDIwMjEtMDktMjIpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM4MzAiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJuYW1lZHZpZXc4MjgiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMC43NzAzNTc0MSIKICAgICBpbmtzY2FwZTpjeD0iNDM2LjgxMDIzIgogICAgIGlua3NjYXBlOmN5PSI1NDEuOTU2MjMiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxNzIwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEzODciCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjE3MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjI1IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnODI2IiAvPgogIDxyZWN0CiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtzdHJva2Utd2lkdGg6MC42MTc5MDUiCiAgICAgaWQ9InJlY3Q5MzgiCiAgICAgd2lkdGg9IjY4LjI1MDAyMyIKICAgICBoZWlnaHQ9IjY4LjI1MDAyMyIKICAgICB4PSIwIgogICAgIHk9Ii0xLjc3NjM1NjhlLTE1IiAvPgogIDxwYXRoCiAgICAgZmlsbC1ydWxlPSJldmVub2RkIgogICAgIGNsaXAtcnVsZT0iZXZlbm9kZCIKICAgICBkPSJtIDE2LjYwMTU5OCwxMy45MjY5MSBjIDAsLTEuMjgwMTE1IDEuMDE1MDUxLC0yLjMxNzg2MSAyLjI2NzQyNCwtMi4zMTc4NjEgaCAzMS43NDM5MzcgYyAxLjI1MjM3OCwwIDIuMjY3NDIsMS4wMzc3NDYgMi4yNjc0MiwyLjMxNzg2MSB2IDAgYyAwLDEuMjgwMTMzIC0xLjAxNTA0MiwyLjMxNzg3IC0yLjI2NzQyLDIuMzE3ODcgSCAxOC44NjkwMjIgYyAtMS4yNTIzNzMsMCAtMi4yNjc0MjQsLTEuMDM3NzM3IC0yLjI2NzQyNCwtMi4zMTc4NyB6IG0gMCw0MS43MjE1NzIgYyAwLC0xLjI4MDA4MSAxLjAxNTA1MSwtMi4zMTc4NjUgMi4yNjc0MjQsLTIuMzE3ODY1IGggMzEuNzQzOTM3IGMgMS4yNTIzNzgsMCAyLjI2NzQyLDEuMDM3Nzg0IDIuMjY3NDIsMi4zMTc4NjUgdiAwIGMgMCwxLjI4MDE1OCAtMS4wMTUwNDIsMi4zMTc4NjYgLTIuMjY3NDIsMi4zMTc4NjYgSCAxOC44NjkwMjIgYyAtMS4yNTIzNzMsMCAtMi4yNjc0MjQsLTEuMDM3NzA4IC0yLjI2NzQyNCwtMi4zMTc4NjYgeiBtIDM2LjI3ODc4MSwtMjAuODYwOCBjIDAsLTEuMjgwMDggLTEuMDE1MDQyLC0yLjMxNzg2NSAtMi4yNjc0MiwtMi4zMTc4NjUgSCAxOC44NjkwMjIgYyAtMS4yNTIzNzMsMCAtMi4yNjc0MjQsMS4wMzc3ODUgLTIuMjY3NDI0LDIuMzE3ODY1IHYgMCBjIDAsMS4yODAxNTggMS4wMTUwNTEsMi4zMTc4NjYgMi4yNjc0MjQsMi4zMTc4NjYgaCAzMS43NDM5MzcgYyAxLjI1MjM3OCwwIDIuMjY3NDIsLTEuMDM3NzA4IDIuMjY3NDIsLTIuMzE3ODY2IHoiCiAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICBpZD0icGF0aDg0MCIKICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuNzY0MTYzIiAvPgo8L3N2Zz4K" }} @@ -16,9 +16,9 @@ metadata: app.kubernetes.io/instance: incloud-web app.kubernetes.io/name: web data: - TENANT_TEXT: {{ $brandingConfig | dig "data" "tenantText" $tenantText | quote }} + CUSTOM_TENANT_TEXT: {{ $brandingConfig | dig "data" "tenantText" $tenantText | quote }} FOOTER_TEXT: {{ $brandingConfig | dig "data" "footerText" $footerText | quote }} TITLE_TEXT: {{ $brandingConfig | dig "data" "titleText" $titleText | quote }} LOGO_TEXT: {{ $brandingConfig | dig "data" "logoText" $logoText | quote }} - LOGO_SVG: {{ $brandingConfig | dig "data" "logoSvg" $logoSvg | quote }} + CUSTOM_LOGO_SVG: {{ $brandingConfig | dig "data" "logoSvg" $logoSvg | quote }} ICON_SVG: {{ $brandingConfig | dig "data" "iconSvg" $iconSvg | quote }} diff --git a/packages/system/dashboard/templates/gatekeeper.yaml b/packages/system/dashboard/templates/gatekeeper.yaml index 0bf20b02..b4503f4f 100644 --- a/packages/system/dashboard/templates/gatekeeper.yaml +++ b/packages/system/dashboard/templates/gatekeeper.yaml @@ -55,6 +55,8 @@ spec: - --http-address=0.0.0.0:8000 - --redirect-url=https://dashboard.{{ $host }}/oauth2/callback - --oidc-issuer-url=https://keycloak.{{ $host }}/realms/cozy + - --backend-logout-url=https://keycloak.{{ $host }}/realms/cozy/protocol/openid-connect/logout?id_token_hint={id_token} + - --whitelist-domain=keycloak.{{ $host }} - --email-domain=* - --pass-access-token=true - --pass-authorization-header=true diff --git a/packages/system/dashboard/templates/nginx.yaml b/packages/system/dashboard/templates/nginx.yaml index a5cbebed..caa75fa7 100644 --- a/packages/system/dashboard/templates/nginx.yaml +++ b/packages/system/dashboard/templates/nginx.yaml @@ -16,7 +16,7 @@ spec: template: metadata: annotations: - checksum/configmap-configurationnginxfile: 258c66b019c8c7f4a5d0a78dfd7bf297ce486b213346fbd2879c466abfc377e0 + checksum/config: {{ include (print $.Template.BasePath "/nginx-config.yaml") . | sha256sum }} labels: app.kubernetes.io/instance: incloud-web app.kubernetes.io/name: nginx diff --git a/packages/system/dashboard/templates/web.yaml b/packages/system/dashboard/templates/web.yaml index 8a12d3f5..17bc9c78 100644 --- a/packages/system/dashboard/templates/web.yaml +++ b/packages/system/dashboard/templates/web.yaml @@ -15,7 +15,8 @@ spec: type: RollingUpdate template: metadata: - annotations: null + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} labels: app.kubernetes.io/instance: incloud-web app.kubernetes.io/name: web @@ -42,10 +43,12 @@ spec: value: dashboard.cozystack.io - name: BASE_API_VERSION value: v1alpha1 + - name: BASE_NAMESPACE_FULL_PATH + value: "/apis/core.cozystack.io/v1alpha1/tenantnamespaces" - name: LOGGER - value: "TRUE" + value: "true" - name: LOGGER_WITH_HEADERS - value: "TRUE" + value: "false" - name: PORT value: "64231" image: {{ .Values.openapiUIK8sBff.image | quote }} @@ -92,6 +95,8 @@ spec: - env: - name: BASEPREFIX value: /openapi-ui + - name: HIDE_INSIDE + value: "true" - name: CUSTOMIZATION_API_GROUP value: dashboard.cozystack.io - name: CUSTOMIZATION_API_VERSION @@ -122,6 +127,12 @@ spec: value: tenantnamespaces - name: PROJECTS_VERSION value: v1alpha1 + - name: CUSTOM_NAMESPACE_API_RESOURCE_API_GROUP + value: core.cozystack.io + - name: CUSTOM_NAMESPACE_API_RESOURCE_API_VERSION + value: v1alpha1 + - name: CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME + value: tenantnamespaces - name: USE_NAMESPACE_NAV value: "true" - name: LOGIN_URL @@ -140,21 +151,21 @@ spec: configMapKeyRef: name: incloud-web-dashboard-config key: TITLE_TEXT - - name: TENANT_TEXT + - name: CUSTOM_TENANT_TEXT valueFrom: configMapKeyRef: name: incloud-web-dashboard-config - key: TENANT_TEXT + key: CUSTOM_TENANT_TEXT - name: LOGO_TEXT valueFrom: configMapKeyRef: name: incloud-web-dashboard-config key: LOGO_TEXT - - name: LOGO_SVG + - name: CUSTOM_LOGO_SVG valueFrom: configMapKeyRef: name: incloud-web-dashboard-config - key: LOGO_SVG + key: CUSTOM_LOGO_SVG - name: ICON_SVG valueFrom: configMapKeyRef: diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml index 4cb8571b..e1352550 100644 --- a/packages/system/dashboard/values.yaml +++ b/packages/system/dashboard/values.yaml @@ -1,6 +1,6 @@ openapiUI: - image: ghcr.io/cozystack/cozystack/openapi-ui:v0.37.0@sha256:13f38cf56830e899eb5e3d9dc8184965dd8dba9f8cd3c5ca10df0970355842d6 + image: ghcr.io/cozystack/cozystack/openapi-ui:v0.37.10@sha256:73e9a77e15c9e4e8bd72a292ee6093354048ac5d06df6db8e91ec0f3de6a37f0 openapiUIK8sBff: - image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v0.37.0@sha256:2b626dbbf87241e8621ac5b0285f402edbc2c2069ba254ca2ace2dd5c9248ac8 + image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v0.37.10@sha256:2845688891e0cf9513768aa468ff4de5b6b40e58cc763bacbfe2b417ebad1bdf tokenProxy: - image: ghcr.io/cozystack/cozystack/token-proxy:v0.37.0@sha256:fad27112617bb17816702571e1f39d0ac3fe5283468d25eb12f79906cdab566b + image: ghcr.io/cozystack/cozystack/token-proxy:v0.37.10@sha256:fad27112617bb17816702571e1f39d0ac3fe5283468d25eb12f79906cdab566b diff --git a/packages/system/fluxcd-operator/Makefile b/packages/system/fluxcd-operator/Makefile index f41360ee..84ffc6fe 100644 --- a/packages/system/fluxcd-operator/Makefile +++ b/packages/system/fluxcd-operator/Makefile @@ -10,3 +10,4 @@ update: rm -rf charts helm pull oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator --untar --untardir charts patch --no-backup-if-mismatch -p1 < patches/kubernetesEnvs.diff + patch --no-backup-if-mismatch -p1 < patches/networkPolicy.diff diff --git a/packages/system/fluxcd-operator/charts/flux-operator/templates/network-policy.yaml b/packages/system/fluxcd-operator/charts/flux-operator/templates/network-policy.yaml new file mode 100644 index 00000000..0a9db1cc --- /dev/null +++ b/packages/system/fluxcd-operator/charts/flux-operator/templates/network-policy.yaml @@ -0,0 +1,21 @@ +{{- if .Capabilities.APIVersions.Has "cilium.io/v2/CiliumClusterwideNetworkPolicy" }} +--- +apiVersion: cilium.io/v2 +kind: CiliumClusterwideNetworkPolicy +metadata: + name: {{ include "flux-operator.fullname" . }}-restrict +spec: + nodeSelector: {} + ingressDeny: + - fromEntities: + - world + toPorts: + - ports: + - port: "8080" + protocol: TCP + - port: "8081" + protocol: TCP + ingress: + - fromEntities: + - cluster +{{- end }} diff --git a/packages/system/fluxcd-operator/patches/networkPolicy.diff b/packages/system/fluxcd-operator/patches/networkPolicy.diff new file mode 100644 index 00000000..a7bf3207 --- /dev/null +++ b/packages/system/fluxcd-operator/patches/networkPolicy.diff @@ -0,0 +1,25 @@ +diff --git a/packages/system/fluxcd-operator/charts/flux-operator/templates/network-policy.yaml b/packages/system/fluxcd-operator/charts/flux-operator/templates/network-policy.yaml +new file mode 100644 +--- /dev/null (revision 52a23eacfc32430d8b008b765c64a81526521bae) ++++ b/packages/system/fluxcd-operator/charts/flux-operator/templates/network-policy.yaml (revision 52a23eacfc32430d8b008b765c64a81526521bae) +@@ -0,0 +1,18 @@ ++{{- if .Capabilities.APIVersions.Has "cilium.io/v2/CiliumClusterwideNetworkPolicy" }} ++apiVersion: cilium.io/v2 ++kind: CiliumClusterwideNetworkPolicy ++metadata: ++ name: {{ include "flux-operator.fullname" . }}-restrict ++spec: ++ nodeSelector: {} ++ ingressDeny: ++ - fromEntities: ++ - world ++ toPorts: ++ - ports: ++ - port: "8080" ++ protocol: TCP ++ - port: "8081" ++ protocol: TCP ++ ingress: ++ - fromEntities: ++ - cluster ++{{- end }} diff --git a/packages/system/kafka-operator/values.yaml b/packages/system/kafka-operator/values.yaml index 3e85e449..ddfa2c9b 100644 --- a/packages/system/kafka-operator/values.yaml +++ b/packages/system/kafka-operator/values.yaml @@ -1,4 +1,7 @@ strimzi-kafka-operator: watchAnyNamespace: true generateNetworkPolicy: false - kubernetesServiceDnsDomain: cozy.local \ No newline at end of file + kubernetesServiceDnsDomain: cozy.local + resources: + limits: + memory: 512Mi diff --git a/packages/system/kamaji/images/kamaji/patches/992.diff b/packages/system/kamaji/images/kamaji/patches/992.diff new file mode 100644 index 00000000..cd9c2ad3 --- /dev/null +++ b/packages/system/kamaji/images/kamaji/patches/992.diff @@ -0,0 +1,156 @@ +diff --git a/internal/resources/api_server_certificate.go b/internal/resources/api_server_certificate.go +index 436cdf9..4702b6c 100644 +--- a/internal/resources/api_server_certificate.go ++++ b/internal/resources/api_server_certificate.go +@@ -108,6 +108,7 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k + } + + r.resource.SetLabels(utilities.MergeMaps( ++ r.resource.GetLabels(), + utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), + map[string]string{ + constants.ControllerLabelResource: "x509", +diff --git a/internal/resources/api_server_kubelet_client_certificate.go b/internal/resources/api_server_kubelet_client_certificate.go +index 85b4d42..da18db4 100644 +--- a/internal/resources/api_server_kubelet_client_certificate.go ++++ b/internal/resources/api_server_kubelet_client_certificate.go +@@ -95,6 +95,7 @@ func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantCo + } + + r.resource.SetLabels(utilities.MergeMaps( ++ r.resource.GetLabels(), + utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), + map[string]string{ + constants.ControllerLabelResource: "x509", +diff --git a/internal/resources/ca_certificate.go b/internal/resources/ca_certificate.go +index 5425b0b..625273f 100644 +--- a/internal/resources/ca_certificate.go ++++ b/internal/resources/ca_certificate.go +@@ -137,7 +137,7 @@ func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1 + corev1.TLSPrivateKeyKey: ca.PrivateKey, + } + +- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())) ++ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))) + + utilities.SetObjectChecksum(r.resource, r.resource.Data) + +diff --git a/internal/resources/datastore/datastore_certificate.go b/internal/resources/datastore/datastore_certificate.go +index dea45ae..8492a5e 100644 +--- a/internal/resources/datastore/datastore_certificate.go ++++ b/internal/resources/datastore/datastore_certificate.go +@@ -94,6 +94,7 @@ func (r *Certificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1al + r.resource.Data["ca.crt"] = ca + + r.resource.SetLabels(utilities.MergeMaps( ++ r.resource.GetLabels(), + utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), + map[string]string{ + constants.ControllerLabelResource: "x509", +diff --git a/internal/resources/datastore/datastore_storage_config.go b/internal/resources/datastore/datastore_storage_config.go +index 7d03420..4ea9e64 100644 +--- a/internal/resources/datastore/datastore_storage_config.go ++++ b/internal/resources/datastore/datastore_storage_config.go +@@ -181,7 +181,7 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1. + + utilities.SetObjectChecksum(r.resource, r.resource.Data) + +- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())) ++ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))) + + return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()) + } +diff --git a/internal/resources/front-proxy-client-certificate.go b/internal/resources/front-proxy-client-certificate.go +index f5ed67c..2dd4eda 100644 +--- a/internal/resources/front-proxy-client-certificate.go ++++ b/internal/resources/front-proxy-client-certificate.go +@@ -95,6 +95,7 @@ func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlP + } + + r.resource.SetLabels(utilities.MergeMaps( ++ r.resource.GetLabels(), + utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), + map[string]string{ + constants.ControllerLabelResource: "x509", +diff --git a/internal/resources/front_proxy_ca_certificate.go b/internal/resources/front_proxy_ca_certificate.go +index d410720..ccadc70 100644 +--- a/internal/resources/front_proxy_ca_certificate.go ++++ b/internal/resources/front_proxy_ca_certificate.go +@@ -114,7 +114,7 @@ func (r *FrontProxyCACertificate) mutate(ctx context.Context, tenantControlPlane + kubeadmconstants.FrontProxyCAKeyName: ca.PrivateKey, + } + +- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())) ++ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))) + + utilities.SetObjectChecksum(r.resource, r.resource.Data) + +diff --git a/internal/resources/k8s_ingress_resource.go b/internal/resources/k8s_ingress_resource.go +index f2e014f..e1aef59 100644 +--- a/internal/resources/k8s_ingress_resource.go ++++ b/internal/resources/k8s_ingress_resource.go +@@ -147,7 +147,7 @@ func (r *KubernetesIngressResource) Define(_ context.Context, tenantControlPlane + + func (r *KubernetesIngressResource) mutate(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn { + return func() error { +- labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels) ++ labels := utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels) + r.resource.SetLabels(labels) + + annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Annotations) +diff --git a/internal/resources/k8s_service_resource.go b/internal/resources/k8s_service_resource.go +index 7e7f11f..9c30145 100644 +--- a/internal/resources/k8s_service_resource.go ++++ b/internal/resources/k8s_service_resource.go +@@ -76,7 +76,12 @@ func (r *KubernetesServiceResource) mutate(ctx context.Context, tenantControlPla + address, _ := tenantControlPlane.DeclaredControlPlaneAddress(ctx, r.Client) + + return func() error { +- labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels) ++ labels := utilities.MergeMaps( ++ r.resource.GetLabels(), ++ utilities.KamajiLabels( ++ tenantControlPlane.GetName(), r.GetName()), ++ tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels, ++ ) + r.resource.SetLabels(labels) + + annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Annotations) +diff --git a/internal/resources/kubeadm_config.go b/internal/resources/kubeadm_config.go +index ae4cfc0..98dc36d 100644 +--- a/internal/resources/kubeadm_config.go ++++ b/internal/resources/kubeadm_config.go +@@ -89,7 +89,7 @@ func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane * + return err + } + +- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())) ++ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))) + + params := kubeadm.Parameters{ + TenantControlPlaneAddress: address, +diff --git a/internal/resources/kubeconfig.go b/internal/resources/kubeconfig.go +index a87da7f..bd77676 100644 +--- a/internal/resources/kubeconfig.go ++++ b/internal/resources/kubeconfig.go +@@ -163,6 +163,7 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam + } + + r.resource.SetLabels(utilities.MergeMaps( ++ r.resource.GetLabels(), + utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), + map[string]string{ + constants.ControllerLabelResource: "kubeconfig", +diff --git a/internal/resources/sa_certificate.go b/internal/resources/sa_certificate.go +index b53c7b0..4001eca 100644 +--- a/internal/resources/sa_certificate.go ++++ b/internal/resources/sa_certificate.go +@@ -113,7 +113,7 @@ func (r *SACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1 + kubeadmconstants.ServiceAccountPrivateKeyName: sa.PrivateKey, + } + +- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())) ++ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))) + + utilities.SetObjectChecksum(r.resource, r.resource.Data) + diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index 906d20bf..002fdfca 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -3,7 +3,7 @@ kamaji: deploy: false image: pullPolicy: IfNotPresent - tag: v0.37.0@sha256:9f4fd5045ede2909fbaf2572e4138fcbd8921071ecf8f08446257fddd0e6f655 + tag: v0.37.10@sha256:98ff5a995f77465d2d97566a342fc6204eacd0b51c845a1c895276145cdf4819 repository: ghcr.io/cozystack/cozystack/kamaji resources: limits: @@ -13,4 +13,4 @@ kamaji: cpu: 100m memory: 100Mi extraArgs: - - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v0.37.0@sha256:9f4fd5045ede2909fbaf2572e4138fcbd8921071ecf8f08446257fddd0e6f655 + - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v0.37.10@sha256:98ff5a995f77465d2d97566a342fc6204eacd0b51c845a1c895276145cdf4819 diff --git a/packages/system/kubeovn-plunger/values.yaml b/packages/system/kubeovn-plunger/values.yaml index 11595d48..89d853b8 100644 --- a/packages/system/kubeovn-plunger/values.yaml +++ b/packages/system/kubeovn-plunger/values.yaml @@ -1,4 +1,4 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v0.37.0@sha256:9950614571ea77a55925eba0839b6b12c8e5a7a30b8858031a8c6050f261af1a +image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v0.37.10@sha256:d7a6835ef256de2ea241512f23eca376d425c89a8942d9285f6a060004c48f46 ovnCentralName: ovn-central diff --git a/packages/system/kubeovn-webhook/values.yaml b/packages/system/kubeovn-webhook/values.yaml index 116f077a..4934c288 100644 --- a/packages/system/kubeovn-webhook/values.yaml +++ b/packages/system/kubeovn-webhook/values.yaml @@ -1,3 +1,3 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v0.37.0@sha256:7e63205708e607ce2cedfe2a2cafd323ca51e3ebc71244a21ff6f9016c6c87bc +image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v0.37.10@sha256:1144245e22a546f0603aed3df046feccb0fea88c1218dee40c8acbd91ddc661a diff --git a/packages/system/kubeovn/values.yaml b/packages/system/kubeovn/values.yaml index 74f21b52..bffd03fc 100644 --- a/packages/system/kubeovn/values.yaml +++ b/packages/system/kubeovn/values.yaml @@ -2,6 +2,7 @@ kube-ovn: namespace: cozy-kubeovn func: ENABLE_NP: false + ENABLE_LB: false ipv4: POD_CIDR: "10.244.0.0/16" POD_GATEWAY: "10.244.0.1" @@ -43,7 +44,7 @@ kube-ovn: memory: "50Mi" limits: cpu: "1000m" - memory: "1Gi" + memory: "2Gi" kube-ovn-pinger: requests: cpu: "10m" @@ -64,4 +65,4 @@ global: images: kubeovn: repository: kubeovn - tag: v1.14.5@sha256:af10da442a0c6dc7df47a0ef752e2eb5c247bb0b43069fdfcb2aa51511185ea2 + tag: v1.14.5@sha256:665ad3f64332dc8b70337953ddb3ac72559137b4110c2ec36ba609ceded6b972 diff --git a/packages/system/kubevirt-csi-node/values.yaml b/packages/system/kubevirt-csi-node/values.yaml index a7fb1e67..6ff9991a 100644 --- a/packages/system/kubevirt-csi-node/values.yaml +++ b/packages/system/kubevirt-csi-node/values.yaml @@ -1,3 +1,3 @@ storageClass: replicated csiDriver: - image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:c8b08084a86251cdd18e237de89b695bca0e4f7eb1f1f6ddc2b903b4d74ea5ff + image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:47396dac04a24f824f82892c90efa160539e1e0f8ac697f5dc8ecf397e1ac413 diff --git a/packages/system/kubevirt-operator/alerts/PrometheusRule.yaml b/packages/system/kubevirt-operator/alerts/PrometheusRule.yaml index f20762df..73ef3ce4 100644 --- a/packages/system/kubevirt-operator/alerts/PrometheusRule.yaml +++ b/packages/system/kubevirt-operator/alerts/PrometheusRule.yaml @@ -10,7 +10,7 @@ spec: expr: | max_over_time( kubevirt_vm_info{ - status!="Running", + status!="running", exported_namespace=~".+", name=~".+" }[10m] @@ -27,7 +27,7 @@ spec: expr: | max_over_time( kubevirt_vmi_info{ - phase!="running", + phase!="Running", exported_namespace=~".+", name=~".+" }[10m] diff --git a/packages/system/lineage-controller-webhook/.gitignore b/packages/system/lineage-controller-webhook/.gitignore new file mode 100644 index 00000000..ada68ff0 --- /dev/null +++ b/packages/system/lineage-controller-webhook/.gitignore @@ -0,0 +1,27 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/packages/system/lineage-controller-webhook/Chart.yaml b/packages/system/lineage-controller-webhook/Chart.yaml new file mode 100644 index 00000000..5b262f05 --- /dev/null +++ b/packages/system/lineage-controller-webhook/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: cozy-lineage-controller-webhook +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/lineage-controller-webhook/Makefile b/packages/system/lineage-controller-webhook/Makefile new file mode 100644 index 00000000..04d81a12 --- /dev/null +++ b/packages/system/lineage-controller-webhook/Makefile @@ -0,0 +1,18 @@ +NAME=lineage-controller-webhook +NAMESPACE=cozy-system + +include ../../../scripts/common-envs.mk +include ../../../scripts/package.mk + +image: image-lineage-controller-webhook + +image-lineage-controller-webhook: + docker buildx build -f images/lineage-controller-webhook/Dockerfile ../../.. \ + --tag $(REGISTRY)/lineage-controller-webhook:$(call settag,$(TAG)) \ + --cache-from type=registry,ref=$(REGISTRY)/lineage-controller-webhook:latest \ + --cache-to type=inline \ + --metadata-file images/lineage-controller-webhook.json \ + $(BUILDX_ARGS) + IMAGE="$(REGISTRY)/lineage-controller-webhook:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/lineage-controller-webhook.json -o json -r)" \ + yq -i '.lineageControllerWebhook.image = strenv(IMAGE)' values.yaml + rm -f images/lineage-controller-webhook.json diff --git a/packages/system/lineage-controller-webhook/images/lineage-controller-webhook/Dockerfile b/packages/system/lineage-controller-webhook/images/lineage-controller-webhook/Dockerfile new file mode 100644 index 00000000..e043e472 --- /dev/null +++ b/packages/system/lineage-controller-webhook/images/lineage-controller-webhook/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.24-alpine AS builder + +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace + +COPY go.mod go.sum ./ +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download + +COPY api api/ +COPY pkg pkg/ +COPY cmd cmd/ +COPY internal internal/ + +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /lineage-controller-webhook cmd/lineage-controller-webhook/main.go + +FROM scratch + +COPY --from=builder /lineage-controller-webhook /lineage-controller-webhook +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +ENTRYPOINT ["/lineage-controller-webhook"] diff --git a/packages/system/lineage-controller-webhook/templates/certmanager.yaml b/packages/system/lineage-controller-webhook/templates/certmanager.yaml new file mode 100644 index 00000000..c2dc18d2 --- /dev/null +++ b/packages/system/lineage-controller-webhook/templates/certmanager.yaml @@ -0,0 +1,45 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: lineage-controller-webhook-selfsigned + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: lineage-controller-webhook-ca + namespace: {{ .Release.Namespace }} +spec: + secretName: lineage-controller-webhook-ca + duration: 43800h # 5 years + commonName: lineage-controller-webhook-ca + issuerRef: + name: lineage-controller-webhook-selfsigned + isCA: true +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: lineage-controller-webhook-ca + namespace: {{ .Release.Namespace }} +spec: + ca: + secretName: lineage-controller-webhook-ca +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: lineage-controller-webhook + namespace: {{ .Release.Namespace }} +spec: + secretName: lineage-controller-webhook-cert + duration: 8760h + renewBefore: 720h + issuerRef: + name: lineage-controller-webhook-ca + commonName: lineage-controller-webhook + dnsNames: + - lineage-controller-webhook + - lineage-controller-webhook.{{ .Release.Namespace }}.svc diff --git a/packages/system/lineage-controller-webhook/templates/daemonset.yaml b/packages/system/lineage-controller-webhook/templates/daemonset.yaml new file mode 100644 index 00000000..22074e1d --- /dev/null +++ b/packages/system/lineage-controller-webhook/templates/daemonset.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: lineage-controller-webhook + labels: + app: lineage-controller-webhook +spec: + selector: + matchLabels: + app: lineage-controller-webhook + template: + metadata: + labels: + app: lineage-controller-webhook + spec: + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + serviceAccountName: lineage-controller-webhook + containers: + - name: lineage-controller-webhook + image: "{{ .Values.lineageControllerWebhook.image }}" + {{- if .Values.lineageControllerWebhook.localK8sAPIEndpoint.enabled }} + env: + - name: KUBERNETES_SERVICE_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: KUBERNETES_SERVICE_PORT + value: "6443" + {{- end }} + args: + {{- if .Values.lineageControllerWebhook.debug }} + - --zap-log-level=debug + {{- else }} + - --zap-log-level=info + {{- end }} + ports: + - name: webhook + containerPort: 9443 + volumeMounts: + - name: webhook-certs + mountPath: /tmp/k8s-webhook-server/serving-certs + readOnly: true + volumes: + - name: webhook-certs + secret: + secretName: lineage-controller-webhook-cert + defaultMode: 0400 diff --git a/packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml b/packages/system/lineage-controller-webhook/templates/mutatingwebhookconfiguration.yaml similarity index 81% rename from packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml rename to packages/system/lineage-controller-webhook/templates/mutatingwebhookconfiguration.yaml index f246865c..88b62a21 100644 --- a/packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml +++ b/packages/system/lineage-controller-webhook/templates/mutatingwebhookconfiguration.yaml @@ -3,7 +3,7 @@ kind: MutatingWebhookConfiguration metadata: name: lineage annotations: - cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/cozystack-controller-webhook + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/lineage-controller-webhook labels: app: cozystack-controller webhooks: @@ -12,7 +12,7 @@ webhooks: sideEffects: None clientConfig: service: - name: cozystack-controller + name: lineage-controller-webhook namespace: {{ .Release.Namespace }} path: /mutate-lineage rules: @@ -40,3 +40,7 @@ webhooks: values: - kube-system - default + objectSelector: + matchExpressions: + - key: internal.cozystack.io/managed-by-cozystack + operator: DoesNotExist diff --git a/packages/system/lineage-controller-webhook/templates/rbac-bind.yaml b/packages/system/lineage-controller-webhook/templates/rbac-bind.yaml new file mode 100644 index 00000000..9a509d08 --- /dev/null +++ b/packages/system/lineage-controller-webhook/templates/rbac-bind.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: lineage-controller-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: lineage-controller-webhook +subjects: +- kind: ServiceAccount + name: lineage-controller-webhook + namespace: {{ .Release.Namespace }} diff --git a/packages/system/lineage-controller-webhook/templates/rbac.yaml b/packages/system/lineage-controller-webhook/templates/rbac.yaml new file mode 100644 index 00000000..d8b3f871 --- /dev/null +++ b/packages/system/lineage-controller-webhook/templates/rbac.yaml @@ -0,0 +1,8 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: lineage-controller-webhook +rules: +- apiGroups: ['*'] + resources: ['*'] + verbs: ["get", "list", "watch"] diff --git a/packages/system/lineage-controller-webhook/templates/sa.yaml b/packages/system/lineage-controller-webhook/templates/sa.yaml new file mode 100644 index 00000000..a17761f0 --- /dev/null +++ b/packages/system/lineage-controller-webhook/templates/sa.yaml @@ -0,0 +1,4 @@ +kind: ServiceAccount +apiVersion: v1 +metadata: + name: lineage-controller-webhook diff --git a/packages/system/cozystack-controller/templates/service.yaml b/packages/system/lineage-controller-webhook/templates/service.yaml similarity index 55% rename from packages/system/cozystack-controller/templates/service.yaml rename to packages/system/lineage-controller-webhook/templates/service.yaml index 8cc0eb61..c5df90fb 100644 --- a/packages/system/cozystack-controller/templates/service.yaml +++ b/packages/system/lineage-controller-webhook/templates/service.yaml @@ -1,10 +1,11 @@ apiVersion: v1 kind: Service metadata: - name: cozystack-controller + name: lineage-controller-webhook labels: - app: cozystack-controller + app: lineage-controller-webhook spec: + internalTrafficPolicy: Local type: ClusterIP ports: - port: 443 @@ -12,4 +13,4 @@ spec: protocol: TCP name: webhook selector: - app: cozystack-controller + app: lineage-controller-webhook diff --git a/packages/system/lineage-controller-webhook/values.yaml b/packages/system/lineage-controller-webhook/values.yaml new file mode 100644 index 00000000..c367a33f --- /dev/null +++ b/packages/system/lineage-controller-webhook/values.yaml @@ -0,0 +1,5 @@ +lineageControllerWebhook: + image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v0.37.10@sha256:bcb95c433ce24793ce1350c4f094d9d53a7c56f5415506c88f261949da42711a + debug: false + localK8sAPIEndpoint: + enabled: true diff --git a/packages/system/objectstorage-controller/values.yaml b/packages/system/objectstorage-controller/values.yaml index 489052ac..13856a2f 100644 --- a/packages/system/objectstorage-controller/values.yaml +++ b/packages/system/objectstorage-controller/values.yaml @@ -1,3 +1,3 @@ objectstorage: controller: - image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v0.37.0@sha256:5f2eed05d19ba971806374834cb16ca49282aac76130194c00b213c79ce3e10d" + image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v0.37.10@sha256:91ed0ba4de7958708f77427d2c752dece969236c13927c17033d792700e9c6f4" diff --git a/packages/system/piraeus-operator/charts/piraeus/Chart.yaml b/packages/system/piraeus-operator/charts/piraeus/Chart.yaml index 9cadb1b9..96393c85 100644 --- a/packages/system/piraeus-operator/charts/piraeus/Chart.yaml +++ b/packages/system/piraeus-operator/charts/piraeus/Chart.yaml @@ -3,8 +3,8 @@ name: piraeus description: | The Piraeus Operator manages software defined storage clusters using LINSTOR in Kubernetes. type: application -version: 2.9.0 -appVersion: "v2.9.0" +version: 2.9.1 +appVersion: "v2.9.1" maintainers: - name: Piraeus Datastore url: https://piraeus.io diff --git a/packages/system/piraeus-operator/charts/piraeus/templates/config.yaml b/packages/system/piraeus-operator/charts/piraeus/templates/config.yaml index 7bd14541..62bdb444 100644 --- a/packages/system/piraeus-operator/charts/piraeus/templates/config.yaml +++ b/packages/system/piraeus-operator/charts/piraeus/templates/config.yaml @@ -1,4 +1,4 @@ -# DO NOT EDIT; Automatically created by hack/copy-image-config-to-chart.sh +# DO NOT EDIT; Automatically created by tools/copy-image-config-to-chart.sh apiVersion: v1 kind: ConfigMap metadata: @@ -17,16 +17,16 @@ data: # quay.io/piraeusdatastore/piraeus-server:v1.24.2 components: linstor-controller: - tag: v1.31.3 + tag: v1.32.3 image: piraeus-server linstor-satellite: - tag: v1.31.3 + tag: v1.32.3 image: piraeus-server linstor-csi: - tag: v1.8.0 + tag: v1.9.0 image: piraeus-csi drbd-reactor: - tag: v1.8.0 + tag: v1.9.0 image: drbd-reactor ha-controller: tag: v1.3.0 @@ -35,45 +35,45 @@ data: tag: v1.0.0 image: drbd-shutdown-guard ktls-utils: - tag: v1.1.0 + tag: v1.2.1 image: ktls-utils drbd-module-loader: - tag: v9.2.14 + tag: v9.2.15 # The special "match" attribute is used to select an image based on the node's reported OS. # The operator will first check the k8s node's ".status.nodeInfo.osImage" field, and compare it against the list # here. If one matches, that specific image name will be used instead of the fallback image. image: drbd9-noble # Fallback image: chose a recent kernel, which can hopefully compile whatever config is actually in use match: - - osImage: Red Hat Enterprise Linux Server 7\. - image: drbd9-centos7 - osImage: Red Hat Enterprise Linux 8\. image: drbd9-almalinux8 - osImage: Red Hat Enterprise Linux 9\. image: drbd9-almalinux9 + - osImage: Red Hat Enterprise Linux 10\. + image: drbd9-almalinux10 - osImage: "Red Hat Enterprise Linux CoreOS 41[3-9]" image: drbd9-almalinux9 - osImage: Red Hat Enterprise Linux CoreOS image: drbd9-almalinux8 - - osImage: CentOS Linux 7 - image: drbd9-centos7 - osImage: CentOS Linux 8 image: drbd9-almalinux8 - osImage: AlmaLinux 8 image: drbd9-almalinux8 - osImage: AlmaLinux 9 image: drbd9-almalinux9 + - osImage: AlmaLinux 10 + image: drbd9-almalinux10 - osImage: Oracle Linux Server 8\. image: drbd9-almalinux8 - osImage: Oracle Linux Server 9\. image: drbd9-almalinux9 + - osImage: Oracle Linux Server 10\. + image: drbd9-almalinux10 - osImage: Rocky Linux 8 image: drbd9-almalinux8 - osImage: Rocky Linux 9 image: drbd9-almalinux9 - - osImage: Ubuntu 18\.04 - image: drbd9-bionic - - osImage: Ubuntu 20\.04 - image: drbd9-focal + - osImage: Rocky Linux 10 + image: drbd9-almalinux10 - osImage: Ubuntu 22\.04 image: drbd9-jammy - osImage: Ubuntu 24\.04 @@ -82,32 +82,30 @@ data: image: drbd9-bookworm - osImage: Debian GNU/Linux 11 image: drbd9-bullseye - - osImage: Debian GNU/Linux 10 - image: drbd9-buster 0_sig_storage_images.yaml: | --- base: registry.k8s.io/sig-storage components: csi-attacher: - tag: v4.9.0 + tag: v4.10.0 image: csi-attacher csi-livenessprobe: - tag: v2.16.0 + tag: v2.17.0 image: livenessprobe csi-provisioner: tag: v5.3.0 image: csi-provisioner csi-snapshotter: - tag: v8.2.1 + tag: v8.3.0 image: csi-snapshotter csi-resizer: - tag: v1.13.2 + tag: v1.14.0 image: csi-resizer csi-external-health-monitor-controller: - tag: v0.15.0 + tag: v0.16.0 image: csi-external-health-monitor-controller csi-node-driver-registrar: - tag: v2.14.0 + tag: v2.15.0 image: csi-node-driver-registrar {{- range $idx, $value := .Values.imageConfigOverride }} {{ add $idx 1 }}_helm_override.yaml: | diff --git a/packages/system/piraeus-operator/charts/piraeus/templates/rbac.yaml b/packages/system/piraeus-operator/charts/piraeus/templates/rbac.yaml index e487114c..5d49c6e6 100644 --- a/packages/system/piraeus-operator/charts/piraeus/templates/rbac.yaml +++ b/packages/system/piraeus-operator/charts/piraeus/templates/rbac.yaml @@ -1,11 +1,5 @@ -{{ if .Values.serviceAccount.create }} ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "piraeus-operator.serviceAccountName" . }} - labels: - {{- include "piraeus-operator.labels" . | nindent 4 }} +# DO NOT EDIT; Automatically created by tools/copy-rbac-config-to-chart.sh +{{ if .Values.rbac.create }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -14,448 +8,288 @@ metadata: labels: {{- include "piraeus-operator.labels" . | nindent 4 }} rules: - - apiGroups: - - "" - resources: - - configmaps - - events - - persistentvolumes - - secrets - - serviceaccounts - - services - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - "" - resources: - - configmaps - - pods - - secrets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - "" - resources: - - nodes - verbs: - - get - - list - - watch - - apiGroups: - - "" - resources: - - nodes - - persistentvolumeclaims - verbs: - - get - - list - - patch - - update - - watch - - apiGroups: - - "" - resources: - - persistentvolumeclaims/status - verbs: - - patch - - apiGroups: - - "" - resources: - - pods - verbs: - - delete - - list - - watch - - apiGroups: - - "" - resources: - - pods/eviction - verbs: - - create - - apiGroups: - - apiextensions.k8s.io - resources: - - customresourcedefinitions - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - - daemonsets - - deployments - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - - replicasets - verbs: - - get - - apiGroups: - - cert-manager.io - resources: - - certificates - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - events.k8s.io - resources: - - events - verbs: - - create - - get - - list - - patch - - update - - watch - - apiGroups: - - internal.linstor.linbit.com - resources: - - '*' - verbs: - - create - - delete - - deletecollection - - get - - list - - patch - - update - - watch - - apiGroups: - - piraeus.io - resources: - - linstorclusters - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - piraeus.io - resources: - - linstorclusters/finalizers - verbs: - - update - - apiGroups: - - piraeus.io - resources: - - linstorclusters/status - verbs: - - get - - patch - - update - - apiGroups: - - piraeus.io - resources: - - linstornodeconnections - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - piraeus.io - resources: - - linstornodeconnections/finalizers - verbs: - - update - - apiGroups: - - piraeus.io - resources: - - linstornodeconnections/status - verbs: - - get - - patch - - update - - apiGroups: - - piraeus.io - resources: - - linstorsatelliteconfigurations - verbs: - - get - - list - - watch - - apiGroups: - - piraeus.io - resources: - - linstorsatelliteconfigurations/status - verbs: - - get - - patch - - update - - apiGroups: - - piraeus.io - resources: - - linstorsatellites - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - piraeus.io - resources: - - linstorsatellites/finalizers - verbs: - - update - - apiGroups: - - piraeus.io - resources: - - linstorsatellites/status - verbs: - - get - - patch - - update - - apiGroups: - - rbac.authorization.k8s.io - resources: - - clusterrolebindings - - clusterroles - - rolebindings - - roles - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - security.openshift.io - resourceNames: - - privileged - resources: - - securitycontextconstraints - verbs: - - use - - apiGroups: - - snapshot.storage.k8s.io - resources: - - volumesnapshotclasses - - volumesnapshots - verbs: - - get - - list - - watch - - apiGroups: - - snapshot.storage.k8s.io - resources: - - volumesnapshotcontents - verbs: - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - snapshot.storage.k8s.io - resources: - - volumesnapshotcontents/status - verbs: - - patch - - update - - apiGroups: - - storage.k8s.io - resources: - - csidrivers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - storage.k8s.io - resources: - - csinodes - verbs: - - get - - list - - patch - - watch - - apiGroups: - - storage.k8s.io - resources: - - csistoragecapacities - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - storage.k8s.io - resources: - - storageclasses - verbs: - - get - - list - - watch - - apiGroups: - - storage.k8s.io - resources: - - volumeattachments - verbs: - - delete - - get - - list - - patch - - watch - - apiGroups: - - storage.k8s.io - resources: - - volumeattachments/status - verbs: - - patch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "piraeus-operator.fullname" . }}-manager-rolebinding - labels: - {{- include "piraeus-operator.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: '{{ include "piraeus-operator.fullname" . }}-controller-manager' -subjects: - - kind: ServiceAccount - name: '{{ include "piraeus-operator.serviceAccountName" . }}' - namespace: '{{ .Release.Namespace }}' -{{ end }} -{{ if.Values.rbac.create }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "piraeus-operator.fullname" . }}-proxy-role - labels: - {{- include "piraeus-operator.labels" . | nindent 4 }} -rules: - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "piraeus-operator.fullname" . }}-proxy-rolebinding - labels: - {{- include "piraeus-operator.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: '{{ include "piraeus-operator.fullname" . }}-proxy-role' -subjects: - - kind: ServiceAccount - name: {{ include "piraeus-operator.serviceAccountName" . }} - namespace: '{{ .Release.Namespace }}' +- apiGroups: + - "" + resources: + - configmaps + - events + - persistentvolumes + - pods + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - nodes + - persistentvolumeclaims + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims/status + verbs: + - patch +- apiGroups: + - "" + resources: + - pods/eviction + verbs: + - create +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - daemonsets + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get +- apiGroups: + - cert-manager.io + resources: + - certificates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - internal.linstor.linbit.com + resources: + - '*' + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - piraeus.io + resources: + - linstorclusters + - linstornodeconnections + - linstorsatellites + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - piraeus.io + resources: + - linstorclusters/finalizers + - linstornodeconnections/finalizers + - linstorsatellites/finalizers + verbs: + - update +- apiGroups: + - piraeus.io + resources: + - linstorclusters/status + - linstornodeconnections/status + - linstorsatelliteconfigurations/status + - linstorsatellites/status + verbs: + - get + - patch + - update +- apiGroups: + - piraeus.io + resources: + - linstorsatelliteconfigurations + verbs: + - get + - list + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use +- apiGroups: + - snapshot.storage.k8s.io + resources: + - volumesnapshotclasses + - volumesnapshots + verbs: + - get + - list + - watch +- apiGroups: + - snapshot.storage.k8s.io + resources: + - volumesnapshotcontents + verbs: + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - snapshot.storage.k8s.io + resources: + - volumesnapshotcontents/status + verbs: + - patch + - update +- apiGroups: + - storage.k8s.io + resources: + - csidrivers + - csistoragecapacities + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - storage.k8s.io + resources: + - csinodes + verbs: + - get + - list + - patch + - watch +- apiGroups: + - storage.k8s.io + resources: + - storageclasses + verbs: + - get + - list + - watch +- apiGroups: + - storage.k8s.io + resources: + - volumeattachments + verbs: + - delete + - get + - list + - patch + - watch +- apiGroups: + - storage.k8s.io + resources: + - volumeattachments/status + verbs: + - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: {{ include "piraeus-operator.fullname" . }}-leader-election-role + name: {{ include "piraeus-operator.fullname" . }}-leader-election labels: {{- include "piraeus-operator.labels" . | nindent 4 }} rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "piraeus-operator.fullname" . }}-leader-election-rolebinding - labels: - {{- include "piraeus-operator.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: '{{ include "piraeus-operator.fullname" . }}-leader-election-role' -subjects: - - kind: ServiceAccount - name: {{ include "piraeus-operator.serviceAccountName" . }} - namespace: '{{ .Release.Namespace }}' +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch {{ end }} diff --git a/packages/system/piraeus-operator/charts/piraeus/templates/rolebindings.yaml b/packages/system/piraeus-operator/charts/piraeus/templates/rolebindings.yaml new file mode 100644 index 00000000..97e53b95 --- /dev/null +++ b/packages/system/piraeus-operator/charts/piraeus/templates/rolebindings.yaml @@ -0,0 +1,33 @@ +{{ if .Values.rbac.create }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "piraeus-operator.fullname" . }}-leader-election + namespace: {{ .Release.Namespace }} + labels: + {{- include "piraeus-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "piraeus-operator.fullname" . }}-leader-election +subjects: +- kind: ServiceAccount + name: {{ include "piraeus-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "piraeus-operator.fullname" . }}-controller-manager + labels: + {{- include "piraeus-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "piraeus-operator.fullname" . }}-controller-manager +subjects: +- kind: ServiceAccount + name: {{ include "piraeus-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{ end }} diff --git a/packages/system/piraeus-operator/charts/piraeus/templates/serviceaccount.yaml b/packages/system/piraeus-operator/charts/piraeus/templates/serviceaccount.yaml new file mode 100644 index 00000000..89315de9 --- /dev/null +++ b/packages/system/piraeus-operator/charts/piraeus/templates/serviceaccount.yaml @@ -0,0 +1,9 @@ +{{ if .Values.serviceAccount.create }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "piraeus-operator.serviceAccountName" . }} + labels: + {{- include "piraeus-operator.labels" . | nindent 4 }} +{{ end }} diff --git a/packages/system/redis-operator/Makefile b/packages/system/redis-operator/Makefile index 9ea964dd..650cbc72 100644 --- a/packages/system/redis-operator/Makefile +++ b/packages/system/redis-operator/Makefile @@ -1,6 +1,8 @@ +REDIS_OPERATOR_TAG=$(shell grep -F 'ARG VERSION=' images/redis-operator/Dockerfile | cut -f2 -d=) export NAME=redis-operator export NAMESPACE=cozy-$(NAME) +include ../../../scripts/common-envs.mk include ../../../scripts/package.mk update: @@ -9,3 +11,16 @@ update: helm repo update redis-operator helm pull redis-operator/redis-operator --untar --untardir charts sed -i '/{{/d' charts/redis-operator/crds/databases.spotahome.com_redisfailovers.yaml + +image: + docker buildx build images/redis-operator \ + --tag $(REGISTRY)/redis-operator:$(REDIS_OPERATOR_TAG) \ + --cache-from type=registry,ref=$(REGISTRY)/redis-operator:latest \ + --cache-to type=inline \ + --metadata-file images/redis-operator.json \ + $(BUILDX_ARGS) + REPOSITORY="$(REGISTRY)/redis-operator" \ + yq -i '.redis-operator.image.repository = strenv(REPOSITORY)' values.yaml + TAG=$(REDIS_OPERATOR_TAG)@$$(yq e '."containerimage.digest"' images/redis-operator.json -o json -r) \ + yq -i '.redis-operator.image.tag = strenv(TAG)' values.yaml + rm -f images/redis-operator.json diff --git a/packages/system/redis-operator/images/redis-operator/Dockerfile b/packages/system/redis-operator/images/redis-operator/Dockerfile new file mode 100644 index 00000000..d0d6b682 --- /dev/null +++ b/packages/system/redis-operator/images/redis-operator/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.20 AS builder + +ARG VERSION=v1.3.0-rc1 + +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace + +RUN curl -sSL https://github.com/spotahome/redis-operator/archive/refs/tags/${VERSION}.tar.gz | tar -xzvf- --strip=1 + +COPY patches /patches +RUN git apply /patches/*.diff + +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH VERSION=$VERSION ./scripts/build.sh + +FROM alpine:latest +RUN apk --no-cache add \ + ca-certificates +COPY --from=builder /workspace/bin/redis-operator /usr/local/bin +RUN addgroup -g 1000 rf && \ + adduser -D -u 1000 -G rf rf && \ + chown rf:rf /usr/local/bin/redis-operator +USER rf + +ENTRYPOINT ["/usr/local/bin/redis-operator"] + diff --git a/packages/system/redis-operator/images/redis-operator/patches/labels.diff b/packages/system/redis-operator/images/redis-operator/patches/labels.diff new file mode 100644 index 00000000..fe4c4254 --- /dev/null +++ b/packages/system/redis-operator/images/redis-operator/patches/labels.diff @@ -0,0 +1,23 @@ +diff --git a/service/k8s/service.go b/service/k8s/service.go +index 712cc4c0..e84afc92 100644 +--- a/service/k8s/service.go ++++ b/service/k8s/service.go +@@ -10,6 +10,7 @@ import ( + + "github.com/spotahome/redis-operator/log" + "github.com/spotahome/redis-operator/metrics" ++ "github.com/spotahome/redis-operator/operator/redisfailover/util" + ) + + // Service the ServiceAccount service that knows how to interact with k8s to manage them +@@ -95,6 +96,10 @@ func (s *ServiceService) CreateOrUpdateService(namespace string, service *corev1 + // namespace is our spec(https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency), + // we will replace the current namespace state. + service.ResourceVersion = storedService.ResourceVersion ++ newLabels := util.MergeLabels(storedService.GetLabels(), service.GetLabels()) ++ newAnnotations := util.MergeAnnotations(storedService.GetAnnotations(), service.GetAnnotations()) ++ service.SetLabels(newLabels) ++ service.SetAnnotations(newAnnotations) + return s.UpdateService(namespace, service) + } + diff --git a/packages/system/redis-operator/values.yaml b/packages/system/redis-operator/values.yaml index eb8c61a9..77e91011 100644 --- a/packages/system/redis-operator/values.yaml +++ b/packages/system/redis-operator/values.yaml @@ -1,3 +1,4 @@ redis-operator: image: - tag: v1.3.0-rc1 + repository: ghcr.io/cozystack/cozystack/redis-operator + tag: v1.3.0-rc1@sha256:a4012e6a1b5daaedb57cc27edfdbff52124de4164b5ec0ee53c5ce5710ef4c25 diff --git a/packages/system/seaweedfs/Makefile b/packages/system/seaweedfs/Makefile index c9fd7b74..2344f6b5 100644 --- a/packages/system/seaweedfs/Makefile +++ b/packages/system/seaweedfs/Makefile @@ -1,12 +1,30 @@ -NAME=seaweedfs-system +export NAME=seaweedfs-system +include ../../../scripts/common-envs.mk include ../../../scripts/package.mk update: rm -rf charts mkdir -p charts - curl -sSL https://github.com/seaweedfs/seaweedfs/archive/refs/heads/master.tar.gz | \ - tar xzvf - --strip 3 -C charts seaweedfs-master/k8s/charts/seaweedfs + version=$$(git ls-remote --tags --sort="v:refname" https://github.com/seaweedfs/seaweedfs | grep -v '\^{}' | grep 'refs/tags/[0-9]' | awk -F'/' 'END{print $$3}') && \ + curl -sSL https://github.com/seaweedfs/seaweedfs/archive/refs/tags/$${version}.tar.gz | \ + tar xzvf - --strip 3 -C charts seaweedfs-$${version}/k8s/charts/seaweedfs && \ + sed -i.bak "/ARG VERSION/ s|=.*|=$${version}|g" images/seaweedfs/Dockerfile && \ + rm -f images/seaweedfs/Dockerfile.bak patch --no-backup-if-mismatch -p4 < patches/resize-api-server-annotation.diff - patch --no-backup-if-mismatch -p4 < patches/fix-volume-servicemonitor.patch + patch --no-backup-if-mismatch -p4 < patches/long-term-ca.diff #patch --no-backup-if-mismatch -p4 < patches/retention-policy-delete.yaml + +image: + docker buildx build images/seaweedfs \ + --tag $(REGISTRY)/seaweedfs:$(call settag,$(TAG)) \ + --cache-from type=registry,ref=$(REGISTRY)/seaweedfs:latest \ + --cache-to type=inline \ + --metadata-file images/seaweedfs.json \ + $(BUILDX_ARGS) + REGISTRY="$(REGISTRY)" \ + yq -i '.seaweedfs.image.registry = strenv(REGISTRY)' values.yaml + TAG=$(TAG)@$$(yq e '."containerimage.digest"' images/seaweedfs.json -o json -r) \ + yq -i '.seaweedfs.image.tag = strenv(TAG)' values.yaml + yq -i '.global.imageName = "seaweedfs"' values.yaml + rm -f images/seaweedfs.json diff --git a/packages/system/seaweedfs/charts/seaweedfs/Chart.yaml b/packages/system/seaweedfs/charts/seaweedfs/Chart.yaml index cd0f27a0..c595d65e 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/Chart.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v1 description: SeaweedFS name: seaweedfs -appVersion: "3.97" +appVersion: "3.99" # Dev note: Trigger a helm chart release by `git tag -a helm-` -version: 4.0.397 +version: 4.0.399 diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/all-in-one/all-in-one-deployment.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/all-in-one/all-in-one-deployment.yaml index 86bb45a8..8700a8a6 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/all-in-one/all-in-one-deployment.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/all-in-one/all-in-one-deployment.yaml @@ -79,6 +79,12 @@ spec: image: {{ template "master.image" . }} imagePullPolicy: {{ default "IfNotPresent" .Values.global.imagePullPolicy }} env: + {{- /* Determine default cluster alias and the corresponding env var keys to avoid conflicts */}} + {{- $envMerged := merge (.Values.global.extraEnvironmentVars | default dict) (.Values.allInOne.extraEnvironmentVars | default dict) }} + {{- $clusterDefault := default "sw" (index $envMerged "WEED_CLUSTER_DEFAULT") }} + {{- $clusterUpper := upper $clusterDefault }} + {{- $clusterMasterKey := printf "WEED_CLUSTER_%s_MASTER" $clusterUpper }} + {{- $clusterFilerKey := printf "WEED_CLUSTER_%s_FILER" $clusterUpper }} - name: POD_IP valueFrom: fieldRef: @@ -95,6 +101,7 @@ spec: value: "{{ template "seaweedfs.name" . }}" {{- if .Values.allInOne.extraEnvironmentVars }} {{- range $key, $value := .Values.allInOne.extraEnvironmentVars }} + {{- if and (ne $key $clusterMasterKey) (ne $key $clusterFilerKey) }} - name: {{ $key }} {{- if kindIs "string" $value }} value: {{ $value | quote }} @@ -104,8 +111,10 @@ spec: {{- end }} {{- end }} {{- end }} + {{- end }} {{- if .Values.global.extraEnvironmentVars }} {{- range $key, $value := .Values.global.extraEnvironmentVars }} + {{- if and (ne $key $clusterMasterKey) (ne $key $clusterFilerKey) }} - name: {{ $key }} {{- if kindIs "string" $value }} value: {{ $value | quote }} @@ -115,6 +124,12 @@ spec: {{- end }} {{- end }} {{- end }} + {{- end }} + # Inject computed cluster endpoints for the default cluster + - name: {{ $clusterMasterKey }} + value: {{ include "seaweedfs.cluster.masterAddress" . | quote }} + - name: {{ $clusterFilerKey }} + value: {{ include "seaweedfs.cluster.filerAddress" . | quote }} command: - "/bin/sh" - "-ec" diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml index 0fd6615e..f2572558 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml @@ -13,6 +13,8 @@ spec: secretName: {{ template "seaweedfs.name" . }}-ca-cert commonName: "{{ template "seaweedfs.name" . }}-root-ca" isCA: true + duration: 87600h + renewBefore: 720h issuerRef: name: {{ template "seaweedfs.name" . }}-issuer kind: Issuer diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/cosi/cosi-deployment.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/cosi/cosi-deployment.yaml index b200c89a..813af850 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/cosi/cosi-deployment.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/cosi/cosi-deployment.yaml @@ -15,7 +15,6 @@ spec: selector: matchLabels: app.kubernetes.io/name: {{ template "seaweedfs.name" . }} - helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/component: objectstorage-provisioner template: diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/filer/filer-ingress.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/filer/filer-ingress.yaml index 7a7c9886..9ce15ae9 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/filer/filer-ingress.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/filer/filer-ingress.yaml @@ -28,8 +28,8 @@ spec: rules: - http: paths: - - path: /sw-filer/?(.*) - pathType: ImplementationSpecific + - path: {{ .Values.filer.ingress.path | quote }} + pathType: {{ .Values.filer.ingress.pathType | quote }} backend: {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }} service: diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/master/master-ingress.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/master/master-ingress.yaml index 62d7f7a5..ac1cb339 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/master/master-ingress.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/master/master-ingress.yaml @@ -28,8 +28,8 @@ spec: rules: - http: paths: - - path: /sw-master/?(.*) - pathType: ImplementationSpecific + - path: {{ .Values.master.ingress.path | quote }} + pathType: {{ .Values.master.ingress.pathType | quote }} backend: {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }} service: diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/s3/s3-ingress.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/s3/s3-ingress.yaml index f9c36206..a856923e 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/s3/s3-ingress.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/s3/s3-ingress.yaml @@ -27,8 +27,8 @@ spec: rules: - http: paths: - - path: / - pathType: ImplementationSpecific + - path: {{ .Values.s3.ingress.path | quote }} + pathType: {{ .Values.s3.ingress.pathType | quote }} backend: {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }} service: diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/shared/_helpers.tpl b/packages/system/seaweedfs/charts/seaweedfs/templates/shared/_helpers.tpl index b15b07fa..d22d1422 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/shared/_helpers.tpl +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/shared/_helpers.tpl @@ -96,13 +96,16 @@ Inject extra environment vars in the format key:value, if populated {{/* Computes the container image name for all components (if they are not overridden) */}} {{- define "common.image" -}} {{- $registryName := default .Values.image.registry .Values.global.registry | toString -}} -{{- $repositoryName := .Values.image.repository | toString -}} +{{- $repositoryName := default .Values.image.repository .Values.global.repository | toString -}} {{- $name := .Values.global.imageName | toString -}} {{- $tag := default .Chart.AppVersion .Values.image.tag | toString -}} +{{- if $repositoryName -}} +{{- $name = printf "%s/%s" (trimSuffix "/" $repositoryName) (base $name) -}} +{{- end -}} {{- if $registryName -}} -{{- printf "%s/%s%s:%s" $registryName $repositoryName $name $tag -}} +{{- printf "%s/%s:%s" $registryName $name $tag -}} {{- else -}} -{{- printf "%s%s:%s" $repositoryName $name $tag -}} +{{- printf "%s:%s" $name $tag -}} {{- end -}} {{- end -}} @@ -219,3 +222,27 @@ or generate a new random password if it doesn't exist. {{- randAlphaNum $length -}} {{- end -}} {{- end -}} + +{{/* +Compute the master service address to be used in cluster env vars. +If allInOne is enabled, point to the all-in-one service; otherwise, point to the master service. +*/}} +{{- define "seaweedfs.cluster.masterAddress" -}} +{{- $serviceNameSuffix := "-master" -}} +{{- if .Values.allInOne.enabled -}} +{{- $serviceNameSuffix = "-all-in-one" -}} +{{- end -}} +{{- printf "%s%s.%s:%d" (include "seaweedfs.name" .) $serviceNameSuffix .Release.Namespace (int .Values.master.port) -}} +{{- end -}} + +{{/* +Compute the filer service address to be used in cluster env vars. +If allInOne is enabled, point to the all-in-one service; otherwise, point to the filer-client service. +*/}} +{{- define "seaweedfs.cluster.filerAddress" -}} +{{- $serviceNameSuffix := "-filer-client" -}} +{{- if .Values.allInOne.enabled -}} +{{- $serviceNameSuffix = "-all-in-one" -}} +{{- end -}} +{{- printf "%s%s.%s:%d" (include "seaweedfs.name" .) $serviceNameSuffix .Release.Namespace (int .Values.filer.port) -}} +{{- end -}} diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml index b60e9e44..ac82eb57 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml @@ -21,9 +21,9 @@ metadata: {{- with $.Values.global.monitoring.additionalLabels }} {{- toYaml . | nindent 4 }} {{- end }} -{{- if $.Values.volume.annotations }} +{{- with $volume.annotations }} annotations: - {{- toYaml $.Values.volume.annotations | nindent 4 }} + {{- toYaml . | nindent 4 }} {{- end }} spec: endpoints: diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-statefulset.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-statefulset.yaml index 19740160..29a035a2 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-statefulset.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-statefulset.yaml @@ -88,6 +88,9 @@ spec: - name: {{ $dir.name }} mountPath: /{{ $dir.name }} {{- end }} + {{- if $volume.containerSecurityContext.enabled }} + securityContext: {{- omit $volume.containerSecurityContext "enabled" | toYaml | nindent 12 }} + {{- end }} {{- end }} {{- if $volume.initContainers }} {{ tpl (printf "{{ $volumeName := \"%s\" }}%s" $volumeName $volume.initContainers) $ | indent 8 | trim }} diff --git a/packages/system/seaweedfs/charts/seaweedfs/values.yaml b/packages/system/seaweedfs/charts/seaweedfs/values.yaml index 351cb966..cf16623c 100644 --- a/packages/system/seaweedfs/charts/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/charts/seaweedfs/values.yaml @@ -3,6 +3,7 @@ global: createClusterRole: true registry: "" + # if repository is set, it overrides the namespace part of imageName repository: "" imageName: chrislusf/seaweedfs imagePullPolicy: IfNotPresent @@ -201,8 +202,7 @@ master: # nodeSelector labels for master pod assignment, formatted as a muli-line string. # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector # Example: - nodeSelector: | - kubernetes.io/arch: amd64 + nodeSelector: "" # nodeSelector: | # sw-backend: "true" @@ -238,6 +238,8 @@ master: className: "nginx" # host: false for "*" hostname host: "master.seaweedfs.local" + path: "/sw-master/?(.*)" + pathType: ImplementationSpecific annotations: nginx.ingress.kubernetes.io/auth-type: "basic" nginx.ingress.kubernetes.io/auth-secret: "default/ingress-basic-auth-secret" @@ -478,8 +480,7 @@ volume: # nodeSelector labels for server pod assignment, formatted as a muli-line string. # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector # Example: - nodeSelector: | - kubernetes.io/arch: amd64 + nodeSelector: "" # nodeSelector: | # sw-volume: "true" @@ -735,8 +736,7 @@ filer: # nodeSelector labels for server pod assignment, formatted as a muli-line string. # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector # Example: - nodeSelector: | - kubernetes.io/arch: amd64 + nodeSelector: "" # nodeSelector: | # sw-backend: "true" @@ -772,6 +772,8 @@ filer: className: "nginx" # host: false for "*" hostname host: "seaweedfs.cluster.local" + path: "/sw-filer/?(.*)" + pathType: ImplementationSpecific annotations: nginx.ingress.kubernetes.io/backend-protocol: GRPC nginx.ingress.kubernetes.io/auth-type: "basic" @@ -871,7 +873,7 @@ filer: # anonymousRead: false s3: - enabled: false + enabled: true imageOverride: null restartPolicy: null replicas: 1 @@ -932,8 +934,7 @@ s3: # nodeSelector labels for server pod assignment, formatted as a muli-line string. # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector # Example: - nodeSelector: | - kubernetes.io/arch: amd64 + nodeSelector: "" # nodeSelector: | # sw-backend: "true" @@ -975,6 +976,11 @@ s3: extraEnvironmentVars: + # Custom command line arguments to add to the s3 command + # Example to fix connection idle seconds: + extraArgs: ["-idleTimeout=30"] + # extraArgs: [] + # used to configure livenessProbe on s3 containers # livenessProbe: @@ -1006,6 +1012,8 @@ s3: className: "nginx" # host: false for "*" hostname host: "seaweedfs.cluster.local" + path: "/" + pathType: Prefix # additional ingress annotations for the s3 endpoint annotations: {} tls: [] @@ -1051,8 +1059,7 @@ sftp: annotations: {} resources: {} tolerations: "" - nodeSelector: | - kubernetes.io/arch: amd64 + nodeSelector: "" priorityClassName: "" serviceAccountName: "" podSecurityContext: {} @@ -1179,8 +1186,7 @@ allInOne: # nodeSelector labels for master pod assignment, formatted as a muli-line string. # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector - nodeSelector: | - kubernetes.io/arch: amd64 + nodeSelector: "" # Used to assign priority to master pods # ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/ diff --git a/packages/system/seaweedfs/images/seaweedfs/Dockerfile b/packages/system/seaweedfs/images/seaweedfs/Dockerfile new file mode 100644 index 00000000..48776a56 --- /dev/null +++ b/packages/system/seaweedfs/images/seaweedfs/Dockerfile @@ -0,0 +1,2 @@ +ARG VERSION=3.99 +FROM chrislusf/seaweedfs:${VERSION} diff --git a/packages/system/seaweedfs/patches/fix-volume-servicemonitor.patch b/packages/system/seaweedfs/patches/fix-volume-servicemonitor.patch deleted file mode 100644 index 3ca91c8b..00000000 --- a/packages/system/seaweedfs/patches/fix-volume-servicemonitor.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml ---- a/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml (revision 8951bc13d7d02b5e6982a239570ed58ed7cb025a) -+++ b/packages/system/seaweedfs/charts/seaweedfs/templates/volume/volume-servicemonitor.yaml (revision fa4fff2292c4b79a92db5cd654a3c6bf590252a6) -@@ -21,9 +21,9 @@ - {{- with $.Values.global.monitoring.additionalLabels }} - {{- toYaml . | nindent 4 }} - {{- end }} --{{- if .Values.volume.annotations }} -+{{- if $.Values.volume.annotations }} - annotations: -- {{- toYaml .Values.volume.annotations | nindent 4 }} -+ {{- toYaml $.Values.volume.annotations | nindent 4 }} - {{- end }} - spec: - endpoints: diff --git a/packages/system/seaweedfs/patches/long-term-ca.diff b/packages/system/seaweedfs/patches/long-term-ca.diff new file mode 100644 index 00000000..a53d697d --- /dev/null +++ b/packages/system/seaweedfs/patches/long-term-ca.diff @@ -0,0 +1,13 @@ +diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml +index 0fd6615e..f2572558 100644 +--- a/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml ++++ b/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml +@@ -13,6 +13,8 @@ spec: + secretName: {{ template "seaweedfs.name" . }}-ca-cert + commonName: "{{ template "seaweedfs.name" . }}-root-ca" + isCA: true ++ duration: 87600h ++ renewBefore: 720h + issuerRef: + name: {{ template "seaweedfs.name" . }}-issuer + kind: Issuer diff --git a/packages/system/seaweedfs/templates/hook.yaml b/packages/system/seaweedfs/templates/hook.yaml index 407d180f..ef02f4ee 100644 --- a/packages/system/seaweedfs/templates/hook.yaml +++ b/packages/system/seaweedfs/templates/hook.yaml @@ -2,7 +2,7 @@ {{- $configMap := lookup "v1" "ConfigMap" .Release.Namespace "seaweedfs-deployed-version" }} {{- if $configMap }} {{- $deployedVersion := dig "data" "version" "0" $configMap }} - {{- if ge $deployedVersion "2" }} + {{- if ge $deployedVersion "3" }} {{- $shouldRunUpdateHook = false }} {{- end }} {{- end }} diff --git a/packages/system/seaweedfs/templates/version.yaml b/packages/system/seaweedfs/templates/version.yaml index 4b7298b2..69b6762f 100644 --- a/packages/system/seaweedfs/templates/version.yaml +++ b/packages/system/seaweedfs/templates/version.yaml @@ -3,4 +3,4 @@ kind: ConfigMap metadata: name: seaweedfs-deployed-version data: - version: "2" + version: "3" diff --git a/packages/system/seaweedfs/values.yaml b/packages/system/seaweedfs/values.yaml index e12102e7..bfe82028 100644 --- a/packages/system/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/values.yaml @@ -1,12 +1,16 @@ global: enableSecurity: true serviceAccountName: "tenant-foo-seaweedfs" + imageName: "seaweedfs" extraEnvironmentVars: WEED_CLUSTER_SW_MASTER: "seaweedfs-master:9333" WEED_CLUSTER_SW_FILER: "seaweedfs-filer-client:8888" monitoring: enabled: true seaweedfs: + image: + tag: "latest@sha256:944e9bff98b088773847270238b63ce57dc5291054814d08e0226a139b3affb2" + registry: ghcr.io/cozystack/cozystack master: volumeSizeLimitMB: 30000 replicas: 3 @@ -83,7 +87,7 @@ seaweedfs: existingConfigSecret: null auditLogConfig: {} s3: - enabled: true + enabled: false extraArgs: - -idleTimeout=60 enableAuth: false @@ -120,7 +124,7 @@ seaweedfs: bucketClassName: "seaweedfs" region: "" sidecar: - image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.37.0@sha256:f166f09cdc9cdbb758209883819ab8261a3793bc1d7a6b6685efd5a2b2930847" + image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.37.10@sha256:233a1177f309aa159a8c4a08212dab3f992f5a2b626f86a15c8ffd44f10bf2ad" certificates: commonName: "SeaweedFS CA" ipAddresses: [] diff --git a/packages/system/velero/values.yaml b/packages/system/velero/values.yaml index d78a69c5..62f13c1f 100644 --- a/packages/system/velero/values.yaml +++ b/packages/system/velero/values.yaml @@ -13,3 +13,6 @@ velero: volumeSnapshotLocation: null namespace: cozy-velero features: EnableCSI + # Increase timeout for item operations to 24 hours to prevent timeouts + # during backups of very large volumes. The Velero default is 4 hours. + defaultItemOperationTimeout: 24h diff --git a/pkg/apis/core/v1alpha1/register.go b/pkg/apis/core/v1alpha1/register.go index 39976e39..84923d29 100644 --- a/pkg/apis/core/v1alpha1/register.go +++ b/pkg/apis/core/v1alpha1/register.go @@ -59,11 +59,9 @@ func RegisterStaticTypes(scheme *runtime.Scheme) { &TenantNamespaceList{}, &TenantSecret{}, &TenantSecretList{}, - &TenantSecretsTable{}, - &TenantSecretsTableList{}, &TenantModule{}, &TenantModuleList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) - klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret, TenantSecretsTable, TenantModule") + klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret, TenantModule") } diff --git a/pkg/apis/core/v1alpha1/tenantsecretstable_types.go b/pkg/apis/core/v1alpha1/tenantsecretstable_types.go deleted file mode 100644 index 94119696..00000000 --- a/pkg/apis/core/v1alpha1/tenantsecretstable_types.go +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package v1alpha1 - -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - -// TenantSecretEntry represents a single key from a Secret's data. -type TenantSecretEntry struct { - Name string `json:"name,omitempty"` - Key string `json:"key,omitempty"` - Value string `json:"value,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// TenantSecretsTable is a virtual, namespaced resource that exposes every key -// of Secrets labelled cozystack.io/ui=true as a separate object. -type TenantSecretsTable struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Data TenantSecretEntry `json:"data,omitempty"` -} - -// DeepCopy methods are generated by deepcopy-gen - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -type TenantSecretsTableList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []TenantSecretsTable `json:"items"` -} - -// DeepCopy methods are generated by deepcopy-gen diff --git a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go index 54a7f1e6..a19b1d1a 100644 --- a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -216,22 +216,6 @@ func (in *TenantSecret) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TenantSecretEntry) DeepCopyInto(out *TenantSecretEntry) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretEntry. -func (in *TenantSecretEntry) DeepCopy() *TenantSecretEntry { - if in == nil { - return nil - } - out := new(TenantSecretEntry) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantSecretList) DeepCopyInto(out *TenantSecretList) { *out = *in @@ -264,63 +248,3 @@ func (in *TenantSecretList) DeepCopyObject() runtime.Object { } return nil } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TenantSecretsTable) DeepCopyInto(out *TenantSecretsTable) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Data = in.Data - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretsTable. -func (in *TenantSecretsTable) DeepCopy() *TenantSecretsTable { - if in == nil { - return nil - } - out := new(TenantSecretsTable) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *TenantSecretsTable) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TenantSecretsTableList) DeepCopyInto(out *TenantSecretsTableList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]TenantSecretsTable, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretsTableList. -func (in *TenantSecretsTableList) DeepCopy() *TenantSecretsTableList { - if in == nil { - return nil - } - out := new(TenantSecretsTableList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *TenantSecretsTableList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 80220964..ba5cee5b 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -17,18 +17,22 @@ limitations under the License. package apiserver import ( + "context" "fmt" + "time" helmv2 "github.com/fluxcd/helm-controller/api/v2" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "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" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/cozystack/cozystack/pkg/apis/apps" appsinstall "github.com/cozystack/cozystack/pkg/apis/apps/install" @@ -40,7 +44,6 @@ import ( tenantmodulestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantmodule" tenantnamespacestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantnamespace" tenantsecretstorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecret" - tenantsecretstablestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecretstable" ) var ( @@ -50,6 +53,7 @@ var ( // versions and content types. Codecs = serializer.NewCodecFactory(Scheme) CozyComponentName = "cozy" + syncPeriod = 5 * time.Minute ) func init() { @@ -58,9 +62,15 @@ func init() { // Register HelmRelease types. if err := helmv2.AddToScheme(Scheme); err != nil { - panic(fmt.Sprintf("Failed to add HelmRelease types to scheme: %v", err)) + panic(fmt.Errorf("Failed to add HelmRelease types to scheme: %w", err)) } + if err := corev1.AddToScheme(Scheme); err != nil { + panic(fmt.Errorf("Failed to add core types to scheme: %w", err)) + } + if err := rbacv1.AddToScheme(Scheme); err != nil { + panic(fmt.Errorf("Failed to add RBAC types to scheme: %w", err)) + } // Add unversioned types. metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) @@ -118,44 +128,56 @@ func (c completedConfig) New() (*CozyServer, error) { } // Create a dynamic client for HelmRelease using InClusterConfig. - inClusterConfig, err := restclient.InClusterConfig() + cfg, err := ctrl.GetConfig() if err != nil { - return nil, fmt.Errorf("unable to get in-cluster config: %v", err) + return nil, fmt.Errorf("failed to get kubeconfig: %w", err) } - dynamicClient, err := dynamic.NewForConfig(inClusterConfig) + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: Scheme, + Cache: cache.Options{SyncPeriod: &syncPeriod}, + }) if err != nil { - return nil, fmt.Errorf("unable to create dynamic client: %v", err) + return nil, fmt.Errorf("failed to build manager: %w", err) } - clientset, err := kubernetes.NewForConfig(inClusterConfig) - if err != nil { - return nil, fmt.Errorf("create kube clientset: %v", err) + ctx := ctrl.SetupSignalHandler() + + if err = mustGetInformers(ctx, mgr, + &helmv2.HelmRelease{}, + &corev1.Secret{}, + &corev1.Namespace{}, + &corev1.Service{}, + &rbacv1.RoleBinding{}, + ); err != nil { + return nil, fmt.Errorf("failed to get informers: %w", err) } + go func() { + if err := mgr.Start(ctx); err != nil { + panic(fmt.Errorf("manager start failed: %w", err)) + } + }() + + if ok := mgr.GetCache().WaitForCacheSync(ctx); !ok { + return nil, fmt.Errorf("cache sync failed") + } + + cli := mgr.GetClient() + watchCli, err := client.NewWithWatch(cfg, client.Options{Scheme: Scheme}) + if err != nil { + return nil, fmt.Errorf("failed to build watch client: %w", err) + } // --- static, cluster-scoped resource for core group --- coreV1alpha1Storage := map[string]rest.Storage{} coreV1alpha1Storage["tenantnamespaces"] = cozyregistry.RESTInPeace( - tenantnamespacestorage.NewREST( - clientset.CoreV1(), - clientset.AuthorizationV1(), - 20, - ), + tenantnamespacestorage.NewREST(cli, watchCli), ) coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace( - tenantsecretstorage.NewREST( - clientset.CoreV1(), - ), - ) - coreV1alpha1Storage["tenantsecretstables"] = cozyregistry.RESTInPeace( - tenantsecretstablestorage.NewREST( - clientset.CoreV1(), - ), + tenantsecretstorage.NewREST(cli, watchCli), ) coreV1alpha1Storage["tenantmodules"] = cozyregistry.RESTInPeace( - tenantmodulestorage.NewREST( - dynamicClient, - ), + tenantmodulestorage.NewREST(cli, watchCli), ) coreApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(core.GroupName, Scheme, metav1.ParameterCodec, Codecs) @@ -167,7 +189,7 @@ func (c completedConfig) New() (*CozyServer, error) { // --- dynamically-configured, per-tenant resources --- appsV1alpha1Storage := map[string]rest.Storage{} for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(dynamicClient, &resConfig) + storage := applicationstorage.NewREST(cli, watchCli, &resConfig) appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) } appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs) @@ -178,3 +200,12 @@ func (c completedConfig) New() (*CozyServer, error) { return s, nil } + +func mustGetInformers(ctx context.Context, mgr ctrl.Manager, types ...client.Object) error { + for i := range types { + if _, err := mgr.GetCache().GetInformer(ctx, types[i]); err != nil { + return fmt.Errorf("failed to get informer for %T: %w", types[i], err) + } + } + return nil +} diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index a88f259f..4202bbac 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -39,10 +39,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace": schema_pkg_apis_core_v1alpha1_TenantNamespace(ref), "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespaceList": schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref), "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret": schema_pkg_apis_core_v1alpha1_TenantSecret(ref), - "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry": schema_pkg_apis_core_v1alpha1_TenantSecretEntry(ref), "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretList": schema_pkg_apis_core_v1alpha1_TenantSecretList(ref), - "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable": schema_pkg_apis_core_v1alpha1_TenantSecretsTable(ref), - "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTableList": schema_pkg_apis_core_v1alpha1_TenantSecretsTableList(ref), "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionRequest": schema_pkg_apis_apiextensions_v1_ConversionRequest(ref), "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionResponse": schema_pkg_apis_apiextensions_v1_ConversionResponse(ref), "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionReview": schema_pkg_apis_apiextensions_v1_ConversionReview(ref), @@ -557,37 +554,6 @@ func schema_pkg_apis_core_v1alpha1_TenantSecret(ref common.ReferenceCallback) co } } -func schema_pkg_apis_core_v1alpha1_TenantSecretEntry(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "TenantSecretEntry represents a single key from a Secret's data.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "name": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "key": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "value": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - } -} - func schema_pkg_apis_core_v1alpha1_TenantSecretList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -636,95 +602,6 @@ func schema_pkg_apis_core_v1alpha1_TenantSecretList(ref common.ReferenceCallback } } -func schema_pkg_apis_core_v1alpha1_TenantSecretsTable(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "TenantSecretsTable is a virtual, namespaced resource that exposes every key of Secrets labelled cozystack.io/ui=true as a separate object.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "data": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_core_v1alpha1_TenantSecretsTableList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - func schema_pkg_apis_apiextensions_v1_ConversionRequest(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 388d741f..37a5d9a5 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -37,8 +37,8 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/client-go/dynamic" "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" "github.com/cozystack/cozystack/pkg/config" @@ -76,7 +76,8 @@ var helmReleaseGVR = schema.GroupVersionResource{ // REST implements the RESTStorage interface for Application resources type REST struct { - dynamicClient dynamic.Interface + c client.Client + w client.WithWatch gvr schema.GroupVersionResource gvk schema.GroupVersionKind kindName string @@ -86,7 +87,7 @@ type REST struct { } // NewREST creates a new REST storage for Application with specific configuration -func NewREST(dynamicClient dynamic.Interface, config *config.Resource) *REST { +func NewREST(c client.Client, w client.WithWatch, config *config.Resource) *REST { var specSchema *structuralschema.Structural if raw := strings.TrimSpace(config.Application.OpenAPISchema); raw != "" { @@ -110,7 +111,8 @@ func NewREST(dynamicClient dynamic.Interface, config *config.Resource) *REST { } return &REST{ - dynamicClient: dynamicClient, + c: c, + w: w, gvr: schema.GroupVersionResource{ Group: appsv1alpha1.GroupName, Version: "v1alpha1", @@ -158,30 +160,23 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation helmRelease.Labels = mergeMaps(helmRelease.Labels, addPrefixedMap(app.Labels, LabelPrefix)) // Note: Annotations from config are not handled as r.releaseConfig.Annotations is undefined - // Convert HelmRelease to unstructured format - unstructuredHR, err := runtime.DefaultUnstructuredConverter.ToUnstructured(helmRelease) - if err != nil { - klog.Errorf("Failed to convert HelmRelease to unstructured: %v", err) - return nil, fmt.Errorf("failed to convert HelmRelease to unstructured: %v", err) - } - klog.V(6).Infof("Creating HelmRelease %s in namespace %s", helmRelease.Name, app.Namespace) // Create HelmRelease in Kubernetes - createdHR, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(app.Namespace).Create(ctx, &unstructured.Unstructured{Object: unstructuredHR}, *options) + err = r.c.Create(ctx, helmRelease, &client.CreateOptions{Raw: options}) if err != nil { klog.Errorf("Failed to create HelmRelease %s: %v", helmRelease.Name, err) return nil, fmt.Errorf("failed to create HelmRelease: %v", err) } // Convert the created HelmRelease back to Application - convertedApp, err := r.ConvertHelmReleaseToApplication(createdHR) + convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) if err != nil { - klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", createdHR.GetName(), err) + klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", helmRelease.GetName(), err) return nil, fmt.Errorf("conversion error: %v", err) } - klog.V(6).Infof("Successfully created and converted HelmRelease %s to Application", createdHR.GetName()) + klog.V(6).Infof("Successfully created and converted HelmRelease %s to Application", helmRelease.GetName()) // Convert Application to unstructured format unstructuredApp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&convertedApp) @@ -206,7 +201,8 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) // Get the corresponding HelmRelease using the new prefix helmReleaseName := r.releaseConfig.Prefix + name - hr, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Get(ctx, helmReleaseName, *options) + helmRelease := &helmv2.HelmRelease{} + err = r.c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: helmReleaseName}, helmRelease, &client.GetOptions{Raw: options}) if err != nil { klog.Errorf("Error retrieving HelmRelease for resource %s: %v", name, err) @@ -221,14 +217,14 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) } // Check if HelmRelease meets the required chartName and sourceRef criteria - if !r.shouldIncludeHelmRelease(hr) { + if !r.shouldIncludeHelmRelease(helmRelease) { klog.Errorf("HelmRelease %s does not match the required chartName and sourceRef criteria", helmReleaseName) // Return a NotFound error for the Application resource return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) } // Convert HelmRelease to Application - convertedApp, err := r.ConvertHelmReleaseToApplication(hr) + convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) if err != nil { klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", name, err) return nil, fmt.Errorf("conversion error: %v", err) @@ -325,7 +321,11 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption } // List HelmReleases with mapped selectors - hrList, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).List(ctx, metaOptions) + 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 @@ -335,14 +335,14 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption items := make([]unstructured.Unstructured, 0) // Iterate over HelmReleases and convert to Applications - for _, hr := range hrList.Items { - if !r.shouldIncludeHelmRelease(&hr) { + for i := range hrList.Items { + if !r.shouldIncludeHelmRelease(&hrList.Items[i]) { continue } - app, err := r.ConvertHelmReleaseToApplication(&hr) + app, err := r.ConvertHelmReleaseToApplication(&hrList.Items[i]) if err != nil { - klog.Errorf("Error converting HelmRelease %s to Application: %v", hr.GetName(), err) + klog.Errorf("Error converting HelmRelease %s to Application: %v", hrList.Items[i].GetName(), err) continue } @@ -457,7 +457,8 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje // Ensure ResourceVersion if helmRelease.ResourceVersion == "" { - cur, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(helmRelease.Namespace).Get(ctx, helmRelease.Name, metav1.GetOptions{}) + cur := &helmv2.HelmRelease{} + err := r.c.Get(ctx, client.ObjectKey{Namespace: helmRelease.Namespace, Name: helmRelease.Name}, cur, &client.GetOptions{Raw: &metav1.GetOptions{}}) if err != nil { return nil, false, fmt.Errorf("failed to fetch current HelmRelease: %w", err) } @@ -470,53 +471,38 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje helmRelease.Labels = mergeMaps(helmRelease.Labels, addPrefixedMap(app.Labels, LabelPrefix)) // Note: Annotations from config are not handled as r.releaseConfig.Annotations is undefined - // Convert HelmRelease to unstructured format - unstructuredHR, err := runtime.DefaultUnstructuredConverter.ToUnstructured(helmRelease) - if err != nil { - klog.Errorf("Failed to convert HelmRelease to unstructured: %v", err) - return nil, false, fmt.Errorf("failed to convert HelmRelease to unstructured: %v", err) - } - - // Retrieve metadata from unstructured object - metadata, found, err := unstructured.NestedMap(unstructuredHR, "metadata") - if err != nil || !found { - klog.Errorf("Failed to retrieve metadata from HelmRelease: %v, found: %v", err, found) - return nil, false, fmt.Errorf("failed to retrieve metadata from HelmRelease: %v", err) - } - klog.V(6).Infof("HelmRelease Metadata: %+v", metadata) - klog.V(6).Infof("Updating HelmRelease %s in namespace %s", helmRelease.Name, helmRelease.Namespace) // Before updating, ensure the HelmRelease meets the inclusion criteria // This prevents updating HelmReleases that should not be managed as Applications - if !r.shouldIncludeHelmRelease(&unstructured.Unstructured{Object: unstructuredHR}) { + if !r.shouldIncludeHelmRelease(helmRelease) { klog.Errorf("HelmRelease %s does not match the required chartName and sourceRef criteria", helmRelease.Name) // Return a NotFound error for the Application resource return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) } // Update the HelmRelease in Kubernetes - resultHR, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(helmRelease.Namespace).Update(ctx, &unstructured.Unstructured{Object: unstructuredHR}, metav1.UpdateOptions{}) + err = r.c.Update(ctx, helmRelease, &client.UpdateOptions{Raw: &metav1.UpdateOptions{}}) if err != nil { klog.Errorf("Failed to update HelmRelease %s: %v", helmRelease.Name, err) return nil, false, fmt.Errorf("failed to update HelmRelease: %v", err) } // After updating, ensure the updated HelmRelease still meets the inclusion criteria - if !r.shouldIncludeHelmRelease(resultHR) { - klog.Errorf("Updated HelmRelease %s does not match the required chartName and sourceRef criteria", resultHR.GetName()) + if !r.shouldIncludeHelmRelease(helmRelease) { + klog.Errorf("Updated HelmRelease %s does not match the required chartName and sourceRef criteria", helmRelease.GetName()) // Return a NotFound error for the Application resource return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) } // Convert the updated HelmRelease back to Application - convertedApp, err := r.ConvertHelmReleaseToApplication(resultHR) + convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) if err != nil { - klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", resultHR.GetName(), err) + klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", helmRelease.GetName(), err) return nil, false, fmt.Errorf("conversion error: %v", err) } - klog.V(6).Infof("Successfully updated and converted HelmRelease %s to Application", resultHR.GetName()) + klog.V(6).Infof("Successfully updated and converted HelmRelease %s to Application", helmRelease.GetName()) // Explicitly set apiVersion and kind for Application convertedApp.TypeMeta = metav1.TypeMeta{ @@ -554,7 +540,8 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va helmReleaseName := r.releaseConfig.Prefix + name // Retrieve the HelmRelease before attempting to delete - hr, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Get(ctx, helmReleaseName, metav1.GetOptions{}) + helmRelease := &helmv2.HelmRelease{} + err = r.c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: helmReleaseName}, helmRelease, &client.GetOptions{Raw: &metav1.GetOptions{}}) if err != nil { if apierrors.IsNotFound(err) { // If HelmRelease does not exist, return NotFound error for Application @@ -567,7 +554,7 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va } // Validate that the HelmRelease meets the inclusion criteria - if !r.shouldIncludeHelmRelease(hr) { + if !r.shouldIncludeHelmRelease(helmRelease) { klog.Errorf("HelmRelease %s does not match the required chartName and sourceRef criteria", helmReleaseName) // Return NotFound error for Application resource return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) @@ -576,7 +563,7 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va klog.V(6).Infof("Deleting HelmRelease %s in namespace %s", helmReleaseName, namespace) // Delete the HelmRelease corresponding to the Application - err = r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Delete(ctx, helmReleaseName, *options) + err = r.c.Delete(ctx, helmRelease, &client.DeleteOptions{Raw: options}) if err != nil { klog.Errorf("Failed to delete HelmRelease %s: %v", helmReleaseName, err) return nil, false, fmt.Errorf("failed to delete HelmRelease: %v", err) @@ -659,7 +646,11 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio } // Start watch on HelmRelease with mapped selectors - helmWatcher, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Watch(ctx, metaOptions) + 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 @@ -669,13 +660,15 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio 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 := <-helmWatcher.ResultChan(): + 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") @@ -689,19 +682,19 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio continue // Skip processing this event } - // Proceed with processing Unstructured objects - matches, err := r.isRelevantHelmRelease(&event) - if err != nil { - klog.V(4).Infof("Non-critical error filtering HelmRelease event: %v", err) + // 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 !matches { + if !r.shouldIncludeHelmRelease(hr) { continue } // Convert HelmRelease to Application - app, err := r.ConvertHelmReleaseToApplication(event.Object.(*unstructured.Unstructured)) + app, err := r.ConvertHelmReleaseToApplication(hr) if err != nil { klog.Errorf("Error converting HelmRelease to Application: %v", err) continue @@ -771,12 +764,16 @@ 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() + } }) } @@ -785,34 +782,18 @@ func (cw *customWatcher) ResultChan() <-chan watch.Event { return cw.resultChan } -// isRelevantHelmRelease checks if the HelmRelease meets the sourceRef and prefix criteria -func (r *REST) isRelevantHelmRelease(event *watch.Event) (bool, error) { - if event.Object == nil { - return false, nil - } - - // Check if the object is a *v1.Status - if status, ok := event.Object.(*metav1.Status); ok { - // Log at a less severe level or handle specific status errors if needed - klog.V(4).Infof("Received Status object in HelmRelease watch: %v", status.Message) - return false, nil // Not relevant for processing as a HelmRelease - } - - // Proceed if it's an Unstructured object - hr, ok := event.Object.(*unstructured.Unstructured) - if !ok { - return false, fmt.Errorf("expected Unstructured object, got %T", event.Object) - } - - return r.shouldIncludeHelmRelease(hr), nil -} - // shouldIncludeHelmRelease determines if a HelmRelease should be included based on filtering criteria -func (r *REST) shouldIncludeHelmRelease(hr *unstructured.Unstructured) bool { +func (r *REST) shouldIncludeHelmRelease(hr *helmv2.HelmRelease) bool { + // Nil check for Chart field + if hr.Spec.Chart == nil { + klog.V(6).Infof("HelmRelease %s has nil spec.chart field", hr.GetName()) + return false + } + // Filter by Chart Name - chartName, found, err := unstructured.NestedString(hr.Object, "spec", "chart", "spec", "chart") - if err != nil || !found { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.chart field: %v", hr.GetName(), err) + chartName := hr.Spec.Chart.Spec.Chart + if chartName == "" { + klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.chart field", hr.GetName()) return false } if chartName != r.releaseConfig.Chart.Name { @@ -825,21 +806,29 @@ func (r *REST) shouldIncludeHelmRelease(hr *unstructured.Unstructured) bool { } // matchesSourceRefAndPrefix checks both SourceRefConfig and Prefix criteria -func (r *REST) matchesSourceRefAndPrefix(hr *unstructured.Unstructured) bool { +func (r *REST) matchesSourceRefAndPrefix(hr *helmv2.HelmRelease) bool { + // Nil check for Chart field (defensive) + if hr.Spec.Chart == nil { + klog.V(6).Infof("HelmRelease %s has nil spec.chart field", hr.GetName()) + return false + } + // Extract SourceRef fields - sourceRefKind, found, err := unstructured.NestedString(hr.Object, "spec", "chart", "spec", "sourceRef", "kind") - if err != nil || !found { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.kind field: %v", hr.GetName(), err) + sourceRef := hr.Spec.Chart.Spec.SourceRef + sourceRefKind := sourceRef.Kind + sourceRefName := sourceRef.Name + sourceRefNamespace := sourceRef.Namespace + + if sourceRefKind == "" { + klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.kind field", hr.GetName()) return false } - sourceRefName, found, err := unstructured.NestedString(hr.Object, "spec", "chart", "spec", "sourceRef", "name") - if err != nil || !found { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.name field: %v", hr.GetName(), err) + if sourceRefName == "" { + klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.name field", hr.GetName()) return false } - sourceRefNamespace, found, err := unstructured.NestedString(hr.Object, "spec", "chart", "spec", "sourceRef", "namespace") - if err != nil || !found { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.namespace field: %v", hr.GetName(), err) + if sourceRefNamespace == "" { + klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.namespace field", hr.GetName()) return false } @@ -930,19 +919,11 @@ func filterPrefixedMap(original map[string]string, prefix string) map[string]str } // ConvertHelmReleaseToApplication converts a HelmRelease to an Application -func (r *REST) ConvertHelmReleaseToApplication(hr *unstructured.Unstructured) (appsv1alpha1.Application, error) { +func (r *REST) ConvertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { klog.V(6).Infof("Converting HelmRelease to Application for resource %s", hr.GetName()) - var helmRelease helmv2.HelmRelease - // Convert unstructured to HelmRelease struct - err := runtime.DefaultUnstructuredConverter.FromUnstructured(hr.Object, &helmRelease) - if err != nil { - klog.Errorf("Error converting from unstructured to HelmRelease: %v", err) - return appsv1alpha1.Application{}, err - } - // Convert HelmRelease struct to Application struct - app, err := r.convertHelmReleaseToApplication(&helmRelease) + app, err := r.convertHelmReleaseToApplication(hr) if err != nil { klog.Errorf("Error converting from HelmRelease to Application: %v", err) return appsv1alpha1.Application{}, err diff --git a/pkg/registry/core/tenantmodule/rest.go b/pkg/registry/core/tenantmodule/rest.go index aa7d4eeb..852a0b64 100644 --- a/pkg/registry/core/tenantmodule/rest.go +++ b/pkg/registry/core/tenantmodule/rest.go @@ -32,12 +32,13 @@ import ( "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/client-go/dynamic" "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" @@ -69,17 +70,19 @@ var helmReleaseGVR = schema.GroupVersionResource{ // REST implements the RESTStorage interface for TenantModule resources type REST struct { - dynamicClient dynamic.Interface - gvr schema.GroupVersionResource - gvk schema.GroupVersionKind - kindName string - singularName string + 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(dynamicClient dynamic.Interface) *REST { +func NewREST(c client.Client, w client.WithWatch) *REST { return &REST{ - dynamicClient: dynamicClient, + c: c, + w: w, gvr: schema.GroupVersionResource{ Group: corev1alpha1.GroupName, Version: "v1alpha1", @@ -115,7 +118,8 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) klog.V(6).Infof("Attempting to retrieve TenantModule %s in namespace %s", name, namespace) // Get the corresponding HelmRelease - hr, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Get(ctx, name, *options) + 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) @@ -231,7 +235,11 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption } // List HelmReleases with mapped selectors - hrList, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).List(ctx, metaOptions) + 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 @@ -241,15 +249,15 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption items := make([]unstructured.Unstructured, 0) // Iterate over HelmReleases and convert to TenantModules - for _, hr := range hrList.Items { + for i := range hrList.Items { // Double-check the label requirement - if !r.hasTenantModuleLabel(&hr) { + if !r.hasTenantModuleLabel(&hrList.Items[i]) { continue } - module, err := r.ConvertHelmReleaseToTenantModule(&hr) + module, err := r.ConvertHelmReleaseToTenantModule(&hrList.Items[i]) if err != nil { - klog.Errorf("Error converting HelmRelease %s to TenantModule: %v", hr.GetName(), err) + klog.Errorf("Error converting HelmRelease %s to TenantModule: %v", hrList.Items[i].GetName(), err) continue } @@ -376,7 +384,11 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio } // Start watch on HelmRelease with mapped selectors - helmWatcher, err := r.dynamicClient.Resource(helmReleaseGVR).Namespace(namespace).Watch(ctx, metaOptions) + 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 @@ -386,13 +398,15 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio 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 := <-helmWatcher.ResultChan(): + 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") @@ -406,19 +420,19 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio continue // Skip processing this event } - // Proceed with processing Unstructured objects - matches, err := r.isRelevantHelmRelease(&event) - if err != nil { - klog.V(4).Infof("Non-critical error filtering HelmRelease event: %v", err) + // 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 !matches { + if !r.hasTenantModuleLabel(hr) { continue } // Convert HelmRelease to TenantModule - module, err := r.ConvertHelmReleaseToTenantModule(event.Object.(*unstructured.Unstructured)) + module, err := r.ConvertHelmReleaseToTenantModule(hr) if err != nil { klog.Errorf("Error converting HelmRelease to TenantModule: %v", err) continue @@ -480,12 +494,16 @@ 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() + } }) } @@ -494,30 +512,8 @@ func (cw *customWatcher) ResultChan() <-chan watch.Event { return cw.resultChan } -// isRelevantHelmRelease checks if the HelmRelease has the tenant module label -func (r *REST) isRelevantHelmRelease(event *watch.Event) (bool, error) { - if event.Object == nil { - return false, nil - } - - // Check if the object is a *v1.Status - if status, ok := event.Object.(*metav1.Status); ok { - // Log at a less severe level or handle specific status errors if needed - klog.V(4).Infof("Received Status object in HelmRelease watch: %v", status.Message) - return false, nil // Not relevant for processing as a HelmRelease - } - - // Proceed if it's an Unstructured object - hr, ok := event.Object.(*unstructured.Unstructured) - if !ok { - return false, fmt.Errorf("expected Unstructured object, got %T", event.Object) - } - - return r.hasTenantModuleLabel(hr), nil -} - // hasTenantModuleLabel checks if a HelmRelease has the required tenant module label -func (r *REST) hasTenantModuleLabel(hr *unstructured.Unstructured) bool { +func (r *REST) hasTenantModuleLabel(hr *helmv2.HelmRelease) bool { labels := hr.GetLabels() if labels == nil { return false @@ -554,19 +550,11 @@ func (r *REST) getNamespace(ctx context.Context) (string, error) { } // ConvertHelmReleaseToTenantModule converts a HelmRelease to a TenantModule -func (r *REST) ConvertHelmReleaseToTenantModule(hr *unstructured.Unstructured) (corev1alpha1.TenantModule, error) { +func (r *REST) ConvertHelmReleaseToTenantModule(hr *helmv2.HelmRelease) (corev1alpha1.TenantModule, error) { klog.V(6).Infof("Converting HelmRelease to TenantModule for resource %s", hr.GetName()) - var helmRelease helmv2.HelmRelease - // Convert unstructured to HelmRelease struct - err := runtime.DefaultUnstructuredConverter.FromUnstructured(hr.Object, &helmRelease) - if err != nil { - klog.Errorf("Error converting from unstructured to HelmRelease: %v", err) - return corev1alpha1.TenantModule{}, err - } - // Convert HelmRelease struct to TenantModule struct - module, err := r.convertHelmReleaseToTenantModule(&helmRelease) + module, err := r.convertHelmReleaseToTenantModule(hr) if err != nil { klog.Errorf("Error converting from HelmRelease to TenantModule: %v", err) return corev1alpha1.TenantModule{}, err diff --git a/pkg/registry/core/tenantnamespace/rest.go b/pkg/registry/core/tenantnamespace/rest.go index 3b18f3e3..68d9fe3c 100644 --- a/pkg/registry/core/tenantnamespace/rest.go +++ b/pkg/registry/core/tenantnamespace/rest.go @@ -7,26 +7,23 @@ package tenantnamespace import ( "context" "fmt" - "math" "net/http" "strings" - "sync" "time" - authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/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/types" "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" + "sigs.k8s.io/controller-runtime/pkg/client" corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" ) @@ -50,21 +47,18 @@ var ( ) type REST struct { - core corev1client.CoreV1Interface - authClient authorizationv1client.AuthorizationV1Interface - maxWorkers int - gvr schema.GroupVersionResource + c client.Client + w client.WithWatch + gvr schema.GroupVersionResource } func NewREST( - coreCli corev1client.CoreV1Interface, - authCli authorizationv1client.AuthorizationV1Interface, - maxWorkers int, + c client.Client, + w client.WithWatch, ) *REST { return &REST{ - core: coreCli, - authClient: authCli, - maxWorkers: maxWorkers, + c: c, + w: w, gvr: schema.GroupVersionResource{ Group: corev1alpha1.GroupName, Version: "v1alpha1", @@ -96,7 +90,8 @@ func (r *REST) List( ctx context.Context, _ *metainternal.ListOptions, ) (runtime.Object, error) { - nsList, err := r.core.Namespaces().List(ctx, metav1.ListOptions{}) + nsList := &corev1.NamespaceList{} + err := r.c.List(ctx, nsList) if err != nil { return nil, err } @@ -125,7 +120,8 @@ func (r *REST) Get( return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) } - ns, err := r.core.Namespaces().Get(ctx, name, *opts) + ns := &corev1.Namespace{} + err := r.c.Get(ctx, types.NamespacedName{Namespace: "", Name: name}, ns, &client.GetOptions{Raw: opts}) if err != nil { return nil, err } @@ -135,14 +131,7 @@ func (r *REST) Get( 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, - }, + ObjectMeta: ns.ObjectMeta, }, nil } @@ -151,10 +140,11 @@ func (r *REST) Get( // ----------------------------------------------------------------------------- func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { - nsWatch, err := r.core.Namespaces().Watch(ctx, metav1.ListOptions{ + nsList := &corev1.NamespaceList{} + nsWatch, err := r.w.Watch(ctx, nsList, &client.ListOptions{Raw: &metav1.ListOptions{ Watch: true, ResourceVersion: opts.ResourceVersion, - }) + }}) if err != nil { return nil, err } @@ -271,76 +261,66 @@ 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 + u, ok := request.UserFrom(ctx) + if !ok { + return []string{}, fmt.Errorf("user missing in context") } - 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} - } - }() + groups := make(map[string]struct{}) + for _, group := range u.GetGroups() { + groups[group] = struct{}{} } - 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) + if _, ok = groups["system:masters"]; ok { + return names, nil + } + if _, ok = groups["cozystack-cluster-admin"]; ok { + return names, nil + } + nameSet := make(map[string]struct{}) + for _, name := range names { + nameSet[name] = struct{}{} + } + rbs := &rbacv1.RoleBindingList{} + err := r.c.List(ctx, rbs) + if err != nil { + return []string{}, fmt.Errorf("failed to list rolebindings: %w", err) + } + allowedNameSet := make(map[string]struct{}) + for i := range rbs.Items { + if _, ok := allowedNameSet[rbs.Items[i].Namespace]; ok { continue } - if r.allowed { - allowed = append(allowed, r.name) + if _, ok := nameSet[rbs.Items[i].Namespace]; !ok { + continue + } + subjectLoop: + for j := range rbs.Items[i].Subjects { + subj := rbs.Items[i].Subjects[j] + switch subj.Kind { + case "Group": + if _, ok = groups[subj.Name]; ok { + allowedNameSet[rbs.Items[i].Namespace] = struct{}{} + break subjectLoop + } + case "User": + if subj.Name == u.GetName() { + allowedNameSet[rbs.Items[i].Namespace] = struct{}{} + break subjectLoop + } + case "ServiceAccount": + if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", subj.Namespace, subj.Name) { + allowedNameSet[rbs.Items[i].Namespace] = struct{}{} + break subjectLoop + } + } } } + allowed := make([]string, 0, len(allowedNameSet)) + for name := range allowedNameSet { + allowed = append(allowed, 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 // ----------------------------------------------------------------------------- diff --git a/pkg/registry/core/tenantsecret/rest.go b/pkg/registry/core/tenantsecret/rest.go index ad477527..b1667c45 100644 --- a/pkg/registry/core/tenantsecret/rest.go +++ b/pkg/registry/core/tenantsecret/rest.go @@ -9,7 +9,7 @@ import ( "encoding/base64" "fmt" "net/http" - "sort" + "slices" "time" corev1 "k8s.io/api/core/v1" @@ -25,7 +25,7 @@ import ( "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" + "sigs.k8s.io/controller-runtime/pkg/client" corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" ) @@ -157,13 +157,15 @@ var ( ) type REST struct { - core corev1client.CoreV1Interface - gvr schema.GroupVersionResource + c client.Client + w client.WithWatch + gvr schema.GroupVersionResource } -func NewREST(coreCli corev1client.CoreV1Interface) *REST { +func NewREST(c client.Client, w client.WithWatch) *REST { return &REST{ - core: coreCli, + c: c, + w: w, gvr: schema.GroupVersionResource{ Group: corev1alpha1.GroupName, Version: "v1alpha1", @@ -203,11 +205,11 @@ func (r *REST) Create( } sec := tenantToSecret(in, nil) - out, err := r.core.Secrets(sec.Namespace).Create(ctx, sec, *opts) + err := r.c.Create(ctx, sec, &client.CreateOptions{Raw: opts}) if err != nil { return nil, err } - return secretToTenant(out), nil + return secretToTenant(sec), nil } func (r *REST) Get( @@ -219,10 +221,14 @@ func (r *REST) Get( if err != nil { return nil, err } - sec, err := r.core.Secrets(ns).Get(ctx, name, *opts) + sec := &corev1.Secret{} + err = r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, sec, &client.GetOptions{Raw: opts}) if err != nil { return nil, err } + if sec.Labels == nil || sec.Labels[tsLabelKey] != tsLabelValue { + return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) + } return secretToTenant(sec), nil } @@ -247,10 +253,16 @@ func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtim fieldSel = opts.FieldSelector.String() } - list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{ - LabelSelector: ls.String(), - FieldSelector: fieldSel, - }) + list := &corev1.SecretList{} + err = r.c.List(ctx, list, + &client.ListOptions{ + Namespace: ns, + LabelSelector: ls, + Raw: &metav1.ListOptions{ + LabelSelector: ls.String(), + FieldSelector: fieldSel, + }, + }) if err != nil { return nil, err } @@ -266,7 +278,17 @@ func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtim 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 }) + slices.SortFunc(out.Items, func(a, b corev1alpha1.TenantSecret) int { + aKey := fmt.Sprintf("%s/%s", a.Namespace, a.Name) + bKey := fmt.Sprintf("%s/%s", b.Namespace, b.Name) + switch { + case aKey < bKey: + return -1 + case aKey > bKey: + return 1 + } + return 0 + }) return out, nil } @@ -284,9 +306,17 @@ func (r *REST) Update( 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 + var cur *corev1.Secret + previous := &corev1.Secret{} + if err := r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, previous, &client.GetOptions{Raw: &metav1.GetOptions{}}); err != nil { + if !apierrors.IsNotFound(err) { + return nil, false, err + } + } else { + if previous.Labels == nil || previous.Labels[tsLabelKey] != tsLabelValue { + return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) + } + cur = previous } newObj, err := objInfo.UpdatedObject(ctx, nil) @@ -296,17 +326,18 @@ func (r *REST) Update( in := newObj.(*corev1alpha1.TenantSecret) newSec := tenantToSecret(in, cur) + newSec.Namespace = ns if cur == nil { - if !forceCreate && err == nil { + if !forceCreate { 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 + err := r.c.Create(ctx, newSec, &client.CreateOptions{Raw: &metav1.CreateOptions{}}) + return secretToTenant(newSec), true, err } newSec.ResourceVersion = cur.ResourceVersion - out, err := r.core.Secrets(ns).Update(ctx, newSec, *opts) - return secretToTenant(out), false, err + err = r.c.Update(ctx, newSec, &client.UpdateOptions{Raw: opts}) + return secretToTenant(newSec), false, err } func (r *REST) Delete( @@ -319,7 +350,14 @@ func (r *REST) Delete( if err != nil { return nil, false, err } - err = r.core.Secrets(ns).Delete(ctx, name, *opts) + current := &corev1.Secret{} + if err := r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, current, &client.GetOptions{Raw: &metav1.GetOptions{}}); err != nil { + return nil, false, err + } + if current.Labels == nil || current.Labels[tsLabelKey] != tsLabelValue { + return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) + } + err = r.c.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}}, &client.DeleteOptions{Raw: opts}) return nil, err == nil, err } @@ -331,21 +369,40 @@ func (r *REST) Patch( opts *metav1.PatchOptions, subresources ...string, ) (runtime.Object, error) { + if len(subresources) > 0 { + return nil, fmt.Errorf("TenantSecret does not have subresources") + } ns, err := nsFrom(ctx) if err != nil { return nil, err } - - out, err := r.core.Secrets(ns). - Patch(ctx, name, pt, data, *opts, subresources...) + current := &corev1.Secret{} + if err := r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, current, &client.GetOptions{Raw: &metav1.GetOptions{}}); err != nil { + return nil, err + } + if current.Labels == nil || current.Labels[tsLabelKey] != tsLabelValue { + return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) + } + out := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + } + patch := client.RawPatch(pt, data) + err = r.c.Patch(ctx, out, patch, &client.PatchOptions{Raw: opts}) if err != nil { return nil, err } // Ensure tenant secret label is preserved + if out.Labels == nil { + out.Labels = make(map[string]string) + } + if out.Labels[tsLabelKey] != tsLabelValue { out.Labels[tsLabelKey] = tsLabelValue - out, _ = r.core.Secrets(ns).Update(ctx, out, metav1.UpdateOptions{}) + _ = r.c.Update(ctx, out, &client.UpdateOptions{Raw: &metav1.UpdateOptions{}}) } return secretToTenant(out), nil @@ -361,11 +418,16 @@ func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch return nil, err } - ls := labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String() - base, err := r.core.Secrets(ns).Watch(ctx, metav1.ListOptions{ - Watch: true, - LabelSelector: ls, - ResourceVersion: opts.ResourceVersion, + secList := &corev1.SecretList{} + ls := labels.Set{tsLabelKey: tsLabelValue}.AsSelector() + base, err := r.w.Watch(ctx, secList, &client.ListOptions{ + Namespace: ns, + LabelSelector: ls, + Raw: &metav1.ListOptions{ + Watch: true, + LabelSelector: ls.String(), + ResourceVersion: opts.ResourceVersion, + }, }) if err != nil { return nil, err diff --git a/pkg/registry/core/tenantsecretstable/rest.go b/pkg/registry/core/tenantsecretstable/rest.go deleted file mode 100644 index 841bfa32..00000000 --- a/pkg/registry/core/tenantsecretstable/rest.go +++ /dev/null @@ -1,317 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// TenantSecretsTable registry – namespaced, read-only flattened view over -// Secrets labelled "internal.cozystack.io/tenantresource=true". Each data key is a separate object. - -package tenantsecretstable - -import ( - "context" - "encoding/base64" - "fmt" - "net/http" - "sort" - "time" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/selection" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/apiserver/pkg/registry/rest" - corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - - corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" -) - -const ( - tsLabelKey = corev1alpha1.TenantResourceLabelKey - tsLabelValue = corev1alpha1.TenantResourceLabelValue - kindObj = "TenantSecretsTable" - kindObjList = "TenantSecretsTableList" - singularName = "tenantsecretstable" - resourcePlural = "tenantsecretstables" -) - -type REST struct { - core corev1client.CoreV1Interface - gvr schema.GroupVersionResource -} - -func NewREST(coreCli corev1client.CoreV1Interface) *REST { - return &REST{ - core: coreCli, - gvr: schema.GroupVersionResource{ - Group: corev1alpha1.GroupName, - Version: "v1alpha1", - Resource: resourcePlural, - }, - } -} - -var ( - _ rest.Getter = &REST{} - _ rest.Lister = &REST{} - _ rest.Watcher = &REST{} - _ rest.TableConvertor = &REST{} - _ rest.Scoper = &REST{} - _ rest.SingularNameProvider = &REST{} - _ rest.Storage = &REST{} -) - -func (*REST) NamespaceScoped() bool { return true } -func (*REST) New() runtime.Object { return &corev1alpha1.TenantSecretsTable{} } -func (*REST) NewList() runtime.Object { - return &corev1alpha1.TenantSecretsTableList{} -} -func (*REST) Kind() string { return kindObj } -func (r *REST) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind { - return r.gvr.GroupVersion().WithKind(kindObj) -} -func (*REST) GetSingularName() string { return singularName } -func (*REST) Destroy() {} - -func nsFrom(ctx context.Context) (string, error) { - ns, ok := request.NamespaceFrom(ctx) - if !ok { - return "", fmt.Errorf("namespace required") - } - return ns, nil -} - -// ----------------------- -// Get/List -// ----------------------- - -func (r *REST) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) { - ns, err := nsFrom(ctx) - if err != nil { - return nil, err - } - - // We need to identify secret name and key. Iterate secrets in namespace with tenant secret label - // and return the matching composed object. - list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{LabelSelector: labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String()}) - if err != nil { - return nil, err - } - for i := range list.Items { - sec := &list.Items[i] - for k, v := range sec.Data { - composed := composedName(sec.Name, k) - if composed == name { - return secretKeyToObj(sec, k, v), nil - } - } - } - return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) -} - -func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtime.Object, error) { - ns, err := nsFrom(ctx) - if err != nil { - return nil, err - } - - sel := labels.NewSelector() - req, _ := labels.NewRequirement(tsLabelKey, selection.Equals, []string{tsLabelValue}) - sel = sel.Add(*req) - if opts.LabelSelector != nil { - if reqs, _ := opts.LabelSelector.Requirements(); len(reqs) > 0 { - sel = sel.Add(reqs...) - } - } - fieldSel := "" - if opts.FieldSelector != nil { - fieldSel = opts.FieldSelector.String() - } - - list, err := r.core.Secrets(ns).List(ctx, metav1.ListOptions{LabelSelector: sel.String(), FieldSelector: fieldSel}) - if err != nil { - return nil, err - } - - out := &corev1alpha1.TenantSecretsTableList{ - TypeMeta: metav1.TypeMeta{APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: kindObjList}, - ListMeta: list.ListMeta, - } - - for i := range list.Items { - sec := &list.Items[i] - // Ensure stable ordering of keys - keys := make([]string, 0, len(sec.Data)) - for k := range sec.Data { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := sec.Data[k] - o := secretKeyToObj(sec, k, v) - out.Items = append(out.Items, *o) - } - } - - sort.Slice(out.Items, func(i, j int) bool { return out.Items[i].Name < out.Items[j].Name }) - return out, nil -} - -// ----------------------- -// Watch -// ----------------------- - -func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { - ns, err := nsFrom(ctx) - if err != nil { - return nil, err - } - - ls := labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String() - base, err := r.core.Secrets(ns).Watch(ctx, metav1.ListOptions{ - Watch: true, - LabelSelector: ls, - ResourceVersion: opts.ResourceVersion, - }) - if err != nil { - return nil, err - } - - ch := make(chan watch.Event) - proxy := watch.NewProxyWatcher(ch) - - go func() { - defer proxy.Stop() - for ev := range base.ResultChan() { - sec, ok := ev.Object.(*corev1.Secret) - if !ok || sec == nil { - continue - } - // Emit an event per key - for k, v := range sec.Data { - obj := secretKeyToObj(sec, k, v) - ch <- watch.Event{Type: ev.Type, Object: obj} - } - } - }() - - return proxy, nil -} - -// ----------------------- -// TableConvertor -// ----------------------- - -func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) { - now := time.Now() - row := func(o *corev1alpha1.TenantSecretsTable) metav1.TableRow { - return metav1.TableRow{ - Cells: []interface{}{o.Name, o.Data.Name, o.Data.Key, humanAge(o.CreationTimestamp.Time, now)}, - Object: runtime.RawExtension{Object: o}, - } - } - tbl := &metav1.Table{ - TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"}, - ColumnDefinitions: []metav1.TableColumnDefinition{ - {Name: "NAME", Type: "string"}, - {Name: "SECRET", Type: "string"}, - {Name: "KEY", Type: "string"}, - {Name: "AGE", Type: "string"}, - }, - } - switch v := obj.(type) { - case *corev1alpha1.TenantSecretsTableList: - for i := range v.Items { - tbl.Rows = append(tbl.Rows, row(&v.Items[i])) - } - tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion - case *corev1alpha1.TenantSecretsTable: - tbl.Rows = append(tbl.Rows, row(v)) - tbl.ListMeta.ResourceVersion = v.ResourceVersion - default: - return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)} - } - return tbl, nil -} - -// ----------------------- -// Helpers -// ----------------------- - -func composedName(secretName, key string) string { - return secretName + "-" + key -} - -func humanAge(t time.Time, now time.Time) string { - d := now.Sub(t) - // simple human duration - if d.Hours() >= 24 { - return fmt.Sprintf("%dd", int(d.Hours()/24)) - } - if d.Hours() >= 1 { - return fmt.Sprintf("%dh", int(d.Hours())) - } - if d.Minutes() >= 1 { - return fmt.Sprintf("%dm", int(d.Minutes())) - } - return fmt.Sprintf("%ds", int(d.Seconds())) -} - -func secretKeyToObj(sec *corev1.Secret, key string, val []byte) *corev1alpha1.TenantSecretsTable { - return &corev1alpha1.TenantSecretsTable{ - TypeMeta: metav1.TypeMeta{APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: kindObj}, - ObjectMeta: metav1.ObjectMeta{ - Name: sec.Name, - Namespace: sec.Namespace, - UID: sec.UID, - ResourceVersion: sec.ResourceVersion, - CreationTimestamp: sec.CreationTimestamp, - Labels: filterUserLabels(sec.Labels), - Annotations: sec.Annotations, - }, - Data: corev1alpha1.TenantSecretEntry{ - Name: sec.Name, - Key: key, - Value: toBase64String(val), - }, - } -} - -func filterUserLabels(m map[string]string) map[string]string { - if m == nil { - return nil - } - out := make(map[string]string, len(m)) - for k, v := range m { - if k == tsLabelKey { - continue - } - out[k] = v - } - return out -} - -func toBase64String(b []byte) string { - const enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - // Minimal base64 encoder to avoid extra deps; for readability we could use stdlib encoding/base64 - // but keeping inline is fine; however using stdlib is clearer. - // Using stdlib: - return base64.StdEncoding.EncodeToString(b) -} - -type notAcceptable struct { - resource schema.GroupResource - message string -} - -func (e notAcceptable) Error() string { return e.message } -func (e notAcceptable) Status() metav1.Status { - return metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusNotAcceptable, - Reason: metav1.StatusReason("NotAcceptable"), - Message: e.message, - } -} diff --git a/scripts/migrations/20 b/scripts/migrations/20 index 77abec8e..2b2e1530 100755 --- a/scripts/migrations/20 +++ b/scripts/migrations/20 @@ -25,11 +25,31 @@ sleep 5 cozypkg -n cozy-system -C packages/system/cozystack-resource-definition-crd apply cozystack-resource-definition-crd --plain cozypkg -n cozy-system -C packages/system/cozystack-resource-definitions apply cozystack-resource-definitions --plain cozypkg -n cozy-system -C packages/system/cozystack-api apply cozystack-api --plain +if kubectl get ds cozystack-api -n cozy-system >/dev/null 2>&1; then + echo "Waiting for cozystack-api daemonset" + kubectl rollout status ds/cozystack-api -n cozy-system --timeout=5m || exit 1 +else + echo "Waiting for cozystack-api deployment" + kubectl rollout status deploy/cozystack-api -n cozy-system --timeout=5m || exit 1 +fi + helm upgrade --install -n cozy-system cozystack-controller ./packages/system/cozystack-controller/ --take-ownership +echo "Waiting for cozystack-controller" +kubectl rollout status deploy/cozystack-controller -n cozy-system --timeout=5m || exit 1 + +helm upgrade --install -n cozy-system lineage-controller-webhook ./packages/system/lineage-controller-webhook/ --take-ownership +echo "Waiting for lineage-webhook" +kubectl rollout status ds/lineage-controller-webhook -n cozy-system --timeout=5m || exit 1 sleep 5 -kubectl wait deployment/cozystack-api -n cozy-system --timeout=4m --for=condition=available || exit 1 -kubectl wait deployment/cozystack-controller -n cozy-system --timeout=4m --for=condition=available || exit 1 +echo "Running lineage-webhook test" +kubectl delete ns cozy-lineage-webhook-test --ignore-not-found && kubectl create ns cozy-lineage-webhook-test +cleanup_test_ns() { + kubectl delete ns cozy-lineage-webhook-test --ignore-not-found +} +trap cleanup_test_ns ERR +timeout 60 sh -c 'until kubectl -n cozy-lineage-webhook-test create service clusterip lineage-webhook-test --clusterip="None" --dry-run=server; do sleep 1; done' +cleanup_test_ns timestamp=$(date --rfc-3339=ns || date) kubectl get namespace -o custom-columns=NAME:.metadata.name --no-headers | @@ -41,6 +61,7 @@ kubectl get namespace -o custom-columns=NAME:.metadata.name --no-headers | -n "$namespace" --all \ migration.cozystack.io="$timestamp" --overwrite || true) done + # Stamp version kubectl create configmap -n cozy-system cozystack-version \ --from-literal=version=21 --dry-run=client -o yaml | kubectl apply -f-