From e9e50179c734ffa2e6e77806f1e588eaf423063b Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Thu, 9 Feb 2023 08:14:34 -0700 Subject: [PATCH] Curated packages PBC full cluster lifecycle integration This change introduces the creation and deletion of a namespaced package bundle controller for workload clusters created via the full cluster lifecycle. This change is dependent on a change in the eks-anywhere-packages repo to grant permissions to the eksa-controller-manager service account. https://github.com/aws/eks-anywhere-packages/pull/833 Without the change above, the reconciliation will log errors removing some resources, which can be left behind. Future commits will add packages installation/deletion via full cluster lifecycle. --- Makefile | 4 +- config/manifest/eksa-components.yaml | 12 ++ config/rbac/role.yaml | 12 ++ controllers/cluster_controller.go | 111 +++++++++++++++++- controllers/cluster_controller_legacy.go | 1 + controllers/cluster_controller_test.go | 14 +-- controllers/cluster_controller_test_test.go | 101 ++++++++++++++-- controllers/factory.go | 23 +++- controllers/mocks/cluster_controller.go | 60 ++++++++++ pkg/curatedpackages/mocks/installer.go | 37 ++++++ .../packagecontrollerclient.go | 95 ++++++++++++++- .../packagecontrollerclient_test.go | 30 ++--- pkg/dependencies/factory.go | 1 + pkg/executables/helm.go | 31 +++++ 14 files changed, 496 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index e66f4baeb4cf8..1cf3d4bde28c3 100644 --- a/Makefile +++ b/Makefile @@ -568,7 +568,7 @@ mocks: ## Generate mocks ${MOCKGEN} -destination=pkg/curatedpackages/mocks/bundlemanager.go -package=mocks -source "pkg/curatedpackages/bundlemanager.go" Manager ${MOCKGEN} -destination=pkg/clients/kubernetes/mocks/kubectl.go -package=mocks -source "pkg/clients/kubernetes/unauth.go" ${MOCKGEN} -destination=pkg/clients/kubernetes/mocks/kubeconfig.go -package=mocks -source "pkg/clients/kubernetes/kubeconfig.go" - ${MOCKGEN} -destination=pkg/curatedpackages/mocks/installer.go -package=mocks -source "pkg/curatedpackages/packagecontrollerclient.go" ChartInstaller + ${MOCKGEN} -destination=pkg/curatedpackages/mocks/installer.go -package=mocks -source "pkg/curatedpackages/packagecontrollerclient.go" ChartInstaller ChartInstallationDeleter ${MOCKGEN} -destination=pkg/cluster/mocks/client_builder.go -package=mocks -source "pkg/cluster/client_builder.go" ${MOCKGEN} -destination=controllers/mocks/factory.go -package=mocks "github.com/aws/eks-anywhere/controllers" Manager ${MOCKGEN} -destination=pkg/networking/cilium/reconciler/mocks/templater.go -package=mocks -source "pkg/networking/cilium/reconciler/reconciler.go" @@ -578,7 +578,7 @@ mocks: ## Generate mocks ${MOCKGEN} -destination=pkg/providers/docker/reconciler/mocks/reconciler.go -package=mocks -source "pkg/providers/docker/reconciler/reconciler.go" ${MOCKGEN} -destination=pkg/providers/tinkerbell/reconciler/mocks/reconciler.go -package=mocks -source "pkg/providers/tinkerbell/reconciler/reconciler.go" ${MOCKGEN} -destination=pkg/awsiamauth/reconciler/mocks/reconciler.go -package=mocks -source "pkg/awsiamauth/reconciler/reconciler.go" - ${MOCKGEN} -destination=controllers/mocks/cluster_controller.go -package=mocks -source "controllers/cluster_controller.go" AWSIamConfigReconciler ClusterValidator + ${MOCKGEN} -destination=controllers/mocks/cluster_controller.go -package=mocks -source "controllers/cluster_controller.go" AWSIamConfigReconciler ClusterValidator PackageControllerClient ${MOCKGEN} -destination=pkg/workflow/task_mock_test.go -package=workflow_test -source "pkg/workflow/task.go" ${MOCKGEN} -destination=pkg/validations/createcluster/mocks/createcluster.go -package=mocks -source "pkg/validations/createcluster/createcluster.go" ${MOCKGEN} -destination=pkg/awsiamauth/mock_test.go -package=awsiamauth_test -source "pkg/awsiamauth/installer.go" diff --git a/config/manifest/eksa-components.yaml b/config/manifest/eksa-components.yaml index 7586a4eaa6103..cb6df7e219b8a 100644 --- a/config/manifest/eksa-components.yaml +++ b/config/manifest/eksa-components.yaml @@ -5901,6 +5901,18 @@ rules: - patch - update - watch +- apiGroups: + - packages.eks.amazonaws.com + resources: + - packagebundlecontrollers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - tinkerbell.org resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4c81fa1423dd2..841ba435400b1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -210,6 +210,18 @@ rules: - patch - update - watch +- apiGroups: + - packages.eks.amazonaws.com + resources: + - packagebundlecontrollers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - tinkerbell.org resources: diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go index 02a1452cf6c42..f001c7fe8b7f0 100644 --- a/controllers/cluster_controller.go +++ b/controllers/cluster_controller.go @@ -3,10 +3,13 @@ package controllers import ( "context" "fmt" + "os" "time" "github.com/go-logr/logr" "github.com/pkg/errors" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -27,7 +30,10 @@ import ( "github.com/aws/eks-anywhere/pkg/controller/clientutil" "github.com/aws/eks-anywhere/pkg/controller/clusters" "github.com/aws/eks-anywhere/pkg/controller/handlers" + "github.com/aws/eks-anywhere/pkg/curatedpackages" + "github.com/aws/eks-anywhere/pkg/registrymirror" "github.com/aws/eks-anywhere/pkg/utils/ptr" + "github.com/aws/eks-anywhere/release/api/v1alpha1" ) const ( @@ -42,6 +48,7 @@ type ClusterReconciler struct { providerReconcilerRegistry ProviderClusterReconcilerRegistry awsIamAuth AWSIamConfigReconciler clusterValidator ClusterValidator + packageControllerClient PackageControllerClient } type ProviderClusterReconcilerRegistry interface { @@ -61,12 +68,13 @@ type ClusterValidator interface { } // NewClusterReconciler constructs a new ClusterReconciler. -func NewClusterReconciler(client client.Client, registry ProviderClusterReconcilerRegistry, awsIamAuth AWSIamConfigReconciler, clusterValidator ClusterValidator) *ClusterReconciler { +func NewClusterReconciler(client client.Client, registry ProviderClusterReconcilerRegistry, awsIamAuth AWSIamConfigReconciler, clusterValidator ClusterValidator, packageControllerClient PackageControllerClient) *ClusterReconciler { return &ClusterReconciler{ client: client, providerReconcilerRegistry: registry, awsIamAuth: awsIamAuth, clusterValidator: clusterValidator, + packageControllerClient: packageControllerClient, } } @@ -262,9 +270,99 @@ func (r *ClusterReconciler) postClusterProviderReconcile(ctx context.Context, lo } } + if !cluster.IsSelfManaged() { + if err := r.postReconcilePackagesForWorkloadCluster(ctx, log, cluster); err != nil { + return controller.Result{}, err + } + } + return controller.Result{}, nil } +type PackageControllerClient interface { + EnableCuratedPackagesFullLifecycle(context.Context, logr.Logger, string, string, *v1alpha1.Image, *registrymirror.RegistryMirror, ...curatedpackages.PackageControllerClientOpt) error + ReconcileDelete(context.Context, logr.Logger, client.Client, *anywherev1.Cluster) error +} + +var _ PackageControllerClient = (*curatedpackages.PackageControllerClient)(nil) + +func (r *ClusterReconciler) postReconcilePackagesForWorkloadCluster(ctx context.Context, log logr.Logger, cluster *anywherev1.Cluster) (err error) { + bundles := &v1alpha1.Bundles{} + nn := types.NamespacedName{ + Name: cluster.Spec.BundlesRef.Name, + Namespace: cluster.Spec.BundlesRef.Namespace, + } + if err := r.client.Get(ctx, nn, bundles); err != nil { + return err + } + + verBundle, err := r.findMatchingBundle(bundles, string(cluster.Spec.KubernetesVersion)) + if err != nil { + return err + } + + image, ok := verBundle.Charts()["eks-anywhere-packages"] + if !ok { + return fmt.Errorf("no chart image") + } + + kubeConfigNN := types.NamespacedName{ + Namespace: constants.EksaSystemNamespace, + Name: cluster.Name + "-kubeconfig", + } + kubeConfigSecret := &corev1.Secret{} + if err := r.client.Get(ctx, kubeConfigNN, kubeConfigSecret); err != nil { + return fmt.Errorf("getting kubeconfig secret: %w", err) + } + secretBytes, err := yaml.Marshal(kubeConfigSecret) + if err != nil { + return fmt.Errorf("marshaling secret %w", err) + } + f, err := os.CreateTemp("", "kubeconfig-*.yaml") + if err != nil { + return fmt.Errorf("opening kubeconfig file %w", err) + } + defer f.Close() + // TODO unlink + + if _, err := f.Write(secretBytes); err != nil { + return fmt.Errorf("writing kubeconfig file %w", err) + } + f.Close() + + rm := registrymirror.FromCluster(cluster) + var options []curatedpackages.PackageControllerClientOpt + err = r.packageControllerClient.EnableCuratedPackagesFullLifecycle(ctx, log, + cluster.Name, + f.Name(), + image, + rm, + options..., + ) + if err != nil { + return fmt.Errorf("package controller client error: %w", err) + } + + log.V(6).Info("Installed curated packages on workload cluster", "cluster", cluster.Name) + + return nil +} + +func (r *ClusterReconciler) findMatchingBundle(bundles *v1alpha1.Bundles, kubeVersion string) (*v1alpha1.VersionsBundle, error) { + var verBundle *v1alpha1.VersionsBundle + for _, b := range bundles.Spec.VersionsBundles { + if b.KubeVersion == string(kubeVersion) { + verBundle = &b + break + } + } + if verBundle == nil { + return nil, fmt.Errorf("no bundle for kube version %q", kubeVersion) + } + + return verBundle, nil +} + func (r *ClusterReconciler) reconcileDelete(ctx context.Context, log logr.Logger, cluster *anywherev1.Cluster) (ctrl.Result, error) { if cluster.IsSelfManaged() { return ctrl.Result{}, errors.New("deleting self-managed clusters is not supported") @@ -310,6 +408,17 @@ func (r *ClusterReconciler) reconcileDelete(ctx context.Context, log logr.Logger } } + if !cluster.IsSelfManaged() { + // TODO all those tests that pass a nil package controller client will bork here, as the packageControllerClient will be nil. + if r.packageControllerClient == nil { + return ctrl.Result{}, nil + // return ctrl.Result{}, fmt.Errorf("controller has a nil package controller client, cannot delete package bundle controller") + } + if err := r.packageControllerClient.ReconcileDelete(ctx, log, r.client, cluster); err != nil { + return ctrl.Result{}, fmt.Errorf("deleting packages controller for cluster %q %w", cluster.Name, err) + } + } + return ctrl.Result{}, nil } diff --git a/controllers/cluster_controller_legacy.go b/controllers/cluster_controller_legacy.go index 40715fdf7f38c..3e6d69ca6a7b7 100644 --- a/controllers/cluster_controller_legacy.go +++ b/controllers/cluster_controller_legacy.go @@ -60,6 +60,7 @@ func NewClusterReconcilerLegacy(client client.Client, log logr.Logger, scheme *r // +kubebuilder:rbac:groups="",namespace=eksa-system,resources=secrets,verbs=delete; // +kubebuilder:rbac:groups=tinkerbell.org,resources=hardware;hardware/status,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=bmc.tinkerbell.org,resources=machines;machines/status,verbs=get;list;watch +// +kubebuilder:rbac:groups=packages.eks.amazonaws.com,resources=packagebundlecontrollers,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. diff --git a/controllers/cluster_controller_test.go b/controllers/cluster_controller_test.go index 954d89b34dac2..eb8504fe9f400 100644 --- a/controllers/cluster_controller_test.go +++ b/controllers/cluster_controller_test.go @@ -68,7 +68,7 @@ func newVsphereClusterReconcilerTest(t *testing.T, objs ...runtime.Object) *vsph Add(anywherev1.VSphereDatacenterKind, reconciler). Build() - r := controllers.NewClusterReconciler(cl, ®istry, iam, clusterValidator) + r := controllers.NewClusterReconciler(cl, ®istry, iam, clusterValidator, nil) return &vsphereClusterReconcilerTest{ govcClient: govcClient, @@ -101,9 +101,9 @@ func TestClusterReconcilerReconcileSelfManagedCluster(t *testing.T) { providerReconciler.EXPECT().ReconcileWorkerNodes(ctx, gomock.AssignableToTypeOf(logr.Logger{}), sameName(selfManagedCluster)) - r := controllers.NewClusterReconciler(c, registry, iam, clusterValidator) + r := controllers.NewClusterReconciler(c, registry, iam, clusterValidator, nil) result, err := r.Reconcile(ctx, clusterRequest(selfManagedCluster)) - g.Expect(err).ToNot(HaveOccurred()) + g.Expect(err).NotTo(HaveOccurred()) g.Expect(result).To(Equal(ctrl.Result{})) } @@ -128,7 +128,7 @@ func TestClusterReconcilerReconcilePausedCluster(t *testing.T) { iam := mocks.NewMockAWSIamConfigReconciler(ctrl) clusterValidator := mocks.NewMockClusterValidator(ctrl) registry := newRegistryMock(providerReconciler) - r := controllers.NewClusterReconciler(c, registry, iam, clusterValidator) + r := controllers.NewClusterReconciler(c, registry, iam, clusterValidator, nil) g.Expect(r.Reconcile(ctx, clusterRequest(cluster))).To(Equal(reconcile.Result{})) api := envtest.NewAPIExpecter(t, c) @@ -164,7 +164,7 @@ func TestClusterReconcilerReconcileDeletedSelfManagedCluster(t *testing.T) { registry := newRegistryMock(providerReconciler) c := fake.NewClientBuilder().WithRuntimeObjects(selfManagedCluster).Build() - r := controllers.NewClusterReconciler(c, registry, iam, clusterValidator) + r := controllers.NewClusterReconciler(c, registry, iam, clusterValidator, nil) _, err := r.Reconcile(ctx, clusterRequest(selfManagedCluster)) g.Expect(err).To(MatchError(ContainSubstring("deleting self-managed clusters is not supported"))) } @@ -233,7 +233,7 @@ func TestClusterReconcilerReconcileDeletePausedCluster(t *testing.T) { managementCluster, cluster, capiCluster, ).Build() - r := controllers.NewClusterReconciler(c, newRegistryForDummyProviderReconciler(), iam, clusterValidator) + r := controllers.NewClusterReconciler(c, newRegistryForDummyProviderReconciler(), iam, clusterValidator, nil) g.Expect(r.Reconcile(ctx, clusterRequest(cluster))).To(Equal(reconcile.Result{})) api := envtest.NewAPIExpecter(t, c) @@ -276,7 +276,7 @@ func TestClusterReconcilerReconcileDeleteClusterManagedByCLI(t *testing.T) { iam := mocks.NewMockAWSIamConfigReconciler(controller) clusterValidator := mocks.NewMockClusterValidator(controller) - r := controllers.NewClusterReconciler(c, newRegistryForDummyProviderReconciler(), iam, clusterValidator) + r := controllers.NewClusterReconciler(c, newRegistryForDummyProviderReconciler(), iam, clusterValidator, nil) g.Expect(r.Reconcile(ctx, clusterRequest(cluster))).To(Equal(reconcile.Result{})) api := envtest.NewAPIExpecter(t, c) diff --git a/controllers/cluster_controller_test_test.go b/controllers/cluster_controller_test_test.go index 2bb91c6a07269..8ab0389b819cd 100644 --- a/controllers/cluster_controller_test_test.go +++ b/controllers/cluster_controller_test_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-logr/logr" "github.com/golang/mock/gomock" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -21,8 +22,10 @@ import ( "github.com/aws/eks-anywhere/internal/test/envtest" anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1" "github.com/aws/eks-anywhere/pkg/cluster" + "github.com/aws/eks-anywhere/pkg/constants" "github.com/aws/eks-anywhere/pkg/controller" "github.com/aws/eks-anywhere/pkg/controller/clusters" + "github.com/aws/eks-anywhere/release/api/v1alpha1" ) func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { @@ -34,6 +37,11 @@ func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { Name: "my-management-cluster", Namespace: "my-namespace", }, + Spec: anywherev1.ClusterSpec{ + BundlesRef: &anywherev1.BundlesRef{ + Name: "my-bundles-ref", + }, + }, } cluster := &anywherev1.Cluster{ @@ -41,6 +49,13 @@ func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { Name: "my-cluster", Namespace: "my-namespace", }, + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "v1.25", + BundlesRef: &anywherev1.BundlesRef{ + Name: "my-bundles-ref", + Namespace: "my-namespace", + }, + }, } cluster.Spec.IdentityProviderRefs = []anywherev1.Ref{ { @@ -73,7 +88,29 @@ func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { }, }, } - objs := []runtime.Object{cluster, managementCluster, oidc, awsIAM} + bundles := &v1alpha1.Bundles{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-bundles-ref", + Namespace: cluster.Namespace, + }, + Spec: v1alpha1.BundlesSpec{ + VersionsBundles: []v1alpha1.VersionsBundle{ + { + KubeVersion: "v1.25", + PackageController: v1alpha1.PackageBundle{ + HelmChart: v1alpha1.Image{}, + }, + }, + }, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-kubeconfig", + Namespace: constants.EksaSystemNamespace, + }, + } + objs := []runtime.Object{cluster, managementCluster, oidc, awsIAM, bundles, secret} cb := fake.NewClientBuilder() cl := cb.WithRuntimeObjects(objs...).Build() @@ -84,8 +121,15 @@ func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { validator := newMockClusterValidator(t) validator.EXPECT().ValidateManagementClusterName(ctx, gomock.AssignableToTypeOf(logr.Logger{}), gomock.AssignableToTypeOf(cluster)).Return(nil) - r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), iam, validator) + pcc := newMockPackageControllerClient(t) + pcc.EXPECT().EnableCuratedPackagesFullLifecycle(ctx, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), iam, validator, pcc) _, err := r.Reconcile(ctx, clusterRequest(cluster)) + + g.Expect(cl.Get(ctx, client.ObjectKey{Namespace: cluster.Spec.BundlesRef.Namespace, Name: cluster.Spec.BundlesRef.Name}, bundles)).To(Succeed()) + g.Expect(cl.Get(ctx, client.ObjectKey{Namespace: constants.EksaSystemNamespace, Name: cluster.Name + "-kubeconfig"}, secret)).To(Succeed()) + g.Expect(err).NotTo(HaveOccurred()) newOidc := &anywherev1.OIDCConfig{} @@ -133,7 +177,7 @@ func TestClusterReconcilerReconcileChildObjectNotFound(t *testing.T) { cl := cb.WithRuntimeObjects(objs...).Build() api := envtest.NewAPIExpecter(t, cl) - r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), newMockClusterValidator(t)) + r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), newMockClusterValidator(t), nil) g.Expect(r.Reconcile(ctx, clusterRequest(cluster))).Error().To(MatchError(ContainSubstring("not found"))) c := envtest.CloneNameNamespace(cluster) api.ShouldEventuallyMatch(ctx, c, func(g Gomega) { @@ -145,7 +189,7 @@ func TestClusterReconcilerReconcileChildObjectNotFound(t *testing.T) { func TestClusterReconcilerSetupWithManager(t *testing.T) { client := env.Client() - r := controllers.NewClusterReconciler(client, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), newMockClusterValidator(t)) + r := controllers.NewClusterReconciler(client, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), newMockClusterValidator(t), nil) g := NewWithT(t) g.Expect(r.SetupWithManager(env.Manager(), env.Manager().GetLogger())).To(Succeed()) @@ -174,7 +218,7 @@ func TestClusterReconcilerManagementClusterNotFound(t *testing.T) { cl := cb.WithRuntimeObjects(objs...).Build() api := envtest.NewAPIExpecter(t, cl) - r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), newMockClusterValidator(t)) + r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), newMockClusterValidator(t), nil) g.Expect(r.Reconcile(ctx, clusterRequest(cluster))).Error().To(MatchError(ContainSubstring("\"my-management-cluster\" not found"))) c := envtest.CloneNameNamespace(cluster) api.ShouldEventuallyMatch(ctx, c, func(g Gomega) { @@ -192,7 +236,8 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { }, Spec: anywherev1.ClusterSpec{ BundlesRef: &anywherev1.BundlesRef{ - Name: "my-bundles-ref", + Name: "my-bundles-ref", + Namespace: "my-namespace", }, }, } @@ -201,20 +246,53 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "my-cluster", }, + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "v1.25", + BundlesRef: &anywherev1.BundlesRef{ + Name: "my-bundles-ref", + Namespace: "my-namespace", + }, + }, } cluster.SetManagedBy("my-management-cluster") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-kubeconfig", + Namespace: constants.EksaSystemNamespace, + }, + } + bundles := &v1alpha1.Bundles{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-bundles-ref", + Namespace: cluster.Spec.BundlesRef.Namespace, + }, + Spec: v1alpha1.BundlesSpec{ + VersionsBundles: []v1alpha1.VersionsBundle{ + { + KubeVersion: "v1.25", + PackageController: v1alpha1.PackageBundle{ + HelmChart: v1alpha1.Image{}, + }, + }, + }, + }, + } - objs := []runtime.Object{cluster, managementCluster} + objs := []runtime.Object{cluster, managementCluster, secret, bundles} cb := fake.NewClientBuilder() cl := cb.WithRuntimeObjects(objs...).Build() mgmtCluster := &anywherev1.Cluster{} g.Expect(cl.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: managementCluster.Name}, mgmtCluster)).To(Succeed()) + g.Expect(cl.Get(ctx, client.ObjectKey{Namespace: cluster.Spec.BundlesRef.Namespace, Name: cluster.Spec.BundlesRef.Name}, bundles)).To(Succeed()) + g.Expect(cl.Get(ctx, client.ObjectKey{Namespace: constants.EksaSystemNamespace, Name: cluster.Name + "-kubeconfig"}, secret)).To(Succeed()) + pcc := newMockPackageControllerClient(t) + pcc.EXPECT().EnableCuratedPackagesFullLifecycle(ctx, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) validator := newMockClusterValidator(t) validator.EXPECT().ValidateManagementClusterName(ctx, gomock.AssignableToTypeOf(logr.Logger{}), gomock.AssignableToTypeOf(cluster)).Return(nil) - r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), validator) + r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), validator, pcc) _, err := r.Reconcile(ctx, clusterRequest(cluster)) g.Expect(err).ToNot(HaveOccurred()) @@ -250,7 +328,7 @@ func TestClusterReconcilerWorkloadClusterMgmtClusterNameFail(t *testing.T) { validator.EXPECT().ValidateManagementClusterName(ctx, gomock.AssignableToTypeOf(logr.Logger{}), gomock.AssignableToTypeOf(cluster)). Return(errors.New("test error")) - r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), validator) + r := controllers.NewClusterReconciler(cl, newRegistryForDummyProviderReconciler(), newMockAWSIamConfigReconciler(t), validator, nil) _, err := r.Reconcile(ctx, clusterRequest(cluster)) g.Expect(err).To(HaveOccurred()) } @@ -309,3 +387,8 @@ func newMockClusterValidator(t *testing.T) *mocks.MockClusterValidator { ctrl := gomock.NewController(t) return mocks.NewMockClusterValidator(ctrl) } + +func newMockPackageControllerClient(t *testing.T) *mocks.MockPackageControllerClient { + ctrl := gomock.NewController(t) + return mocks.NewMockPackageControllerClient(ctrl) +} diff --git a/controllers/factory.go b/controllers/factory.go index 37f8ed7d8e0ba..0abe8792f303e 100644 --- a/controllers/factory.go +++ b/controllers/factory.go @@ -13,7 +13,9 @@ import ( awsiamconfigreconciler "github.com/aws/eks-anywhere/pkg/awsiamauth/reconciler" "github.com/aws/eks-anywhere/pkg/controller/clusters" "github.com/aws/eks-anywhere/pkg/crypto" + "github.com/aws/eks-anywhere/pkg/curatedpackages" "github.com/aws/eks-anywhere/pkg/dependencies" + "github.com/aws/eks-anywhere/pkg/executables" ciliumreconciler "github.com/aws/eks-anywhere/pkg/networking/cilium/reconciler" cnireconciler "github.com/aws/eks-anywhere/pkg/networking/reconciler" dockerreconciler "github.com/aws/eks-anywhere/pkg/providers/docker/reconciler" @@ -42,6 +44,7 @@ type Factory struct { awsIamConfigReconciler *awsiamconfigreconciler.Reconciler logger logr.Logger deps *dependencies.Dependencies + packageControllerClient *curatedpackages.PackageControllerClient } type Reconcilers struct { @@ -89,7 +92,10 @@ func (f *Factory) Close(ctx context.Context) error { func (f *Factory) WithClusterReconciler(capiProviders []clusterctlv1.Provider) *Factory { f.dependencyFactory.WithGovc() - f.withTracker().WithProviderClusterReconcilerRegistry(capiProviders).withAWSIamConfigReconciler() + f.withTracker(). + WithProviderClusterReconcilerRegistry(capiProviders). + withAWSIamConfigReconciler(). + withPackageControllerClient() f.buildSteps = append(f.buildSteps, func(ctx context.Context) error { if f.reconcilers.ClusterReconciler != nil { @@ -101,6 +107,7 @@ func (f *Factory) WithClusterReconciler(capiProviders []clusterctlv1.Provider) * f.registry, f.awsIamConfigReconciler, clusters.NewClusterValidator(f.manager.GetClient()), + f.packageControllerClient, ) return nil @@ -385,3 +392,17 @@ func (f *Factory) withAWSIamConfigReconciler() *Factory { return f } + +func (f *Factory) withPackageControllerClient() *Factory { + f.dependencyFactory.WithHelm(executables.WithSkipCRDs()).WithKubectl() + + f.buildSteps = append(f.buildSteps, func(ctx context.Context) error { + if f.packageControllerClient != nil { + return nil + } + f.packageControllerClient = curatedpackages.NewPackageControllerClientFullLifecycle(f.logger, f.deps.Helm, f.deps.Helm, f.deps.Kubectl) + return nil + }) + + return f +} diff --git a/controllers/mocks/cluster_controller.go b/controllers/mocks/cluster_controller.go index 64d1e5f75d53b..72357030b1e7a 100644 --- a/controllers/mocks/cluster_controller.go +++ b/controllers/mocks/cluster_controller.go @@ -11,8 +11,12 @@ import ( v1alpha1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1" controller "github.com/aws/eks-anywhere/pkg/controller" clusters "github.com/aws/eks-anywhere/pkg/controller/clusters" + curatedpackages "github.com/aws/eks-anywhere/pkg/curatedpackages" + registrymirror "github.com/aws/eks-anywhere/pkg/registrymirror" + v1alpha10 "github.com/aws/eks-anywhere/release/api/v1alpha1" logr "github.com/go-logr/logr" gomock "github.com/golang/mock/gomock" + client "sigs.k8s.io/controller-runtime/pkg/client" ) // MockProviderClusterReconcilerRegistry is a mock of ProviderClusterReconcilerRegistry interface. @@ -155,3 +159,59 @@ func (mr *MockClusterValidatorMockRecorder) ValidateManagementClusterName(ctx, l mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateManagementClusterName", reflect.TypeOf((*MockClusterValidator)(nil).ValidateManagementClusterName), ctx, log, cluster) } + +// MockPackageControllerClient is a mock of PackageControllerClient interface. +type MockPackageControllerClient struct { + ctrl *gomock.Controller + recorder *MockPackageControllerClientMockRecorder +} + +// MockPackageControllerClientMockRecorder is the mock recorder for MockPackageControllerClient. +type MockPackageControllerClientMockRecorder struct { + mock *MockPackageControllerClient +} + +// NewMockPackageControllerClient creates a new mock instance. +func NewMockPackageControllerClient(ctrl *gomock.Controller) *MockPackageControllerClient { + mock := &MockPackageControllerClient{ctrl: ctrl} + mock.recorder = &MockPackageControllerClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPackageControllerClient) EXPECT() *MockPackageControllerClientMockRecorder { + return m.recorder +} + +// EnableCuratedPackagesFullLifecycle mocks base method. +func (m *MockPackageControllerClient) EnableCuratedPackagesFullLifecycle(arg0 context.Context, arg1 logr.Logger, arg2, arg3 string, arg4 *v1alpha10.Image, arg5 *registrymirror.RegistryMirror, arg6 ...curatedpackages.PackageControllerClientOpt) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2, arg3, arg4, arg5} + for _, a := range arg6 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "EnableCuratedPackagesFullLifecycle", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnableCuratedPackagesFullLifecycle indicates an expected call of EnableCuratedPackagesFullLifecycle. +func (mr *MockPackageControllerClientMockRecorder) EnableCuratedPackagesFullLifecycle(arg0, arg1, arg2, arg3, arg4, arg5 interface{}, arg6 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2, arg3, arg4, arg5}, arg6...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableCuratedPackagesFullLifecycle", reflect.TypeOf((*MockPackageControllerClient)(nil).EnableCuratedPackagesFullLifecycle), varargs...) +} + +// ReconcileDelete mocks base method. +func (m *MockPackageControllerClient) ReconcileDelete(arg0 context.Context, arg1 logr.Logger, arg2 client.Client, arg3 *v1alpha1.Cluster) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReconcileDelete", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// ReconcileDelete indicates an expected call of ReconcileDelete. +func (mr *MockPackageControllerClientMockRecorder) ReconcileDelete(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcileDelete", reflect.TypeOf((*MockPackageControllerClient)(nil).ReconcileDelete), arg0, arg1, arg2, arg3) +} diff --git a/pkg/curatedpackages/mocks/installer.go b/pkg/curatedpackages/mocks/installer.go index efcf494f3fc4e..334263b1b6d46 100644 --- a/pkg/curatedpackages/mocks/installer.go +++ b/pkg/curatedpackages/mocks/installer.go @@ -47,3 +47,40 @@ func (mr *MockChartInstallerMockRecorder) InstallChart(ctx, chart, ociURI, versi mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallChart", reflect.TypeOf((*MockChartInstaller)(nil).InstallChart), ctx, chart, ociURI, version, kubeconfigFilePath, namespace, valueFilePath, values) } + +// MockChartInstallationDeleter is a mock of ChartInstallationDeleter interface. +type MockChartInstallationDeleter struct { + ctrl *gomock.Controller + recorder *MockChartInstallationDeleterMockRecorder +} + +// MockChartInstallationDeleterMockRecorder is the mock recorder for MockChartInstallationDeleter. +type MockChartInstallationDeleterMockRecorder struct { + mock *MockChartInstallationDeleter +} + +// NewMockChartInstallationDeleter creates a new mock instance. +func NewMockChartInstallationDeleter(ctrl *gomock.Controller) *MockChartInstallationDeleter { + mock := &MockChartInstallationDeleter{ctrl: ctrl} + mock.recorder = &MockChartInstallationDeleterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockChartInstallationDeleter) EXPECT() *MockChartInstallationDeleterMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockChartInstallationDeleter) Delete(ctx context.Context, kubeconfigFilePath, installName, namespace string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, kubeconfigFilePath, installName, namespace) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockChartInstallationDeleterMockRecorder) Delete(ctx, kubeconfigFilePath, installName, namespace interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockChartInstallationDeleter)(nil).Delete), ctx, kubeconfigFilePath, installName, namespace) +} diff --git a/pkg/curatedpackages/packagecontrollerclient.go b/pkg/curatedpackages/packagecontrollerclient.go index e5831e90043f0..ed927c630bf25 100644 --- a/pkg/curatedpackages/packagecontrollerclient.go +++ b/pkg/curatedpackages/packagecontrollerclient.go @@ -8,7 +8,13 @@ import ( "strings" "time" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + packagesv1 "github.com/aws/eks-anywhere-packages/api/v1alpha1" + anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1" "github.com/aws/eks-anywhere/pkg/config" "github.com/aws/eks-anywhere/pkg/constants" "github.com/aws/eks-anywhere/pkg/filewriter" @@ -32,6 +38,7 @@ type PackageControllerClient struct { kubeConfig string chart *v1alpha1.Image chartInstaller ChartInstaller + deleter ChartInstallationDeleter clusterName string managementClusterName string kubectl KubectlRunner @@ -45,19 +52,78 @@ type PackageControllerClient struct { // activeBundleTimeout is the timeout to activate a bundle on installation. activeBundleTimeout time.Duration valuesFileWriter filewriter.FileWriter + // skipWaitForPackageBundle indicates whether the installer should wait + // until a package bundle is activated. + // + // Skipping the wait is desirable for full cluster lifecycle use cases, + // where resource creation and error reporting are asynchronous in nature. + skipWaitForPackageBundle bool } type ChartInstaller interface { InstallChart(ctx context.Context, chart, ociURI, version, kubeconfigFilePath, namespace, valueFilePath string, values []string) error } +// ChartInstallationDeleter handles deleting chart installations. +type ChartInstallationDeleter interface { + Delete(ctx context.Context, kubeconfigFilePath, installName, namespace string) error +} + +// NewPackageControllerClientFullLifecycle instantiates a new instance of +// PackageControllerClient for the Full Cluster Lifecycle controller. +// +// It differs because the CLI use case has far more information available at +// instantiation, while the FCL use case has some information at +// instantiation, and the rest when cluster creation is triggered. +func NewPackageControllerClientFullLifecycle(logger logr.Logger, chartInstaller ChartInstaller, deleter ChartInstallationDeleter, kubectl KubectlRunner) *PackageControllerClient { + return &PackageControllerClient{ + chartInstaller: chartInstaller, + deleter: deleter, + kubectl: kubectl, + skipWaitForPackageBundle: true, + } +} + +// EnableCuratedPackagesFullLifecycle receives additional run-time arguments. +// +// This method fills in the gaps between the original CLI use case, where all +// information is known at PackageControllerClient initialization, and the +// Full Cluster Lifecycle use case, where there's limited information at +// initialization. +func (pc *PackageControllerClient) EnableCuratedPackagesFullLifecycle(ctx context.Context, log logr.Logger, clusterName, kubeConfig string, chart *v1alpha1.Image, registryMirror *registrymirror.RegistryMirror, options ...PackageControllerClientOpt) (err error) { + log.V(1).Info("enabling curated package full lifecycle", "clusterName", clusterName) + defer func(err *error) { + if err != nil && *err != nil { + log.V(1).Info("error enabling curated packages full lifecycle", "error", *err, "clusterName", clusterName) + } else { + log.V(1).Info("success enabling curated packages full lifecycle") + } + }(&err) + pc.skipWaitForPackageBundle = true + pc.clusterName = clusterName + pc.kubeConfig = kubeConfig + pc.chart = chart + pc.registryMirror = registryMirror + writer, err := filewriter.NewWriter(clusterName) + if err != nil { + return fmt.Errorf("creating file writer for helm values: %w", err) + } + options = append(options, WithValuesFileWriter(writer)) + for _, o := range options { + o(pc) + } + + return pc.EnableCuratedPackages(ctx) +} + // NewPackageControllerClient instantiates a new instance of PackageControllerClient. -func NewPackageControllerClient(chartInstaller ChartInstaller, kubectl KubectlRunner, clusterName, kubeConfig string, chart *v1alpha1.Image, registryMirror *registrymirror.RegistryMirror, options ...PackageControllerClientOpt) *PackageControllerClient { +func NewPackageControllerClient(chartInstaller ChartInstaller, deleter ChartInstallationDeleter, kubectl KubectlRunner, clusterName, kubeConfig string, chart *v1alpha1.Image, registryMirror *registrymirror.RegistryMirror, options ...PackageControllerClientOpt) *PackageControllerClient { pcc := &PackageControllerClient{ kubeConfig: kubeConfig, clusterName: clusterName, chart: chart, chartInstaller: chartInstaller, + deleter: deleter, kubectl: kubectl, registryMirror: registryMirror, } @@ -111,11 +177,18 @@ func (pc *PackageControllerClient) EnableCuratedPackages(ctx context.Context) er chartName = chartName + "-" + pc.clusterName } + // TODO should this namespace be specified? + // namespace := "eksa-packages-" + pc.clusterName + if err := pc.chartInstaller.InstallChart(ctx, chartName, ociURI, pc.chart.Tag(), pc.kubeConfig, "", valueFilePath, values); err != nil { return err } - return pc.waitForActiveBundle(ctx) + if !pc.skipWaitForPackageBundle { + return pc.waitForActiveBundle(ctx) + } + + return nil } // GetCuratedPackagesRegistries gets value for configurable registries from PBC. @@ -274,6 +347,24 @@ func (pc *PackageControllerClient) IsInstalled(ctx context.Context) bool { return hasResource && err == nil } +// ReconcileDelete removes resources after a full cluster lifecycle cluster is deleted. +func (pc *PackageControllerClient) ReconcileDelete(ctx context.Context, logger logr.Logger, client client.Client, cluster *anywherev1.Cluster) error { + namespace := "eksa-packages-" + cluster.Name + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + if err := client.Delete(ctx, ns); err != nil { + return fmt.Errorf("deleting workload cluster curated packages namespace %q %w", namespace, err) + } + + name := "eks-anywhere-packages-" + pc.clusterName + if err := pc.deleter.Delete(ctx, pc.kubeConfig, name, ""); err != nil { + return err + } + + logger.V(6).Info("Removed curated packages installation", "clusterName", cluster.Name) + + return nil +} + func WithEksaAccessKeyId(eksaAccessKeyId string) func(client *PackageControllerClient) { return func(config *PackageControllerClient) { config.eksaAccessKeyID = eksaAccessKeyId diff --git a/pkg/curatedpackages/packagecontrollerclient_test.go b/pkg/curatedpackages/packagecontrollerclient_test.go index 70e25f73f699e..ec193104397b3 100644 --- a/pkg/curatedpackages/packagecontrollerclient_test.go +++ b/pkg/curatedpackages/packagecontrollerclient_test.go @@ -32,6 +32,7 @@ type packageControllerTest struct { ctx context.Context kubectl *mocks.MockKubectlRunner chartInstaller *mocks.MockChartInstaller + deleter *mocks.MockChartInstallationDeleter command *curatedpackages.PackageControllerClient clusterName string kubeConfig string @@ -51,6 +52,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { ctrl := gomock.NewController(t) k := mocks.NewMockKubectlRunner(ctrl) ci := mocks.NewMockChartInstaller(ctrl) + del := mocks.NewMockChartInstallationDeleter(ctrl) kubeConfig := "kubeconfig.kubeconfig" chart := &artifactsv1.Image{ Name: "test_controller", @@ -96,7 +98,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { kubectl: k, chartInstaller: ci, command: curatedpackages.NewPackageControllerClient( - ci, k, clusterName, kubeConfig, chart, registryMirror, + ci, del, k, clusterName, kubeConfig, chart, registryMirror, curatedpackages.WithEksaSecretAccessKey(eksaAccessKey), curatedpackages.WithEksaRegion(eksaRegion), curatedpackages.WithEksaAccessKeyId(eksaAccessId), @@ -122,7 +124,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { kubectl: k, chartInstaller: ci, command: curatedpackages.NewPackageControllerClient( - ci, k, clusterName, kubeConfig, chart, + ci, del, k, clusterName, kubeConfig, chart, nil, curatedpackages.WithEksaSecretAccessKey(eksaAccessKey), curatedpackages.WithEksaRegion(eksaRegion), @@ -149,7 +151,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { kubectl: k, chartInstaller: ci, command: curatedpackages.NewPackageControllerClient( - ci, k, clusterName, kubeConfig, chartDev, + ci, del, k, clusterName, kubeConfig, chartDev, nil, curatedpackages.WithEksaSecretAccessKey(eksaAccessKey), curatedpackages.WithEksaRegion(eksaRegion), @@ -176,7 +178,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { kubectl: k, chartInstaller: ci, command: curatedpackages.NewPackageControllerClient( - ci, k, clusterName, kubeConfig, chartStaging, + ci, del, k, clusterName, kubeConfig, chartStaging, nil, curatedpackages.WithEksaSecretAccessKey(eksaAccessKey), curatedpackages.WithEksaRegion(eksaRegion), @@ -203,7 +205,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { kubectl: k, chartInstaller: ci, command: curatedpackages.NewPackageControllerClient( - ci, k, clusterName, kubeConfig, chart, registryMirrorInsecure, + ci, del, k, clusterName, kubeConfig, chart, registryMirrorInsecure, curatedpackages.WithManagementClusterName(clusterName), curatedpackages.WithValuesFileWriter(writer), ), @@ -226,7 +228,7 @@ func newPackageControllerTests(t *testing.T) []*packageControllerTest { kubectl: k, chartInstaller: ci, command: curatedpackages.NewPackageControllerClient( - ci, k, clusterName, kubeConfig, chart, nil, + ci, del, k, clusterName, kubeConfig, chart, nil, curatedpackages.WithManagementClusterName(clusterName), curatedpackages.WithValuesFileWriter(writer), ), @@ -283,7 +285,7 @@ func TestEnableCuratedPackagesSuccess(t *testing.T) { func TestEnableCuratedPackagesSucceedInWorkloadCluster(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, tt.clusterName, tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, tt.clusterName, tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion("us-west-2"), @@ -343,7 +345,7 @@ func getPBCFail(t *testing.T) func(context.Context, string, string, string, stri func TestEnableCuratedPackagesWithProxy(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion(tt.eksaRegion), @@ -397,7 +399,7 @@ func TestEnableCuratedPackagesWithProxy(t *testing.T) { func TestEnableCuratedPackagesWithEmptyProxy(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion(tt.eksaRegion), @@ -566,7 +568,7 @@ func TestIsInstalledFalse(t *testing.T) { func TestEnableCuratedPackagesActiveBundleCustomTimeout(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion(tt.eksaRegion), @@ -665,7 +667,7 @@ func getPBCLoops(t *testing.T, loops int) func(context.Context, string, string, func TestEnableCuratedPackagesActiveBundleTimesOut(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion(tt.eksaRegion), @@ -711,7 +713,7 @@ func TestEnableCuratedPackagesActiveBundleTimesOut(t *testing.T) { func TestEnableCuratedPackagesActiveBundleNamespaceTimesOut(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion(tt.eksaRegion), @@ -797,7 +799,7 @@ func TestCreateHelmOverrideValuesYamlFail(t *testing.T) { func TestCreateHelmOverrideValuesYamlFailWithNoWriter(t *testing.T) { for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithEksaSecretAccessKey(tt.eksaAccessKey), curatedpackages.WithEksaRegion(tt.eksaRegion), @@ -823,7 +825,7 @@ func TestCreateHelmOverrideValuesYamlFailWithWriteError(t *testing.T) { writer := writermocks.NewMockFileWriter(ctrl) for _, tt := range newPackageControllerTests(t) { tt.command = curatedpackages.NewPackageControllerClient( - tt.chartInstaller, tt.kubectl, "billy", tt.kubeConfig, tt.chart, + tt.chartInstaller, tt.deleter, tt.kubectl, "billy", tt.kubeConfig, tt.chart, tt.registryMirror, curatedpackages.WithValuesFileWriter(writer), ) diff --git a/pkg/dependencies/factory.go b/pkg/dependencies/factory.go index 5ee6fc866ed00..f4f06e4ef59c1 100644 --- a/pkg/dependencies/factory.go +++ b/pkg/dependencies/factory.go @@ -996,6 +996,7 @@ func (f *Factory) WithPackageControllerClient(spec *cluster.Spec, kubeConfig str return err } f.dependencies.PackageControllerClient = curatedpackages.NewPackageControllerClient( + f.dependencies.Helm, f.dependencies.Helm, f.dependencies.Kubectl, spec.Cluster.Name, diff --git a/pkg/executables/helm.go b/pkg/executables/helm.go index c2f0bd2c29462..a422c5cb6a4c1 100644 --- a/pkg/executables/helm.go +++ b/pkg/executables/helm.go @@ -21,6 +21,8 @@ type Helm struct { registryMirror *registrymirror.RegistryMirror env map[string]string insecure bool + // skipCRDs passes the --skip-crds flag to the helm executable. + skipCRDs bool } type HelmOpt func(*Helm) @@ -32,6 +34,16 @@ func WithRegistryMirror(mirror *registrymirror.RegistryMirror) HelmOpt { } } +// WithSkipCRDs configures helm to skip the creation of CRDs when installing a +// chart. +func WithSkipCRDs() HelmOpt { + return func(h *Helm) { + h.skipCRDs = true + } +} + +// WithInsecure configures helm to skip validating TLS certificates when +// communicating with the Kubernetes API. func WithInsecure() HelmOpt { return func(h *Helm) { h.insecure = true @@ -127,6 +139,9 @@ func (h *Helm) InstallChartFromName(ctx context.Context, ociURI, kubeConfig, nam func (h *Helm) InstallChart(ctx context.Context, chart, ociURI, version, kubeconfigFilePath, namespace, valueFilePath string, values []string) error { valueArgs := GetHelmValueArgs(values) params := []string{"upgrade", "--install", chart, ociURI, "--version", version} + if h.skipCRDs { + params = append(params, "--skip-crds") + } params = append(params, valueArgs...) params = append(params, "--kubeconfig", kubeconfigFilePath) if len(namespace) > 0 { @@ -151,6 +166,22 @@ func (h *Helm) InstallChartWithValuesFile(ctx context.Context, chart, ociURI, ve return err } +// Delete removes an installation. +func (h *Helm) Delete(ctx context.Context, kubeconfigFilePath, installName, namespace string) error { + params := []string{ + "delete", installName, + "--kubeconfig", kubeconfigFilePath, + "--namespace", namespace, + } + params = h.addInsecureFlagIfProvided(params) + if _, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run(); err != nil { + return fmt.Errorf("deleting helm installation %w", err) + } + logger.V(6).Info("Deleted helm installation", "name", installName, "namespace", namespace) + + return nil +} + func (h *Helm) ListCharts(ctx context.Context, kubeconfigFilePath string) ([]string, error) { params := []string{"list", "-q", "--kubeconfig", kubeconfigFilePath} out, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run()