diff --git a/.github/workflows/test_chart.yaml b/.github/workflows/test_chart.yaml index e7da2366..461259a1 100644 --- a/.github/workflows/test_chart.yaml +++ b/.github/workflows/test_chart.yaml @@ -53,6 +53,9 @@ jobs: - name: Add CAPI operator chart repo run: helm repo add capi-operator https://kubernetes-sigs.github.io/cluster-api-operator + - name: Add CAPI UI chart repo + run: helm repo add capi https://rancher.github.io/capi-ui-extension + - name: Package operator chart run: make release-chart diff --git a/charts/rancher-turtles/Chart.yaml b/charts/rancher-turtles/Chart.yaml index f883582d..31036813 100644 --- a/charts/rancher-turtles/Chart.yaml +++ b/charts/rancher-turtles/Chart.yaml @@ -16,6 +16,10 @@ dependencies: version: v0.16.0 repository: https://kubernetes-sigs.github.io/cluster-api-operator condition: cluster-api-operator.enabled + - name: capi + version: v0.8.2 + repository: https://rancher.github.io/capi-ui-extension + condition: capi.enabled annotations: catalog.cattle.io/certified: rancher catalog.cattle.io/display-name: Rancher Turtles - the Cluster API Extension diff --git a/charts/rancher-turtles/questions.yml b/charts/rancher-turtles/questions.yml index 6eaf8de7..ae423eef 100644 --- a/charts/rancher-turtles/questions.yml +++ b/charts/rancher-turtles/questions.yml @@ -13,6 +13,11 @@ questions: type: boolean description: "Flag to enable or disable installation of cert-manager. If set to false then you will need to install cert-manager manually." label: "Enable Cert Manager" + - variable: capi.enabled + default: true + type: boolean + description: "Flag to enable or disable installation of CAPI UI extension. If set to false then you will need to install CAPI UI extension manually." + label: "Install CAPI UI" - variable: rancherTurtles.cluster-api-operator.cleanup default: true description: "Specify that the CAPI Operator post-delete cleanup job will be performed." diff --git a/charts/rancher-turtles/templates/rancher-turtles-components.yaml b/charts/rancher-turtles/templates/rancher-turtles-components.yaml index ade93dd3..da83aa08 100644 --- a/charts/rancher-turtles/templates/rancher-turtles-components.yaml +++ b/charts/rancher-turtles/templates/rancher-turtles-components.yaml @@ -3385,6 +3385,17 @@ rules: - patch - update - watch +- apiGroups: + - catalog.cattle.io + resources: + - uiplugins + verbs: + - create + - delete + - get + - list + - patch + - watch - apiGroups: - cluster.x-k8s.io resources: @@ -3450,6 +3461,15 @@ rules: - get - list - watch +- apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - rancher-turtles-manager-role + resources: + - clusterroles + verbs: + - get + - list - apiGroups: - turtles-capi.cattle.io resources: diff --git a/charts/rancher-turtles/values.yaml b/charts/rancher-turtles/values.yaml index f42c5e83..ce35f589 100644 --- a/charts/rancher-turtles/values.yaml +++ b/charts/rancher-turtles/values.yaml @@ -1,3 +1,6 @@ +capi: + enabled: true + fullnameOverride: capi rancherTurtles: image: controller imageVersion: v0.0.0 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a9b1760b..bfab2d04 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -19,6 +19,17 @@ rules: - patch - update - watch +- apiGroups: + - catalog.cattle.io + resources: + - uiplugins + verbs: + - create + - delete + - get + - list + - patch + - watch - apiGroups: - cluster.x-k8s.io resources: @@ -84,6 +95,15 @@ rules: - get - list - watch +- apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - rancher-turtles-manager-role + resources: + - clusterroles + verbs: + - get + - list - apiGroups: - turtles-capi.cattle.io resources: diff --git a/docs/adr/0014-turtles-ui-installation.md b/docs/adr/0014-turtles-ui-installation.md new file mode 100644 index 00000000..3a45c632 --- /dev/null +++ b/docs/adr/0014-turtles-ui-installation.md @@ -0,0 +1,47 @@ + + + +- [14. Turtles UI installation](#title) + - [Context](#context) + - [Decision](#decision) + - [Consequences](#consequences) + + + +# Turtles UI installation + +- Status: proposed +- Date: 2025-02-17 +- Authors: @Danil-Grigorev +- Deciders: @alexander-demicev @furkatgofurov7 @salasberryfin @anmazzotti @mjura @yiannistri + +## Context + +Turtles UI [extension][] provides UI functionality for the Turtles backend. Current installation procedure involves set of mandatory steps described in documentation, which involves: +- [Installing rancher turtles][turtles-install] chart via dashboard +- [Installing UI][ui-install] extension via dashboard + +This process is more complicated then a combined and automated installation, and may also lead to issues like: +- Missed UI extension installation step +- Installation of incompatible version of UI extension and Rancher Turles (involving CAPI version) +- Invalid combination of Turtles and UI versions in case of Turtles chart upgrade + +[extension]: https://github.com/rancher/capi-ui-extension +[turtles-install]: https://turtles.docs.rancher.com/turtles/stable/en/getting-started/install-rancher-turtles/using_rancher_dashboard.html#_installation +[ui-install]: https://turtles.docs.rancher.com/turtles/stable/en/getting-started/install-rancher-turtles/using_rancher_dashboard.html#_capi_ui_extension_installation + +## Decision + +The proposed solution is to install UI extension chart as a `Helm` dependency for the Turtles `Helm` chart. This will leverage `questions.yaml` [integration][] to allow users to configure extension settings or disable UI chart installation. + +UI extensions use `cattle-ui-plugin-system` namespace. Turtles will leverage a new controller, which will be able to asyncroniously move the `UIPlugin` resource to the namespace, once it is available and Rancher chart is installed. + +Turtles chart will manage the lifecycle of UI extension by setting ownership references on the `UIPlugin` resource, to ensure automatic deletion on chart removal. + +[integration]: https://ranchermanager.docs.rancher.com/how-to-guides/new-user-guides/helm-charts-in-rancher/create-apps#questionsyml + +## Consequences + +- UI extension will be managed by Turtles chart +- Existing UI extension installation will be adopted by Turtles chart upgrade +- UI extension version will be seamlessly updated with Turtles chart upgrade diff --git a/internal/controllers/uiplugin_controller.go b/internal/controllers/uiplugin_controller.go new file mode 100644 index 00000000..9993405d --- /dev/null +++ b/internal/controllers/uiplugin_controller.go @@ -0,0 +1,125 @@ +/* +Copyright © 2023 - 2024 SUSE LLC + +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 controllers + +import ( + "context" + "fmt" + "os" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// UIPluginReconciler reconciles a UIPlugin object. +type UIPluginReconciler struct { + client.Client + *runtime.Scheme + UncachedClient client.Client +} + +// SetupWithManager sets up the controller with the Manager. +func (r *UIPluginReconciler) SetupWithManager(_ context.Context, mgr ctrl.Manager, _ controller.Options) error { + uiPlugin := &metav1.PartialObjectMetadata{} + uiPlugin.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "catalog.cattle.io", + Version: "v1", + Kind: "UIPlugin", + }) + + if err := ctrl.NewControllerManagedBy(mgr). + Named("ui-plugin"). + For(uiPlugin). + WithEventFilter(predicate.NewPredicateFuncs(func(plugin client.Object) bool { + return plugin.GetNamespace() == os.Getenv("POD_NAMESPACE") + })). + Complete(r); err != nil { + return fmt.Errorf("creating UIPlugin controller: %w", err) + } + + return nil +} + +//+kubebuilder:rbac:groups=catalog.cattle.io,resources=uiplugins,verbs=get;list;watch;create;patch;delete +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resourceNames=rancher-turtles-manager-role,resources=clusterroles,verbs=get;list + +// Reconcile moves the UIPlugin into cattle-ui-plugin-system namespace. +func (r *UIPluginReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + plugin := &unstructured.Unstructured{} + plugin.SetKind("UIPlugin") + plugin.SetAPIVersion("catalog.cattle.io/v1") + + if err := r.Client.Get(ctx, req.NamespacedName, plugin); err != nil { + log.Error(err, "Unable to get UIPlugin") + + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if plugin.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + + role := &rbacv1.ClusterRole{} + if err := r.UncachedClient.Get(ctx, types.NamespacedName{ + Name: "rancher-turtles-manager-role", + }, role); err != nil { + log.Error(err, "Unable to get turtles clusterRole") + + return ctrl.Result{}, err + } + + destination := &unstructured.Unstructured{} + destination.SetGroupVersionKind(plugin.GroupVersionKind()) + destination.SetName(plugin.GetName()) + destination.SetNamespace("cattle-ui-plugin-system") + destination.Object["spec"] = plugin.Object["spec"] + + if err := controllerutil.SetOwnerReference(role, destination, r.Scheme); err != nil { + log.Error(err, "Unable to set ClusterRole owner on UIPlugin") + + return ctrl.Result{}, err + } + + if err := r.Patch(ctx, destination, client.Apply, []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner("ui-plugin-controller"), + }...); err != nil { + log.Error(err, "Unable to patch UIPlugin") + + return ctrl.Result{}, err + } + + if err := r.Delete(ctx, plugin); err != nil { + log.Error(err, "Unable to cleanup source UIPlugin") + + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} diff --git a/main.go b/main.go index abafd046..4718880b 100644 --- a/main.go +++ b/main.go @@ -278,6 +278,20 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager) { setupLog.Error(err, "unable to create CAPI Provider controller") os.Exit(1) } + + setupLog.Info("enabling UI installation controller") + + if err := (&controllers.UIPluginReconciler{ + Client: mgr.GetClient(), + Scheme: scheme, + UncachedClient: uncachedClient, + }).SetupWithManager(ctx, mgr, controller.Options{ + MaxConcurrentReconciles: concurrencyNumber, + CacheSyncTimeout: maxDuration, + }); err != nil { + setupLog.Error(err, "unable to create UI Plugin controller") + os.Exit(1) + } } // setupRancherClient can either create a client for an in-cluster installation (rancher and rancher-turtles in the same cluster) diff --git a/updatecli/updatecli.d/manifest.yaml b/updatecli/updatecli.d/manifest.yaml index c700e5fe..e3839cff 100644 --- a/updatecli/updatecli.d/manifest.yaml +++ b/updatecli/updatecli.d/manifest.yaml @@ -86,6 +86,29 @@ sources: username: '{{ requiredEnv "UPDATECLI_GITHUB_ACTOR" }}' typeFilter: latest: true + capioperatorrelease: + kind: githubrelease + name: Get the latest CAPI operator release + spec: + owner: "rancher-sandbox" + repository: "cluster-api-operator" + token: '{{ requiredEnv "UPDATECLI_GITHUB_TOKEN" }}' + username: '{{ requiredEnv "UPDATECLI_GITHUB_ACTOR" }}' + typeFilter: + latest: true + capiuirelease: + kind: githubrelease + name: Get the latest CAPI UI extension release + spec: + owner: "rancher" + repository: "capi-ui-extension" + token: '{{ requiredEnv "UPDATECLI_GITHUB_TOKEN" }}' + username: '{{ requiredEnv "UPDATECLI_GITHUB_ACTOR" }}' + typeFilter: + latest: true + transformers: + - trimprefix: "capi-" + - addprefix: "v" # update config.yaml accordingly targets: @@ -179,6 +202,22 @@ targets: replacepattern: 'https://github.com/rancher-sandbox/cluster-api-addon-provider-fleet/releases/{{ source "capifleetrelease" }}/' scmid: turtles sourceid: capifleetrelease # Will be ignored as `replacepattern` is specified + bumpcapioperator: + name: bump CAPI Operator version + kind: yaml + spec: + file: "charts/rancher-turtles/Chart.yaml" + key: "$.dependencies[0].version" + scmid: turtles + sourceid: capioperatorrelease # Will be ignored as `replacepattern` is specified + bumpcapiui: + name: bump CAPI UI version + kind: yaml + spec: + file: "charts/rancher-turtles/Chart.yaml" + key: "$.dependencies[1].version" + scmid: turtles + sourceid: capiuirelease # Will be ignored as `replacepattern` is specified # create a pr with the changes actions: