diff --git a/cmd/clusteradm/clusteradm.go b/cmd/clusteradm/clusteradm.go index ba0799a15..a5e2058cf 100644 --- a/cmd/clusteradm/clusteradm.go +++ b/cmd/clusteradm/clusteradm.go @@ -35,10 +35,11 @@ import ( deletecmd "open-cluster-management.io/clusteradm/pkg/cmd/delete" "open-cluster-management.io/clusteradm/pkg/cmd/get" inithub "open-cluster-management.io/clusteradm/pkg/cmd/init" - install "open-cluster-management.io/clusteradm/pkg/cmd/install" + "open-cluster-management.io/clusteradm/pkg/cmd/install" joinhub "open-cluster-management.io/clusteradm/pkg/cmd/join" "open-cluster-management.io/clusteradm/pkg/cmd/proxy" - unjoin "open-cluster-management.io/clusteradm/pkg/cmd/unjoin" + "open-cluster-management.io/clusteradm/pkg/cmd/uninstall" + "open-cluster-management.io/clusteradm/pkg/cmd/unjoin" "open-cluster-management.io/clusteradm/pkg/cmd/upgrade" "open-cluster-management.io/clusteradm/pkg/cmd/version" ) @@ -96,6 +97,7 @@ func main() { deletecmd.NewCmd(clusteradmFlags, streams), get.NewCmd(clusteradmFlags, streams), install.NewCmd(clusteradmFlags, streams), + uninstall.NewCmd(clusteradmFlags, streams), upgrade.NewCmd(clusteradmFlags, streams), version.NewCmd(clusteradmFlags, streams), }, diff --git a/pkg/cmd/install/cmd.go b/pkg/cmd/install/cmd.go index a64ba30e6..2eece412f 100644 --- a/pkg/cmd/install/cmd.go +++ b/pkg/cmd/install/cmd.go @@ -8,7 +8,7 @@ import ( genericclioptionsclusteradm "open-cluster-management.io/clusteradm/pkg/genericclioptions" ) -// NewCmd provides a cobra command wrapping NewCmdImportCluster +// NewCmd provides a cobra command wrapping addon install cmd func NewCmd(clusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags, streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "install", diff --git a/pkg/cmd/install/hubaddon/exec.go b/pkg/cmd/install/hubaddon/exec.go index 8970649e4..c4f643a74 100644 --- a/pkg/cmd/install/hubaddon/exec.go +++ b/pkg/cmd/install/hubaddon/exec.go @@ -15,11 +15,6 @@ import ( "open-cluster-management.io/clusteradm/pkg/version" ) -const ( - appMgrAddonName = "application-manager" - policyFrameworkAddonName = "governance-policy-framework" -) - func (o *Options) complete(cmd *cobra.Command, args []string) (err error) { klog.V(1).InfoS("addon options:", "dry-run", o.ClusteradmFlags.DryRun, "names", o.names, "output-file", o.outputFile) return nil @@ -37,12 +32,7 @@ func (o *Options) validate() (err error) { names := strings.Split(o.names, ",") for _, n := range names { - switch n { - case appMgrAddonName: - continue - case policyFrameworkAddonName: - continue - default: + if _, ok := scenario.AddonDeploymentFiles[n]; !ok { return fmt.Errorf("invalid add-on name %s", n) } } @@ -67,9 +57,9 @@ func (o *Options) run() error { addons = append(addons, strings.TrimSpace(n)) } } - o.values.hubAddons = addons + o.values.HubAddons = addons - klog.V(3).InfoS("values:", "addon", o.values.hubAddons) + klog.V(3).InfoS("values:", "addon", o.values.HubAddons) return o.runWithClient() } @@ -78,86 +68,26 @@ func (o *Options) runWithClient() error { r := reader.NewResourceReader(o.ClusteradmFlags.KubectlFactory, o.ClusteradmFlags.DryRun, o.Streams) - for _, addon := range o.values.hubAddons { - switch addon { - // Install the Application Management Addon - case appMgrAddonName: - files := []string{ - "addon/appmgr/clustermanagementaddon_appmgr.yaml", - "addon/appmgr/clusterrole_agent.yaml", - "addon/appmgr/clusterrole_binding.yaml", - "addon/appmgr/clusterrole.yaml", - "addon/appmgr/crd_channel.yaml", - "addon/appmgr/crd_helmrelease.yaml", - "addon/appmgr/crd_placementrule.yaml", - "addon/appmgr/crd_subscription.yaml", - "addon/appmgr/crd_subscriptionstatuses.yaml", - "addon/appmgr/crd_report.yaml", - "addon/appmgr/crd_clusterreport.yaml", - "addon/appmgr/service_account.yaml", - "addon/appmgr/service_metrics.yaml", - "addon/appmgr/service_operator.yaml", - "addon/appmgr/mutatingwebhookconfiguration.yaml", - } - - err := r.Apply(scenario.Files, o.values, files...) - if err != nil { - return err - } - - deployments := []string{ - "addon/appmgr/deployment_channel.yaml", - "addon/appmgr/deployment_subscription.yaml", - "addon/appmgr/deployment_placementrule.yaml", - "addon/appmgr/deployment_appsubsummary.yaml", - } - err = r.Apply(scenario.Files, o.values, deployments...) - if err != nil { - return err - } - - fmt.Fprintf(o.Streams.Out, "Installing built-in %s add-on to the Hub cluster...\n", appMgrAddonName) - - // Install the Policy Framework Addon - case policyFrameworkAddonName: - files := []string{ - "addon/policy/addon-controller_clusterrole.yaml", - "addon/policy/addon-controller_clusterrolebinding.yaml", - "addon/policy/addon-controller_role.yaml", - "addon/policy/addon-controller_rolebinding.yaml", - "addon/policy/addon-controller_serviceaccount.yaml", - "addon/policy/policy.open-cluster-management.io_placementbindings.yaml", - "addon/policy/policy.open-cluster-management.io_policies.yaml", - "addon/policy/policy.open-cluster-management.io_policyautomations.yaml", - "addon/policy/policy.open-cluster-management.io_policysets.yaml", - "addon/policy/propagator_clusterrole.yaml", - "addon/policy/propagator_clusterrolebinding.yaml", - "addon/policy/propagator_role.yaml", - "addon/policy/propagator_rolebinding.yaml", - "addon/policy/propagator_service.yaml", - "addon/policy/propagator_serviceaccount.yaml", - "addon/policy/clustermanagementaddon_configpolicy.yaml", - "addon/policy/clustermanagementaddon_policyframework.yaml", - "addon/appmgr/crd_placementrule.yaml", - } - - err := r.Apply(scenario.Files, o.values, files...) - if err != nil { - return fmt.Errorf("Error deploying framework deployment dependencies: %w", err) - } - - deployments := []string{ - "addon/policy/addon-controller_deployment.yaml", - "addon/policy/propagator_deployment.yaml", - } - - err = r.Apply(scenario.Files, o.values, deployments...) - if err != nil { - return fmt.Errorf("Error deploying framework deployments: %w", err) - } - - fmt.Fprintf(o.Streams.Out, "Installing built-in %s add-on to the Hub cluster...\n", policyFrameworkAddonName) + for _, addon := range o.values.HubAddons { + files, ok := scenario.AddonDeploymentFiles[addon] + if !ok { + continue + } + err := r.Apply(scenario.Files, o.values, files.CRDFiles...) + if err != nil { + return fmt.Errorf("Error deploying %s CRDs: %w", addon, err) } + err = r.Apply(scenario.Files, o.values, files.ConfigFiles...) + if err != nil { + return fmt.Errorf("Error deploying %s dependencies: %w", addon, err) + } + err = r.Apply(scenario.Files, o.values, files.DeploymentFiles...) + if err != nil { + return fmt.Errorf("Error deploying %s deployments: %w", addon, err) + } + + fmt.Fprintf(o.Streams.Out, "Installing built-in %s add-on to the Hub cluster...\n", addon) + } if len(o.outputFile) > 0 { diff --git a/pkg/cmd/install/hubaddon/exec_test.go b/pkg/cmd/install/hubaddon/exec_test.go index 96b5168c1..d20085334 100644 --- a/pkg/cmd/install/hubaddon/exec_test.go +++ b/pkg/cmd/install/hubaddon/exec_test.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + "open-cluster-management.io/clusteradm/pkg/cmd/install/hubaddon/scenario" "open-cluster-management.io/clusteradm/pkg/version" ) @@ -76,8 +77,8 @@ var _ = ginkgo.Describe("install hub-addon", func() { ginkgo.It("Should not create any built-in add-on deployment(s) because it's not a valid add-on name", func() { o := Options{ ClusteradmFlags: clusteradmFlags, - values: Values{ - hubAddons: []string{invalidAddon}, + values: scenario.Values{ + HubAddons: []string{invalidAddon}, }, } @@ -98,9 +99,9 @@ var _ = ginkgo.Describe("install hub-addon", func() { o := Options{ ClusteradmFlags: clusteradmFlags, bundleVersion: ocmVersion, - values: Values{ + values: scenario.Values{ Namespace: invalidNamespace, - hubAddons: []string{appMgrAddonName}, + HubAddons: []string{scenario.AppMgrAddonName}, }, Streams: genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr}, } @@ -122,9 +123,9 @@ var _ = ginkgo.Describe("install hub-addon", func() { o := Options{ ClusteradmFlags: clusteradmFlags, bundleVersion: ocmVersion, - values: Values{ + values: scenario.Values{ Namespace: ocmNamespace, - hubAddons: []string{hubAddon}, + HubAddons: []string{hubAddon}, BundleVersion: ocmBundleVersion, }, Streams: genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr}, diff --git a/pkg/cmd/install/hubaddon/options.go b/pkg/cmd/install/hubaddon/options.go index bbc2deec1..d3005353f 100644 --- a/pkg/cmd/install/hubaddon/options.go +++ b/pkg/cmd/install/hubaddon/options.go @@ -3,8 +3,8 @@ package hubaddon import ( "k8s.io/cli-runtime/pkg/genericclioptions" + "open-cluster-management.io/clusteradm/pkg/cmd/install/hubaddon/scenario" genericclioptionsclusteradm "open-cluster-management.io/clusteradm/pkg/genericclioptions" - "open-cluster-management.io/clusteradm/pkg/version" ) type Options struct { @@ -14,21 +14,12 @@ type Options struct { names string //The file to output the resources will be sent to the file. outputFile string - values Values + values scenario.Values bundleVersion string Streams genericclioptions.IOStreams } -// Values: The values used in the template -type Values struct { - hubAddons []string - // Namespace to install - Namespace string - // Version to install - BundleVersion version.VersionBundle -} - func newOptions(clusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags, streams genericclioptions.IOStreams) *Options { return &Options{ ClusteradmFlags: clusteradmFlags, diff --git a/pkg/cmd/install/hubaddon/scenario/resources.go b/pkg/cmd/install/hubaddon/scenario/resources.go index 7988f0fa5..79f9e2fd2 100644 --- a/pkg/cmd/install/hubaddon/scenario/resources.go +++ b/pkg/cmd/install/hubaddon/scenario/resources.go @@ -3,7 +3,89 @@ package scenario import ( "embed" + + "open-cluster-management.io/clusteradm/pkg/version" ) //go:embed addon var Files embed.FS + +const ( + AppMgrAddonName = "application-manager" + PolicyFrameworkAddonName = "governance-policy-framework" +) + +type AddonDeploymentFile struct { + ConfigFiles []string + DeploymentFiles []string + CRDFiles []string +} + +// Values: The values used in the template +type Values struct { + HubAddons []string + // Namespace to install + Namespace string + // Version to install + BundleVersion version.VersionBundle +} + +var ( + AddonDeploymentFiles = map[string]AddonDeploymentFile{ + PolicyFrameworkAddonName: { + ConfigFiles: []string{ + "addon/policy/addon-controller_clusterrole.yaml", + "addon/policy/addon-controller_clusterrolebinding.yaml", + "addon/policy/addon-controller_role.yaml", + "addon/policy/addon-controller_rolebinding.yaml", + "addon/policy/addon-controller_serviceaccount.yaml", + "addon/policy/propagator_clusterrole.yaml", + "addon/policy/propagator_clusterrolebinding.yaml", + "addon/policy/propagator_role.yaml", + "addon/policy/propagator_rolebinding.yaml", + "addon/policy/propagator_service.yaml", + "addon/policy/propagator_serviceaccount.yaml", + "addon/policy/clustermanagementaddon_configpolicy.yaml", + "addon/policy/clustermanagementaddon_policyframework.yaml", + }, + CRDFiles: []string{ + "addon/policy/policy.open-cluster-management.io_placementbindings.yaml", + "addon/policy/policy.open-cluster-management.io_policies.yaml", + "addon/policy/policy.open-cluster-management.io_policyautomations.yaml", + "addon/policy/policy.open-cluster-management.io_policysets.yaml", + "addon/appmgr/crd_placementrule.yaml", + }, + DeploymentFiles: []string{ + "addon/policy/addon-controller_deployment.yaml", + "addon/policy/propagator_deployment.yaml", + }, + }, + AppMgrAddonName: { + ConfigFiles: []string{ + "addon/appmgr/clustermanagementaddon_appmgr.yaml", + "addon/appmgr/clusterrole_agent.yaml", + "addon/appmgr/clusterrole_binding.yaml", + "addon/appmgr/clusterrole.yaml", + "addon/appmgr/service_account.yaml", + "addon/appmgr/service_metrics.yaml", + "addon/appmgr/service_operator.yaml", + "addon/appmgr/mutatingwebhookconfiguration.yaml", + }, + CRDFiles: []string{ + "addon/appmgr/crd_channel.yaml", + "addon/appmgr/crd_helmrelease.yaml", + "addon/appmgr/crd_placementrule.yaml", + "addon/appmgr/crd_subscription.yaml", + "addon/appmgr/crd_subscriptionstatuses.yaml", + "addon/appmgr/crd_report.yaml", + "addon/appmgr/crd_clusterreport.yaml", + }, + DeploymentFiles: []string{ + "addon/appmgr/deployment_channel.yaml", + "addon/appmgr/deployment_subscription.yaml", + "addon/appmgr/deployment_placementrule.yaml", + "addon/appmgr/deployment_appsubsummary.yaml", + }, + }, + } +) diff --git a/pkg/cmd/uninstall/cmd.go b/pkg/cmd/uninstall/cmd.go new file mode 100644 index 000000000..f42f4f724 --- /dev/null +++ b/pkg/cmd/uninstall/cmd.go @@ -0,0 +1,21 @@ +// Copyright Contributors to the Open Cluster Management project +package uninstall + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "open-cluster-management.io/clusteradm/pkg/cmd/uninstall/hubaddon" + genericclioptionsclusteradm "open-cluster-management.io/clusteradm/pkg/genericclioptions" +) + +// NewCmd provides a cobra command wrapping addon uninstall cmd +func NewCmd(clusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "uninstall", + Short: "uninstall a feature", + } + + cmd.AddCommand(hubaddon.NewCmd(clusteradmFlags, streams)) + + return cmd +} diff --git a/pkg/cmd/uninstall/hubaddon/cmd.go b/pkg/cmd/uninstall/hubaddon/cmd.go new file mode 100644 index 000000000..14ee024e5 --- /dev/null +++ b/pkg/cmd/uninstall/hubaddon/cmd.go @@ -0,0 +1,54 @@ +// Copyright Contributors to the Open Cluster Management project +package hubaddon + +import ( + "fmt" + + genericclioptionsclusteradm "open-cluster-management.io/clusteradm/pkg/genericclioptions" + clusteradmhelpers "open-cluster-management.io/clusteradm/pkg/helpers" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +var example = ` +# Uninstall built-in add-ons from the hub cluster +%[1]s uninstall hub-addon --names application-manager +%[1]s uninstall hub-addon --names governance-policy-framework +` + +// NewCmd... +func NewCmd(clusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags, streams genericclioptions.IOStreams) *cobra.Command { + o := newOptions(clusteradmFlags, streams) + + cmd := &cobra.Command{ + Use: "hub-addon", + Short: "uninstall hub-addon", + Long: "Uninstall specific built-in add-on(s) to the hub cluster", + Example: fmt.Sprintf(example, clusteradmhelpers.GetExampleHeader()), + SilenceUsage: true, + PreRunE: func(c *cobra.Command, args []string) error { + clusteradmhelpers.DryRunMessage(clusteradmFlags.DryRun) + + return nil + }, + RunE: func(c *cobra.Command, args []string) error { + if err := o.complete(c, args); err != nil { + return err + } + if err := o.validate(); err != nil { + return err + } + if err := o.run(); err != nil { + return err + } + + return nil + }, + } + + cmd.Flags().StringVar(&o.names, "names", "", "Names of the built-in add-on to uninstall (comma separated). The built-in add-ons are: application-manager, governance-policy-framework") + cmd.Flags().StringVar(&o.values.Namespace, "namespace", "open-cluster-management", "Namespace of the built-in add-on to uninstall. Defaults to open-cluster-management") + + return cmd +} diff --git a/pkg/cmd/uninstall/hubaddon/exec.go b/pkg/cmd/uninstall/hubaddon/exec.go new file mode 100644 index 000000000..da8a9ab58 --- /dev/null +++ b/pkg/cmd/uninstall/hubaddon/exec.go @@ -0,0 +1,121 @@ +// Copyright Contributors to the Open Cluster Management project +package hubaddon + +import ( + "context" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + addonclientset "open-cluster-management.io/api/client/addon/clientset/versioned" + "open-cluster-management.io/clusteradm/pkg/helpers/reader" + "open-cluster-management.io/clusteradm/pkg/version" + + "github.com/spf13/cobra" + "k8s.io/klog/v2" + + "open-cluster-management.io/clusteradm/pkg/cmd/install/hubaddon/scenario" +) + +func (o *Options) complete(cmd *cobra.Command, args []string) (err error) { + klog.V(1).InfoS("addon options:", "dry-run", o.ClusteradmFlags.DryRun, "names", o.names) + return nil +} + +func (o *Options) validate() (err error) { + err = o.ClusteradmFlags.ValidateHub() + if err != nil { + return err + } + + if o.names == "" { + return fmt.Errorf("names is missing") + } + + names := strings.Split(o.names, ",") + for _, n := range names { + if _, ok := scenario.AddonDeploymentFiles[n]; !ok { + return fmt.Errorf("invalid add-on name %s", n) + } + } + + return nil +} + +func (o *Options) run() error { + alreadyProvidedAddons := make(map[string]bool) + addons := make([]string, 0) + names := strings.Split(o.names, ",") + for _, n := range names { + if _, ok := alreadyProvidedAddons[n]; !ok { + alreadyProvidedAddons[n] = true + addons = append(addons, strings.TrimSpace(n)) + } + } + o.values.HubAddons = addons + // this needs to be set to render the manifests, but the version value + // does not matter. + o.values.BundleVersion, _ = version.GetVersionBundle("default") + + klog.V(3).InfoS("values:", "addon", o.values.HubAddons) + + return o.runWithClient() +} + +func (o *Options) runWithClient() error { + + r := reader.NewResourceReader(o.ClusteradmFlags.KubectlFactory, o.ClusteradmFlags.DryRun, o.Streams) + + for _, addon := range o.values.HubAddons { + if err := o.checkExistingAddon(addon); err != nil { + return err + } + files, ok := scenario.AddonDeploymentFiles[addon] + if !ok { + continue + } + + err := r.Delete(scenario.Files, o.values, files.ConfigFiles...) + if err != nil { + return err + } + + err = r.Delete(scenario.Files, o.values, files.DeploymentFiles...) + if err != nil { + return err + } + + fmt.Fprintf(o.Streams.Out, "Uninstalling built-in %s add-on from the Hub cluster...\n", addon) + } + + return nil +} + +func (o *Options) checkExistingAddon(name string) error { + restConfig, err := o.ClusteradmFlags.KubectlFactory.ToRESTConfig() + if err != nil { + return err + } + + addonClient, err := addonclientset.NewForConfig(restConfig) + if err != nil { + return err + } + + addons, err := addonClient.AddonV1alpha1().ManagedClusterAddOns(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", name), + }) + if err != nil { + return err + } + + if len(addons.Items) > 0 { + var enabledClusters []string + for _, addon := range addons.Items { + enabledClusters = append(enabledClusters, addon.Namespace) + } + return fmt.Errorf("there are still addons for %s enabled on some clusters, run `cluster addon disable --names %s "+ + "--clusters %s` to disable addons", name, name, strings.Join(enabledClusters, ",")) + } + return nil +} diff --git a/pkg/cmd/uninstall/hubaddon/options.go b/pkg/cmd/uninstall/hubaddon/options.go new file mode 100644 index 000000000..87bf2d8eb --- /dev/null +++ b/pkg/cmd/uninstall/hubaddon/options.go @@ -0,0 +1,26 @@ +// Copyright Contributors to the Open Cluster Management project +package hubaddon + +import ( + "k8s.io/cli-runtime/pkg/genericclioptions" + "open-cluster-management.io/clusteradm/pkg/cmd/install/hubaddon/scenario" + genericclioptionsclusteradm "open-cluster-management.io/clusteradm/pkg/genericclioptions" +) + +type Options struct { + //ClusteradmFlags: The generic options from the clusteradm cli-runtime. + ClusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags + //A list of comma separated addon names + names string + //The file to output the resources will be sent to the file. + values scenario.Values + + Streams genericclioptions.IOStreams +} + +func newOptions(clusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags, streams genericclioptions.IOStreams) *Options { + return &Options{ + ClusteradmFlags: clusteradmFlags, + Streams: streams, + } +} diff --git a/pkg/helpers/reader/reader.go b/pkg/helpers/reader/reader.go index 3b60a5e22..a57f7b376 100644 --- a/pkg/helpers/reader/reader.go +++ b/pkg/helpers/reader/reader.go @@ -135,6 +135,46 @@ func (r *ResourceReader) applyOneObject(info *resource.Info) error { return nil } +func (r *ResourceReader) Delete(fs embed.FS, config interface{}, files ...string) error { + rawObjects := []byte{} + for _, file := range files { + template, err := fs.ReadFile(file) + if err != nil { + return err + } + objData := assets.MustCreateAssetFromTemplate(file, template, config).Data + rawObjects = append(rawObjects, objData...) + rawObjects = append(rawObjects, []byte(yamlSeparator)...) + } + + rb := r.builder. + Stream(bytes.NewReader(rawObjects), "local"). + Flatten(). + Do() + infos, err := rb.Infos() + if err != nil { + return err + } + + var errs []error + for _, object := range infos { + if err := r.deleteOneObject(object); err != nil { + errs = append(errs, err) + } + } + return utilerrors.NewAggregate(errs) +} + +func (r *ResourceReader) deleteOneObject(info *resource.Info) error { + helper := resource.NewHelper(info.Client, info.Mapping). + DryRun(r.dryRun) + _, err := helper.Delete(info.Namespace, info.Name) + if errors.IsNotFound(err) { + return nil + } + return err +} + func newPatcher(info *resource.Info, helper *resource.Helper, f cmdutil.Factory) *apply.Patcher { return &apply.Patcher{ Mapping: info.Mapping,