diff --git a/Makefile b/Makefile index 51247bdd0..3b63cda05 100644 --- a/Makefile +++ b/Makefile @@ -365,6 +365,8 @@ FLUX_SOURCE_REPO_CRD ?= $(EXTERNAL_CRD_DIR)/source-helmrepositories-$(FLUX_SOURC FLUX_SOURCE_CHART_CRD ?= $(EXTERNAL_CRD_DIR)/source-helmchart-$(FLUX_SOURCE_VERSION).yaml FLUX_HELM_VERSION ?= $(shell go mod edit -json | jq -r '.Require[] | select(.Path == "github.com/fluxcd/helm-controller/api") | .Version') FLUX_HELM_CRD ?= $(EXTERNAL_CRD_DIR)/helm-$(FLUX_HELM_VERSION).yaml +SVELTOS_VERSION ?= $(shell go mod edit -json | jq -r '.Require[] | select(.Path == "github.com/projectsveltos/libsveltos") | .Version') +SVELTOS_CRD ?= $(EXTERNAL_CRD_DIR)/sveltos-$(SVELTOS_VERSION).yaml ## Tool Binaries KUBECTL ?= kubectl @@ -429,8 +431,12 @@ $(FLUX_SOURCE_REPO_CRD): $(EXTERNAL_CRD_DIR) rm -f $(FLUX_SOURCE_REPO_CRD) curl -s https://raw.githubusercontent.com/fluxcd/source-controller/$(FLUX_SOURCE_VERSION)/config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml > $(FLUX_SOURCE_REPO_CRD) +$(SVELTOS_CRD): $(EXTERNAL_CRD_DIR) + rm -f $(SVELTOS_CRD) + curl -s https://raw.githubusercontent.com/projectsveltos/sveltos/$(SVELTOS_VERSION)/manifest/crds/sveltos_crds.yaml > $(SVELTOS_CRD) + .PHONY: external-crd -external-crd: $(FLUX_HELM_CRD) $(FLUX_SOURCE_CHART_CRD) $(FLUX_SOURCE_REPO_CRD) +external-crd: $(FLUX_HELM_CRD) $(FLUX_SOURCE_CHART_CRD) $(FLUX_SOURCE_REPO_CRD) $(SVELTOS_CRD) .PHONY: kind kind: $(KIND) ## Download kind locally if necessary. diff --git a/api/v1alpha1/managedcluster_types.go b/api/v1alpha1/managedcluster_types.go index 4acbbf6f6..c97ca8f11 100644 --- a/api/v1alpha1/managedcluster_types.go +++ b/api/v1alpha1/managedcluster_types.go @@ -91,6 +91,9 @@ type ManagedClusterSpec struct { Priority int32 `json:"priority,omitempty"` // DryRun specifies whether the template should be applied after validation or only validated. DryRun bool `json:"dryRun,omitempty"` + + // +kubebuilder:default:=false + // StopOnConflict specifies what to do in case of a conflict. // E.g. If another object is already managing a service. // By default the remaining services will be deployed even if conflict is detected. diff --git a/api/v1alpha1/multiclusterservice_types.go b/api/v1alpha1/multiclusterservice_types.go index 74916e7ef..9e8c324c0 100644 --- a/api/v1alpha1/multiclusterservice_types.go +++ b/api/v1alpha1/multiclusterservice_types.go @@ -19,6 +19,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // MultiClusterServiceFinalizer is finalizer applied to MultiClusterService objects. + MultiClusterServiceFinalizer = "hmc.mirantis.com/multicluster-service" + // MultiClusterServiceKind is the string representation of a MultiClusterServiceKind. + MultiClusterServiceKind = "MultiClusterService" +) + // ServiceSpec represents a Service to be managed type ServiceSpec struct { // Values is the helm values to be passed to the template. @@ -57,6 +64,9 @@ type MultiClusterServiceSpec struct { // In case of conflict with another object managing the service, // the one with higher priority will get to deploy its services. Priority int32 `json:"priority,omitempty"` + + // +kubebuilder:default:=false + // StopOnConflict specifies what to do in case of a conflict. // E.g. If another object is already managing a service. // By default the remaining services will be deployed even if conflict is detected. diff --git a/config/dev/aws-managedcluster.yaml b/config/dev/aws-managedcluster.yaml index dd303141c..ca5c410af 100644 --- a/config/dev/aws-managedcluster.yaml +++ b/config/dev/aws-managedcluster.yaml @@ -19,6 +19,7 @@ spec: workersNumber: 1 installBeachHeadServices: false template: aws-standalone-cp-0-0-1 + priority: 100 services: - template: kyverno-3-2-6 name: kyverno diff --git a/config/dev/multiclusterservice.yaml b/config/dev/multiclusterservice.yaml new file mode 100644 index 000000000..28d4764f0 --- /dev/null +++ b/config/dev/multiclusterservice.yaml @@ -0,0 +1,13 @@ +apiVersion: hmc.mirantis.com/v1alpha1 +kind: MultiClusterService +metadata: + name: global-ingress +spec: + priority: 1000 + clusterSelector: + matchLabels: + app.kubernetes.io/managed-by: Helm + services: + - template: ingress-nginx-4-11-3 + name: ingress-nginx + namespace: ingress-nginx diff --git a/internal/controller/managedcluster_controller.go b/internal/controller/managedcluster_controller.go index 16af664ec..7b39e8438 100644 --- a/internal/controller/managedcluster_controller.go +++ b/internal/controller/managedcluster_controller.go @@ -38,7 +38,6 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -51,7 +50,6 @@ import ( hmc "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/helm" "github.com/Mirantis/hmc/internal/telemetry" - "github.com/Mirantis/hmc/internal/utils" ) const ( @@ -375,64 +373,13 @@ func (r *ManagedClusterReconciler) Update(ctx context.Context, l logr.Logger, ma // TODO(https://github.com/Mirantis/hmc/issues/361): Set status to ManagedCluster object at appropriate places. func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.ManagedCluster) (ctrl.Result, error) { l := log.FromContext(ctx).WithValues("ManagedClusterController", fmt.Sprintf("%s/%s", mc.Namespace, mc.Name)) - opts := []sveltos.HelmChartOpts{} - - // NOTE: The Profile object will be updated with no helm - // charts if len(mc.Spec.Services) == 0. This will result in the - // helm charts being uninstalled on matching clusters if - // Profile originally had len(m.Spec.Sevices) > 0. - for _, svc := range mc.Spec.Services { - if svc.Disable { - l.Info(fmt.Sprintf("Skip adding Template (%s) to Profile (%s) because Disable=true", svc.Template, mc.Name)) - continue - } - - tmpl := &hmc.ServiceTemplate{} - tmplRef := types.NamespacedName{Name: svc.Template, Namespace: mc.Namespace} - if err := r.Get(ctx, tmplRef, tmpl); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get Template (%s): %w", tmplRef.String(), err) - } - source, err := r.getServiceTemplateSource(ctx, tmpl) - if err != nil { - return ctrl.Result{}, fmt.Errorf("could not get repository url: %w", err) - } - - opts = append(opts, sveltos.HelmChartOpts{ - Values: svc.Values, - RepositoryURL: source.Spec.URL, - // We don't have repository name so chart name becomes repository name. - RepositoryName: tmpl.Spec.Helm.ChartName, - ChartName: func() string { - if source.Spec.Type == utils.RegistryTypeOCI { - return tmpl.Spec.Helm.ChartName - } - // Sveltos accepts ChartName in / format for non-OCI. - // We don't have a repository name, so we can use / instead. - // See: https://projectsveltos.github.io/sveltos/addons/helm_charts/. - return fmt.Sprintf("%s/%s", tmpl.Spec.Helm.ChartName, tmpl.Spec.Helm.ChartName) - }(), - ChartVersion: tmpl.Spec.Helm.ChartVersion, - ReleaseName: svc.Name, - ReleaseNamespace: func() string { - if svc.Namespace != "" { - return svc.Namespace - } - return svc.Name - }(), - // The reason it is passed to PlainHTTP instead of InsecureSkipTLSVerify is because - // the source.Spec.Insecure field is meant to be used for connecting to repositories - // over plain HTTP, which is different than what InsecureSkipTLSVerify is meant for. - // See: https://github.com/fluxcd/source-controller/pull/1288 - PlainHTTP: source.Spec.Insecure, - }) + opts, err := HelmChartOpts(ctx, r.Client, l, mc.Namespace, mc.Spec.Services) + if err != nil { + return ctrl.Result{}, err } if _, err := sveltos.ReconcileProfile(ctx, r.Client, l, mc.Namespace, mc.Name, - map[string]string{ - hmc.FluxHelmChartNamespaceKey: mc.Namespace, - hmc.FluxHelmChartNameKey: mc.Name, - }, sveltos.ReconcileProfileOpts{ OwnerReference: &metav1.OwnerReference{ APIVersion: hmc.GroupVersion.String(), @@ -440,6 +387,12 @@ func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.M Name: mc.Name, UID: mc.UID, }, + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + hmc.FluxHelmChartNamespaceKey: mc.Namespace, + hmc.FluxHelmChartNameKey: mc.Name, + }, + }, HelmChartOpts: opts, Priority: mc.Spec.Priority, StopOnConflict: mc.Spec.StopOnConflict, @@ -455,36 +408,6 @@ func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.M return ctrl.Result{RequeueAfter: DefaultRequeueInterval}, nil } -// getServiceTemplateSource returns the source (HelmRepository) used by the ServiceTemplate. -// It is fetched by querying for ServiceTemplate -> HelmChart -> HelmRepository. -func (r *ManagedClusterReconciler) getServiceTemplateSource(ctx context.Context, tmpl *hmc.ServiceTemplate) (*sourcev1.HelmRepository, error) { - tmplRef := types.NamespacedName{Namespace: tmpl.Namespace, Name: tmpl.Name} - - if tmpl.Status.ChartRef == nil { - return nil, fmt.Errorf("status for ServiceTemplate (%s) has not been updated yet", tmplRef.String()) - } - - hc := &sourcev1.HelmChart{} - if err := r.Get(ctx, types.NamespacedName{ - Namespace: tmpl.Status.ChartRef.Namespace, - Name: tmpl.Status.ChartRef.Name, - }, hc); err != nil { - return nil, fmt.Errorf("failed to get HelmChart (%s): %w", tmplRef.String(), err) - } - - repo := &sourcev1.HelmRepository{} - if err := r.Get(ctx, types.NamespacedName{ - // Using chart's namespace because it's source - // (helm repository in this case) should be within the same namespace. - Namespace: hc.Namespace, - Name: hc.Spec.SourceRef.Name, - }, repo); err != nil { - return nil, fmt.Errorf("failed to get HelmRepository (%s): %w", tmplRef.String(), err) - } - - return repo, nil -} - func validateReleaseWithValues(ctx context.Context, actionConfig *action.Configuration, managedCluster *hmc.ManagedCluster, hcChart *chart.Chart) error { install := action.NewInstall(actionConfig) install.DryRun = true diff --git a/internal/controller/multiclusterservice_controller.go b/internal/controller/multiclusterservice_controller.go index 46e5ab497..de495a8cd 100644 --- a/internal/controller/multiclusterservice_controller.go +++ b/internal/controller/multiclusterservice_controller.go @@ -16,11 +16,19 @@ package controller import ( "context" + "fmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/controllerutil" hmc "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/internal/sveltos" + "github.com/Mirantis/hmc/internal/utils" + "github.com/go-logr/logr" ) // MultiClusterServiceReconciler reconciles a MultiClusterService object @@ -29,10 +37,132 @@ type MultiClusterServiceReconciler struct { } // Reconcile reconciles a MultiClusterService object. -func (*MultiClusterServiceReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { - _ = ctrl.LoggerFrom(ctx) +func (r *MultiClusterServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := ctrl.LoggerFrom(ctx).WithValues("MultiClusterServiceController", req.NamespacedName.String()) + l.Info("Reconciling MultiClusterService") - // TODO(https://github.com/Mirantis/hmc/issues/455): Implement me. + mcsvc := &hmc.MultiClusterService{} + err := r.Get(ctx, req.NamespacedName, mcsvc) + if apierrors.IsNotFound(err) { + l.Info("MultiClusterService not found, ignoring since object must be deleted") + return ctrl.Result{}, nil + } + if err != nil { + l.Error(err, "Failed to get MultiClusterService") + return ctrl.Result{}, err + } + + if !mcsvc.DeletionTimestamp.IsZero() { + l.Info("Deleting MultiClusterService") + return r.reconcileDelete(ctx, mcsvc) + } + + if ok := controllerutil.AddFinalizer(mcsvc, hmc.MultiClusterServiceFinalizer); ok { + if err := r.Client.Update(ctx, mcsvc); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update MultiClusterService %s with finalizer %s: %w", mcsvc.Name, hmc.MultiClusterServiceFinalizer, err) + } + return ctrl.Result{}, nil + } + + // By using DefaultSystemNamespace we are enforcing that MultiClusterService + // may only use ServiceTemplates that are present in the hmc-system namespace. + opts, err := HelmChartOpts(ctx, r.Client, l, utils.DefaultSystemNamespace, mcsvc.Spec.Services) + if err != nil { + return ctrl.Result{}, err + } + + if _, err := sveltos.ReconcileClusterProfile(ctx, r.Client, l, mcsvc.Name, + sveltos.ReconcileProfileOpts{ + OwnerReference: &metav1.OwnerReference{ + APIVersion: hmc.GroupVersion.String(), + Kind: hmc.MultiClusterServiceKind, + Name: mcsvc.Name, + UID: mcsvc.UID, + }, + LabelSelector: mcsvc.Spec.ClusterSelector, + HelmChartOpts: opts, + Priority: mcsvc.Spec.Priority, + StopOnConflict: mcsvc.Spec.StopOnConflict, + }); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to reconcile ClusterProfile: %w", err) + } + + return ctrl.Result{}, nil +} + +// HelmChartOpts returns slice of helm chart options to use with Sveltos. +// Namespace is the namespace of the referred templates in services slice. +func HelmChartOpts(ctx context.Context, c client.Client, l logr.Logger, namespace string, services []hmc.ServiceSpec) ([]sveltos.HelmChartOpts, error) { + opts := []sveltos.HelmChartOpts{} + + // NOTE: The Profile/ClusterProfile object will be updated with + // no helm charts if len(mc.Spec.Services) == 0. This will result + // in the helm charts being uninstalled on matching clusters if + // Profile/ClusterProfile originally had len(m.Spec.Sevices) > 0. + for _, svc := range services { + if svc.Disable { + l.Info(fmt.Sprintf("Skip adding Template %s because Disable=true", svc.Template)) + continue + } + + tmpl := &hmc.ServiceTemplate{} + // Here we can use the same namespace for all services + // because if the services slice is part of: + // 1. ManagedCluster: Then the referred template must be in its own namespace. + // 2. MultiClusterService: Then the referred template must be in hmc-system namespace. + ref := types.NamespacedName{Name: svc.Template, Namespace: namespace} + if err := c.Get(ctx, ref, tmpl); err != nil { + return nil, fmt.Errorf("failed to get Template %s: %w", ref.String(), err) + } + + source, err := TemplateSource(ctx, c, tmpl) + if err != nil { + return nil, fmt.Errorf("could not get repository url: %w", err) + } + + opts = append(opts, sveltos.HelmChartOpts{ + Values: svc.Values, + RepositoryURL: source.Spec.URL, + // We don't have repository name so chart name becomes repository name. + RepositoryName: tmpl.Spec.Helm.ChartName, + ChartName: func() string { + if source.Spec.Type == utils.RegistryTypeOCI { + return tmpl.Spec.Helm.ChartName + } + // Sveltos accepts ChartName in / format for non-OCI. + // We don't have a repository name, so we can use / instead. + // See: https://projectsveltos.github.io/sveltos/addons/helm_charts/. + return fmt.Sprintf("%s/%s", tmpl.Spec.Helm.ChartName, tmpl.Spec.Helm.ChartName) + }(), + ChartVersion: tmpl.Spec.Helm.ChartVersion, + ReleaseName: svc.Name, + ReleaseNamespace: func() string { + if svc.Namespace != "" { + return svc.Namespace + } + return svc.Name + }(), + // The reason it is passed to PlainHTTP instead of InsecureSkipTLSVerify is because + // the source.Spec.Insecure field is meant to be used for connecting to repositories + // over plain HTTP, which is different than what InsecureSkipTLSVerify is meant for. + // See: https://github.com/fluxcd/source-controller/pull/1288 + PlainHTTP: source.Spec.Insecure, + }) + } + + return opts, nil +} + +func (r *MultiClusterServiceReconciler) reconcileDelete(ctx context.Context, mcsvc *hmc.MultiClusterService) (ctrl.Result, error) { + if err := sveltos.DeleteClusterProfile(ctx, r.Client, mcsvc.Name); err != nil { + return ctrl.Result{}, err + } + + if ok := controllerutil.RemoveFinalizer(mcsvc, hmc.MultiClusterServiceFinalizer); ok { + if err := r.Client.Update(ctx, mcsvc); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer %s from MultiClusterService %s: %w", hmc.MultiClusterServiceFinalizer, mcsvc.Name, err) + } + } return ctrl.Result{}, nil } diff --git a/internal/controller/multiclusterservice_controller_test.go b/internal/controller/multiclusterservice_controller_test.go index e14ad3dff..e0adfe6d0 100644 --- a/internal/controller/multiclusterservice_controller_test.go +++ b/internal/controller/multiclusterservice_controller_test.go @@ -16,62 +16,122 @@ package controller import ( "context" + "time" + hcv2 "github.com/fluxcd/helm-controller/api/v2" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - hmcmirantiscomv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" + hmc "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/internal/utils" ) var _ = Describe("MultiClusterService Controller", func() { Context("When reconciling a resource", func() { - const resourceName = "test-resource" + const ( + multiClusterServiceName = "test-multiclusterservice" + serviceTemplateName = "test-service-0-1-0" + serviceReleaseName = "test-service" + ) ctx := context.Background() - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + multiClusterServiceRef := types.NamespacedName{ + Name: multiClusterServiceName, } - multiclusterservice := &hmcmirantiscomv1alpha1.MultiClusterService{} + serviceTemplateRef := types.NamespacedName{ + Namespace: utils.DefaultSystemNamespace, + Name: serviceTemplateName, + } + + multiClusterService := &hmc.MultiClusterService{} + namespace := &corev1.Namespace{} + serviceTemplate := &hmc.ServiceTemplate{} BeforeEach(func() { + By("creating hmc-system namespace") + err := k8sClient.Get(ctx, types.NamespacedName{Name: utils.DefaultSystemNamespace}, namespace) + if err != nil && errors.IsNotFound(err) { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.DefaultSystemNamespace, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + } + + By("creating the custom resource for the Kind Template") + err = k8sClient.Get(ctx, serviceTemplateRef, serviceTemplate) + if err != nil && errors.IsNotFound(err) { + serviceTemplate = &hmc.ServiceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceTemplateName, + Namespace: utils.DefaultSystemNamespace, + }, + Spec: hmc.ServiceTemplateSpec{ + TemplateSpecCommon: hmc.TemplateSpecCommon{ + Helm: hmc.HelmSpec{ + ChartRef: &hcv2.CrossNamespaceSourceReference{ + Kind: "HelmChart", + Name: "ref-test", + Namespace: utils.DefaultSystemNamespace, + }, + }, + }, + }, + } + } + Expect(k8sClient.Create(ctx, serviceTemplate)).To(Succeed()) + By("creating the custom resource for the Kind MultiClusterService") - err := k8sClient.Get(ctx, typeNamespacedName, multiclusterservice) + err = k8sClient.Get(ctx, multiClusterServiceRef, multiClusterService) if err != nil && errors.IsNotFound(err) { - resource := &hmcmirantiscomv1alpha1.MultiClusterService{ + multiClusterService = &hmc.MultiClusterService{ ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", + Name: multiClusterServiceName, + }, + Spec: hmc.MultiClusterServiceSpec{ + Services: []hmc.ServiceSpec{ + { + Template: serviceTemplateName, + Name: serviceReleaseName, + }, + }, }, - // TODO(user): Specify other spec details if needed. } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Create(ctx, multiClusterService)).To(Succeed()) } }) AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &hmcmirantiscomv1alpha1.MultiClusterService{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) + By("Cleanup") + + reconciler := &MultiClusterServiceReconciler{ + Client: k8sClient, + } + + Expect(k8sClient.Delete(ctx, multiClusterService)).To(Succeed()) + // Running reconcile to remove the finalizer and delete the MultiClusterService + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: multiClusterServiceRef}) Expect(err).NotTo(HaveOccurred()) - By("Cleanup the specific resource instance MultiClusterService") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Eventually(k8sClient.Get(ctx, multiClusterServiceRef, multiClusterService), 1*time.Minute, 5*time.Second).Should(HaveOccurred()) + Expect(k8sClient.Delete(ctx, serviceTemplate)).To(Succeed()) }) It("should successfully reconcile the resource", func() { By("Reconciling the created resource") - controllerReconciler := &MultiClusterServiceReconciler{ + reconciler := &MultiClusterServiceReconciler{ Client: k8sClient, } - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + _, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: multiClusterServiceRef, }) Expect(err).NotTo(HaveOccurred()) // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index da312028f..667fdaeff 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -27,6 +27,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -344,3 +345,32 @@ func (r *ProviderTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&hmc.ProviderTemplate{}). Complete(r) } + +// TemplateSource returns the source of the provided template. +func TemplateSource(ctx context.Context, c client.Client, tmpl Template) (*sourcev1.HelmRepository, error) { + ref := types.NamespacedName{Namespace: tmpl.GetNamespace(), Name: tmpl.GetName()} + + if tmpl.GetStatus() == nil || tmpl.GetStatus().ChartRef == nil { + return nil, fmt.Errorf("status for Template (%s) has not been updated yet", ref.String()) + } + + hc := &sourcev1.HelmChart{} + if err := c.Get(ctx, types.NamespacedName{ + Namespace: tmpl.GetStatus().ChartRef.Namespace, + Name: tmpl.GetStatus().ChartRef.Name, + }, hc); err != nil { + return nil, fmt.Errorf("failed to get HelmChart (%s): %w", ref.String(), err) + } + + repo := &sourcev1.HelmRepository{} + if err := c.Get(ctx, types.NamespacedName{ + // Using chart's namespace because it's source + // (helm repository in this case) should be within the same namespace. + Namespace: hc.Namespace, + Name: hc.Spec.SourceRef.Name, + }, repo); err != nil { + return nil, fmt.Errorf("failed to get HelmRepository (%s): %w", ref.String(), err) + } + + return repo, nil +} diff --git a/internal/sveltos/profile.go b/internal/sveltos/profile.go index 500b40dab..cf2e7164d 100644 --- a/internal/sveltos/profile.go +++ b/internal/sveltos/profile.go @@ -33,6 +33,7 @@ import ( type ReconcileProfileOpts struct { OwnerReference *metav1.OwnerReference + LabelSelector metav1.LabelSelector HelmChartOpts []HelmChartOpts Priority int32 StopOnConflict bool @@ -50,96 +51,149 @@ type HelmChartOpts struct { InsecureSkipTLSVerify bool } +// ReconcileClusterProfile reconciles a Sveltos ClusterProfile object. +func ReconcileClusterProfile( + ctx context.Context, + cl client.Client, + l logr.Logger, + name string, + opts ReconcileProfileOpts, +) (*sveltosv1beta1.ClusterProfile, error) { + obj := objectMeta(opts.OwnerReference) + obj.SetName(name) + + cp := &sveltosv1beta1.ClusterProfile{ + ObjectMeta: obj, + } + + operation, err := ctrl.CreateOrUpdate(ctx, cl, cp, func() error { + spec, err := Spec(&opts) + if err != nil { + return err + } + cp.Spec = *spec + + return nil + }) + if err != nil { + return nil, err + } + + if operation == controllerutil.OperationResultCreated || operation == controllerutil.OperationResultUpdated { + l.Info(fmt.Sprintf("Successfully %s ClusterProfile %s", string(operation), cp.Name)) + } + + return cp, nil +} + // ReconcileProfile reconciles a Sveltos Profile object. -func ReconcileProfile(ctx context.Context, +func ReconcileProfile( + ctx context.Context, cl client.Client, l logr.Logger, namespace string, name string, - matchLabels map[string]string, opts ReconcileProfileOpts, ) (*sveltosv1beta1.Profile, error) { - cp := &sveltosv1beta1.Profile{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, + obj := objectMeta(opts.OwnerReference) + obj.SetNamespace(namespace) + obj.SetName(name) + + p := &sveltosv1beta1.Profile{ + ObjectMeta: obj, } + operation, err := ctrl.CreateOrUpdate(ctx, cl, p, func() error { + spec, err := Spec(&opts) + if err != nil { + return err + } + p.Spec = *spec + + return nil + }) + if err != nil { + return nil, err + } + + if operation == controllerutil.OperationResultCreated || operation == controllerutil.OperationResultUpdated { + l.Info(fmt.Sprintf("Successfully %s Profile %s", string(operation), p.Name)) + } + + return p, nil +} + +// Spec returns a spec object to be used with +// a Sveltos Profle or ClusterProfile object. +func Spec(opts *ReconcileProfileOpts) (*sveltosv1beta1.Spec, error) { tier, err := PriorityToTier(opts.Priority) if err != nil { return nil, err } - operation, err := ctrl.CreateOrUpdate(ctx, cl, cp, func() error { - if cp.Labels == nil { - cp.Labels = make(map[string]string) - } + spec := &sveltosv1beta1.Spec{ + ClusterSelector: libsveltosv1beta1.Selector{ + LabelSelector: opts.LabelSelector, + }, + Tier: tier, + ContinueOnConflict: !opts.StopOnConflict, + } - cp.Labels[hmc.HMCManagedLabelKey] = hmc.HMCManagedLabelValue - if opts.OwnerReference != nil { - cp.OwnerReferences = []metav1.OwnerReference{*opts.OwnerReference} + for _, hc := range opts.HelmChartOpts { + helmChart := sveltosv1beta1.HelmChart{ + RepositoryURL: hc.RepositoryURL, + RepositoryName: hc.RepositoryName, + ChartName: hc.ChartName, + ChartVersion: hc.ChartVersion, + ReleaseName: hc.ReleaseName, + ReleaseNamespace: hc.ReleaseNamespace, + HelmChartAction: sveltosv1beta1.HelmChartActionInstall, + RegistryCredentialsConfig: &sveltosv1beta1.RegistryCredentialsConfig{ + PlainHTTP: hc.PlainHTTP, + InsecureSkipTLSVerify: hc.InsecureSkipTLSVerify, + }, } - cp.Spec = sveltosv1beta1.Spec{ - ClusterSelector: libsveltosv1beta1.Selector{ - LabelSelector: metav1.LabelSelector{ - MatchLabels: matchLabels, - }, - }, - Tier: tier, - ContinueOnConflict: !opts.StopOnConflict, + if hc.PlainHTTP { + // InsecureSkipTLSVerify is redundant in this case. + helmChart.RegistryCredentialsConfig.InsecureSkipTLSVerify = false } - for _, hc := range opts.HelmChartOpts { - helmChart := sveltosv1beta1.HelmChart{ - RepositoryURL: hc.RepositoryURL, - RepositoryName: hc.RepositoryName, - ChartName: hc.ChartName, - ChartVersion: hc.ChartVersion, - ReleaseName: hc.ReleaseName, - ReleaseNamespace: hc.ReleaseNamespace, - HelmChartAction: sveltosv1beta1.HelmChartActionInstall, - RegistryCredentialsConfig: &sveltosv1beta1.RegistryCredentialsConfig{ - PlainHTTP: hc.PlainHTTP, - InsecureSkipTLSVerify: hc.InsecureSkipTLSVerify, - }, + if hc.Values != nil { + b, err := hc.Values.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal values to JSON for service %s: %w", hc.RepositoryName, err) } - if hc.PlainHTTP { - // InsecureSkipTLSVerify is redundant in this case. - helmChart.RegistryCredentialsConfig.InsecureSkipTLSVerify = false + b, err = yaml.JSONToYAML(b) + if err != nil { + return nil, fmt.Errorf("failed to convert values from JSON to YAML for service %s: %w", hc.RepositoryName, err) } - if hc.Values != nil { - b, err := hc.Values.MarshalJSON() - if err != nil { - return fmt.Errorf("failed to marshal values to JSON for service (%s) in ManagedCluster: %w", hc.RepositoryName, err) - } + helmChart.Values = string(b) + } - b, err = yaml.JSONToYAML(b) - if err != nil { - return fmt.Errorf("failed to convert values from JSON to YAML for service (%s) in ManagedCluster: %w", hc.RepositoryName, err) - } + spec.HelmCharts = append(spec.HelmCharts, helmChart) + } - helmChart.Values = string(b) - } + return spec, nil +} - cp.Spec.HelmCharts = append(cp.Spec.HelmCharts, helmChart) - } - return nil - }) - if err != nil { - return nil, err +func objectMeta(owner *metav1.OwnerReference) metav1.ObjectMeta { + obj := metav1.ObjectMeta{ + Labels: map[string]string{ + hmc.HMCManagedLabelKey: hmc.HMCManagedLabelValue, + }, } - if operation == controllerutil.OperationResultCreated || operation == controllerutil.OperationResultUpdated { - l.Info(fmt.Sprintf("Successfully %s Profile (%s/%s)", string(operation), cp.Namespace, cp.Name)) + if owner != nil { + obj.OwnerReferences = []metav1.OwnerReference{*owner} } - return cp, nil + return obj } +// DeleteProfile deletes a Sveltos Profile object. func DeleteProfile(ctx context.Context, cl client.Client, namespace string, name string) error { err := cl.Delete(ctx, &sveltosv1beta1.Profile{ ObjectMeta: metav1.ObjectMeta{ @@ -151,6 +205,17 @@ func DeleteProfile(ctx context.Context, cl client.Client, namespace string, name return client.IgnoreNotFound(err) } +// DeleteClusterProfile deletes a Sveltos ClusterProfile object. +func DeleteClusterProfile(ctx context.Context, cl client.Client, name string) error { + err := cl.Delete(ctx, &sveltosv1beta1.ClusterProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }) + + return client.IgnoreNotFound(err) +} + // PriorityToTier converts priority value to Sveltos tier value. func PriorityToTier(priority int32) (int32, error) { var mini int32 = 1 diff --git a/templates/provider/hmc-templates/files/templates/ingress-nginx-4-11-3.yaml b/templates/provider/hmc-templates/files/templates/ingress-nginx-4-11-3.yaml new file mode 100644 index 000000000..6e33c5e85 --- /dev/null +++ b/templates/provider/hmc-templates/files/templates/ingress-nginx-4-11-3.yaml @@ -0,0 +1,10 @@ +apiVersion: hmc.mirantis.com/v1alpha1 +kind: ServiceTemplate +metadata: + name: ingress-nginx-4-11-3 + annotations: + helm.sh/resource-policy: keep +spec: + helm: + chartName: ingress-nginx + chartVersion: 4.11.3 diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml index 83c58b480..29acb4265 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml @@ -113,6 +113,7 @@ spec: type: object type: array stopOnConflict: + default: false description: |- StopOnConflict specifies what to do in case of a conflict. E.g. If another object is already managing a service. diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml index 953f6b87c..d7e5f4ebe 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml @@ -132,6 +132,7 @@ spec: type: object type: array stopOnConflict: + default: false description: |- StopOnConflict specifies what to do in case of a conflict. E.g. If another object is already managing a service. diff --git a/templates/provider/hmc/templates/rbac/controller/roles.yaml b/templates/provider/hmc/templates/rbac/controller/roles.yaml index 21c49b962..8c58c2071 100644 --- a/templates/provider/hmc/templates/rbac/controller/roles.yaml +++ b/templates/provider/hmc/templates/rbac/controller/roles.yaml @@ -165,6 +165,7 @@ rules: - config.projectsveltos.io resources: - profiles + - clusterprofiles verbs: {{ include "rbac.editorVerbs" . | nindent 4 }} - apiGroups: - hmc.mirantis.com diff --git a/templates/service/ingress-nginx/Chart.lock b/templates/service/ingress-nginx-4-11-0/Chart.lock similarity index 100% rename from templates/service/ingress-nginx/Chart.lock rename to templates/service/ingress-nginx-4-11-0/Chart.lock diff --git a/templates/service/ingress-nginx/Chart.yaml b/templates/service/ingress-nginx-4-11-0/Chart.yaml similarity index 100% rename from templates/service/ingress-nginx/Chart.yaml rename to templates/service/ingress-nginx-4-11-0/Chart.yaml diff --git a/templates/service/ingress-nginx-4-11-3/Chart.lock b/templates/service/ingress-nginx-4-11-3/Chart.lock new file mode 100644 index 000000000..228d884c2 --- /dev/null +++ b/templates/service/ingress-nginx-4-11-3/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: ingress-nginx + repository: https://kubernetes.github.io/ingress-nginx + version: 4.11.3 +digest: sha256:0963a4470e5fe0ce97023b16cfc9c3cde18b74707c6379947542e09afa6d5346 +generated: "2024-10-10T22:41:15.721165-04:00" diff --git a/templates/service/ingress-nginx-4-11-3/Chart.yaml b/templates/service/ingress-nginx-4-11-3/Chart.yaml new file mode 100644 index 000000000..8fe3cc1d5 --- /dev/null +++ b/templates/service/ingress-nginx-4-11-3/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: ingress-nginx +description: A Helm chart to refer the official ingress-nginx helm chart +type: application +version: 4.11.3 +appVersion: "1.11.3" +dependencies: + - name: ingress-nginx + version: 4.11.3 + repository: https://kubernetes.github.io/ingress-nginx