diff --git a/.gitignore b/.gitignore index 00d89825..de476be5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ _out .git .idea +.vscode # User-specific stuff .idea/**/workspace.xml @@ -75,4 +76,4 @@ fabric.properties .idea/caches/build_file_checksums.ser .DS_Store -**/.DS_Store \ No newline at end of file +**/.DS_Store diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index f1c10c10..a5624cd3 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -39,6 +39,8 @@ import ( cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" "github.com/cozystack/cozystack/internal/controller" "github.com/cozystack/cozystack/internal/telemetry" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" // +kubebuilder:scaffold:imports ) @@ -51,6 +53,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(cozystackiov1alpha1.AddToScheme(scheme)) + utilruntime.Must(helmv2.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -182,6 +185,14 @@ func main() { if err = (&controller.WorkloadReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "WorkloadReconciler") + os.Exit(1) + } + + if err = (&controller.TenantHelmReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Workload") os.Exit(1) diff --git a/internal/controller/tenant_helm_reconciler.go b/internal/controller/tenant_helm_reconciler.go new file mode 100644 index 00000000..402ff5ba --- /dev/null +++ b/internal/controller/tenant_helm_reconciler.go @@ -0,0 +1,158 @@ +package controller + +import ( + "context" + "fmt" + "strings" + "time" + + e "errors" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type TenantHelmReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *TenantHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + hr := &helmv2.HelmRelease{} + if err := r.Get(ctx, req.NamespacedName, hr); err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + logger.Error(err, "unable to fetch HelmRelease") + return ctrl.Result{}, err + } + + if !strings.HasPrefix(hr.Name, "tenant-") { + return ctrl.Result{}, nil + } + + if len(hr.Status.Conditions) == 0 || hr.Status.Conditions[0].Type != "Ready" { + return ctrl.Result{}, nil + } + + if len(hr.Status.History) == 0 { + logger.Info("no history in HelmRelease status", "name", hr.Name) + return ctrl.Result{}, nil + } + + if hr.Status.History[0].Status != "deployed" { + return ctrl.Result{}, nil + } + + newDigest := hr.Status.History[0].Digest + var hrList helmv2.HelmReleaseList + childNamespace := getChildNamespace(hr.Namespace, hr.Name) + if childNamespace == "tenant-root" && hr.Name == "tenant-root" { + if hr.Spec.Values == nil { + logger.Error(e.New("hr.Spec.Values is nil"), "cant annotate tenant-root ns") + return ctrl.Result{}, nil + } + err := annotateTenantRootNs(*hr.Spec.Values, r.Client) + if err != nil { + logger.Error(err, "cant annotate tenant-root ns") + return ctrl.Result{}, nil + } + logger.Info("namespace 'tenant-root' annotated") + } + + if err := r.List(ctx, &hrList, client.InNamespace(childNamespace)); err != nil { + logger.Error(err, "unable to list HelmReleases in namespace", "namespace", hr.Name) + return ctrl.Result{}, err + } + + for _, item := range hrList.Items { + if item.Name == hr.Name { + continue + } + oldDigest := item.GetAnnotations()["cozystack.io/tenant-config-digest"] + if oldDigest == newDigest { + continue + } + patchTarget := item.DeepCopy() + + if patchTarget.Annotations == nil { + patchTarget.Annotations = map[string]string{} + } + ts := time.Now().Format(time.RFC3339Nano) + + patchTarget.Annotations["cozystack.io/tenant-config-digest"] = newDigest + patchTarget.Annotations["reconcile.fluxcd.io/forceAt"] = ts + patchTarget.Annotations["reconcile.fluxcd.io/requestedAt"] = ts + + patch := client.MergeFrom(item.DeepCopy()) + if err := r.Patch(ctx, patchTarget, patch); err != nil { + logger.Error(err, "failed to patch HelmRelease", "name", patchTarget.Name) + continue + } + + logger.Info("patched HelmRelease with new digest", "name", patchTarget.Name, "digest", newDigest, "version", hr.Status.History[0].Version) + } + + return ctrl.Result{}, nil +} + +func (r *TenantHelmReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&helmv2.HelmRelease{}). + Complete(r) +} + +func getChildNamespace(currentNamespace, hrName string) string { + tenantName := strings.TrimPrefix(hrName, "tenant-") + + switch { + case currentNamespace == "tenant-root" && hrName == "tenant-root": + // 1) root tenant inside root namespace + return "tenant-root" + + case currentNamespace == "tenant-root": + // 2) any other tenant in root namespace + return fmt.Sprintf("tenant-%s", tenantName) + + default: + // 3) tenant in a dedicated namespace + return fmt.Sprintf("%s-%s", currentNamespace, tenantName) + } +} + +func annotateTenantRootNs(values apiextensionsv1.JSON, c client.Client) error { + var data map[string]interface{} + if err := yaml.Unmarshal(values.Raw, &data); err != nil { + return fmt.Errorf("failed to parse HelmRelease values: %w", err) + } + + host, ok := data["host"].(string) + if !ok || host == "" { + return fmt.Errorf("host field not found or not a string") + } + + var ns corev1.Namespace + if err := c.Get(context.TODO(), client.ObjectKey{Name: "tenant-root"}, &ns); err != nil { + return fmt.Errorf("failed to get namespace tenant-root: %w", err) + } + + if ns.Annotations == nil { + ns.Annotations = map[string]string{} + } + ns.Annotations["namespace.cozystack.io/host"] = host + + if err := c.Update(context.TODO(), &ns); err != nil { + return fmt.Errorf("failed to update namespace: %w", err) + } + + return nil +} diff --git a/packages/core/platform/templates/apps.yaml b/packages/core/platform/templates/apps.yaml index f1872870..a24744b8 100644 --- a/packages/core/platform/templates/apps.yaml +++ b/packages/core/platform/templates/apps.yaml @@ -8,7 +8,7 @@ {{- $host = index $cozyConfig.data "root-host" }} {{- end }} {{- end }} -{{- $tenantRoot := list }} +{{- $tenantRoot := dict }} {{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }} {{- $tenantRoot = lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }} {{- end }} @@ -37,7 +37,7 @@ metadata: labels: cozystack.io/ui: "true" spec: - interval: 1m + interval: 0s releaseName: tenant-root install: remediation: diff --git a/packages/extra/info/Chart.yaml b/packages/extra/info/Chart.yaml index 2865c6df..d0a06e9f 100644 --- a/packages/extra/info/Chart.yaml +++ b/packages/extra/info/Chart.yaml @@ -3,4 +3,4 @@ name: info description: Info icon: /logos/info.svg type: application -version: 1.0.0 +version: 1.0.1 diff --git a/packages/extra/info/templates/kubeconfig.yaml b/packages/extra/info/templates/kubeconfig.yaml index ff8127d2..d960a587 100644 --- a/packages/extra/info/templates/kubeconfig.yaml +++ b/packages/extra/info/templates/kubeconfig.yaml @@ -11,6 +11,13 @@ {{- $k8sClient := index $k8sClientSecret.data "client-secret-key" | b64dec }} {{- $rootSaConfigMap := lookup "v1" "ConfigMap" "kube-system" "kube-root-ca.crt" }} {{- $k8sCa := index $rootSaConfigMap.data "ca.crt" | b64enc }} + +{{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }} +{{- $tenantRoot := lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }} +{{- if and $tenantRoot $tenantRoot.spec $tenantRoot.spec.values $tenantRoot.spec.values.host }} +{{- $host = $tenantRoot.spec.values.host }} +{{- end }} +{{- end }} --- apiVersion: v1 kind: Secret diff --git a/packages/extra/ingress/Chart.yaml b/packages/extra/ingress/Chart.yaml index 25d86de0..e6001cc8 100644 --- a/packages/extra/ingress/Chart.yaml +++ b/packages/extra/ingress/Chart.yaml @@ -3,4 +3,4 @@ name: ingress description: NGINX Ingress Controller icon: /logos/ingress-nginx.svg type: application -version: 1.5.0 +version: 1.5.1 diff --git a/packages/extra/ingress/templates/dashboard.yaml b/packages/extra/ingress/templates/dashboard.yaml index 830e7a0a..28b6722c 100644 --- a/packages/extra/ingress/templates/dashboard.yaml +++ b/packages/extra/ingress/templates/dashboard.yaml @@ -4,6 +4,15 @@ {{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }} {{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }} +{{- $tenantRoot := dict }} +{{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }} +{{- $tenantRoot = lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }} +{{- end }} +{{- if and $tenantRoot $tenantRoot.spec $tenantRoot.spec.values $tenantRoot.spec.values.host }} +{{- $host = $tenantRoot.spec.values.host }} +{{- else }} +{{- end }} + {{- if .Values.dashboard }} apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/packages/extra/versions_map b/packages/extra/versions_map index 289b0cfe..a76418a5 100644 --- a/packages/extra/versions_map +++ b/packages/extra/versions_map @@ -11,13 +11,15 @@ etcd 2.5.0 24fa7222 etcd 2.6.0 8c460528 etcd 2.6.1 45a7416c etcd 2.7.0 HEAD -info 1.0.0 HEAD +info 1.0.0 93bdf411 +info 1.0.1 HEAD ingress 1.0.0 d7cfa53c ingress 1.1.0 5bbc488e ingress 1.2.0 28fca4ef ingress 1.3.0 fde4bcfa ingress 1.4.0 fd240701 -ingress 1.5.0 HEAD +ingress 1.5.0 93bdf411 +ingress 1.5.1 HEAD monitoring 1.0.0 d7cfa53c monitoring 1.1.0 25221fdc monitoring 1.2.0 f81be075 diff --git a/packages/system/cozystack-controller/templates/rbac.yaml b/packages/system/cozystack-controller/templates/rbac.yaml index af3dae33..64f1eaa1 100644 --- a/packages/system/cozystack-controller/templates/rbac.yaml +++ b/packages/system/cozystack-controller/templates/rbac.yaml @@ -9,3 +9,6 @@ rules: - apiGroups: ['cozystack.io'] resources: ['*'] verbs: ['*'] +- apiGroups: ["helm.toolkit.fluxcd.io"] + resources: ["helmreleases"] + verbs: ["get", "list", "watch", "patch", "update"] diff --git a/packages/system/keycloak-configure/templates/configure-kk.yaml b/packages/system/keycloak-configure/templates/configure-kk.yaml index adee11b6..b2d8db5b 100644 --- a/packages/system/keycloak-configure/templates/configure-kk.yaml +++ b/packages/system/keycloak-configure/templates/configure-kk.yaml @@ -4,6 +4,15 @@ {{- $rootSaConfigMap := lookup "v1" "ConfigMap" "kube-system" "kube-root-ca.crt" }} {{- $k8sCa := index $rootSaConfigMap.data "ca.crt" | b64enc }} +{{- $tenantRoot := dict }} +{{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }} +{{- $tenantRoot = lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }} +{{- end }} +{{- if and $tenantRoot $tenantRoot.spec $tenantRoot.spec.values $tenantRoot.spec.values.host }} +{{- $host = $tenantRoot.spec.values.host }} +{{- else }} +{{- end }} + {{- $existingK8sSecret := lookup "v1" "Secret" .Release.Namespace "k8s-client" }} {{- $existingKubeappsSecret := lookup "v1" "Secret" .Release.Namespace "kubeapps-client" }} {{- $existingAuthConfig := lookup "v1" "Secret" "cozy-dashboard" "kubeapps-auth-config" }} diff --git a/packages/system/keycloak/templates/ingress.yaml b/packages/system/keycloak/templates/ingress.yaml index 7618a9f5..6ae1384a 100644 --- a/packages/system/keycloak/templates/ingress.yaml +++ b/packages/system/keycloak/templates/ingress.yaml @@ -5,6 +5,15 @@ {{- $rootns := lookup "v1" "Namespace" "" "tenant-root" }} {{- $ingress := index $rootns.metadata.annotations "namespace.cozystack.io/ingress" }} +{{- $tenantRoot := dict }} +{{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }} +{{- $tenantRoot = lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }} +{{- end }} +{{- if and $tenantRoot $tenantRoot.spec $tenantRoot.spec.values $tenantRoot.spec.values.host }} +{{- $host = $tenantRoot.spec.values.host }} +{{- else }} +{{- end }} + apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/packages/system/keycloak/templates/sts.yaml b/packages/system/keycloak/templates/sts.yaml index e625859b..cecb17a1 100644 --- a/packages/system/keycloak/templates/sts.yaml +++ b/packages/system/keycloak/templates/sts.yaml @@ -7,6 +7,15 @@ {{- $password = index $existingPassword.data "password" | b64dec }} {{- end }} +{{- $tenantRoot := dict }} +{{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }} +{{- $tenantRoot = lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }} +{{- end }} +{{- if and $tenantRoot $tenantRoot.spec $tenantRoot.spec.values $tenantRoot.spec.values.host }} +{{- $host = $tenantRoot.spec.values.host }} +{{- else }} +{{- end }} + apiVersion: v1 kind: Secret metadata: