diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index f96d31b86..0899dade4 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -842,7 +842,7 @@ func renderCUEView(elem ElementFile) (*unstructured.Unstructured, error) { return util.Object2Unstructured(*cm) } -// RenderArgsSecret render addon enable argument to secret +// RenderArgsSecret render addon enable argument to secret to remember when restart or upgrade func RenderArgsSecret(addon *InstallPackage, args map[string]interface{}) *unstructured.Unstructured { argsByte, err := json.Marshal(args) if err != nil { @@ -899,19 +899,23 @@ type Installer struct { dc *discovery.DiscoveryClient skipVersionValidate bool overrideDefs bool + + dryRun bool + dryRunBuff *bytes.Buffer } // NewAddonInstaller will create an installer for addon func NewAddonInstaller(ctx context.Context, cli client.Client, discoveryClient *discovery.DiscoveryClient, apply apply.Applicator, config *rest.Config, r *Registry, args map[string]interface{}, cache *Cache, opts ...InstallOption) Installer { i := Installer{ - ctx: ctx, - config: config, - cli: cli, - apply: apply, - r: r, - args: args, - cache: cache, - dc: discoveryClient, + ctx: ctx, + config: config, + cli: cli, + apply: apply, + r: r, + args: args, + cache: cache, + dc: discoveryClient, + dryRunBuff: &bytes.Buffer{}, } for _, opt := range opts { opt(&i) @@ -995,6 +999,7 @@ func (h *Installer) getAddonMeta() (map[string]SourceMeta, error) { // installDependency checks if addon's dependency and install it func (h *Installer) installDependency(addon *InstallPackage) error { + var dependencies []string for _, dep := range addon.Dependencies { _, err := FetchAddonRelatedApp(h.ctx, h.cli, dep.Name) if err == nil { @@ -1003,6 +1008,10 @@ func (h *Installer) installDependency(addon *InstallPackage) error { if !apierrors.IsNotFound(err) { return err } + dependencies = append(dependencies, dep.Name) + if h.dryRun { + continue + } // always install addon's latest version depAddon, err := h.loadInstallPackage(dep.Name, "") if err != nil { @@ -1014,6 +1023,10 @@ func (h *Installer) installDependency(addon *InstallPackage) error { return errors.Wrap(err, "fail to dispatch dependent addon resource") } } + if h.dryRun && len(dependencies) > 0 { + klog.Warningf("dry run addon won't install dependencies, please make sure your system has already installed these addons: %v", strings.Join(dependencies, ", ")) + return nil + } return nil } @@ -1098,43 +1111,40 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { if err := passDefInAppAnnotation(defs, app); err != nil { return errors.Wrapf(err, "cannot pass definition to addon app's annotation") } - - if err = h.createOrUpdate(app); err != nil { - return err - } - - for _, def := range defs { - if !checkBondComponentExist(*def, *app) { - continue + if h.dryRun { + result, err := yaml.Marshal(app) + if err != nil { + return errors.Wrapf(err, "dry-run marshal app into yaml %s", app.Name) } - // if binding component exist, apply the definition - addOwner(def, app) - err = h.apply.Apply(h.ctx, def, apply.DisableUpdateAnnotation()) + h.dryRunBuff.Write(result) + h.dryRunBuff.WriteString("\n") + } else { + err = h.createOrUpdate(app) if err != nil { return err } } - for _, schema := range schemas { - addOwner(schema, app) - err = h.apply.Apply(h.ctx, schema, apply.DisableUpdateAnnotation()) - if err != nil { - return err - } - } - - for _, view := range views { - addOwner(view, app) - err = h.apply.Apply(h.ctx, view, apply.DisableUpdateAnnotation()) - if err != nil { - return err - } - } + auxiliaryOutputs = append(auxiliaryOutputs, defs...) + auxiliaryOutputs = append(auxiliaryOutputs, schemas...) + auxiliaryOutputs = append(auxiliaryOutputs, views...) for _, o := range auxiliaryOutputs { + // bind-component means the content is related with the component + // if component not exists, the resources shouldn't be applied if !checkBondComponentExist(*o, *app) { continue } + if h.dryRun { + result, err := yaml.Marshal(o) + if err != nil { + return errors.Wrapf(err, "dry-run marshal auxiliary object into yaml %s", o.GetName()) + } + h.dryRunBuff.WriteString("---\n") + h.dryRunBuff.Write(result) + h.dryRunBuff.WriteString("\n") + continue + } addOwner(o, app) err = h.apply.Apply(h.ctx, o, apply.DisableUpdateAnnotation()) if err != nil { @@ -1142,6 +1152,11 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { } } + if h.dryRun { + fmt.Print(h.dryRunBuff.String()) + return nil + } + if h.args != nil && len(h.args) > 0 { sec := RenderArgsSecret(addon, h.args) addOwner(sec, app) @@ -1157,6 +1172,9 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { // 1. if last apply failed an workflow have suspend, this func will continue the workflow // 2. restart the workflow, if the new cluster have been added in KubeVela func (h *Installer) continueOrRestartWorkflow() error { + if h.dryRun { + return nil + } app, err := FetchAddonRelatedApp(h.ctx, h.cli, h.addon.Name) if err != nil { return err diff --git a/pkg/addon/addon_suite_test.go b/pkg/addon/addon_suite_test.go index 25c35bc40..175ee7ab6 100644 --- a/pkg/addon/addon_suite_test.go +++ b/pkg/addon/addon_suite_test.go @@ -19,10 +19,13 @@ package addon import ( "context" "fmt" + "io" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/pkg/errors" + yaml3 "gopkg.in/yaml.v3" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,6 +45,7 @@ import ( "github.com/oam-dev/kubevela/pkg/oam/util" addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/apply" + "github.com/oam-dev/kubevela/references/cli/top/model" ) var _ = Describe("Addon test", func() { @@ -385,6 +389,57 @@ var _ = Describe("test enable addon in local dir", func() { }) }) +var _ = Describe("test dry-run addon from local dir", func() { + BeforeEach(func() { + app := v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Namespace: "vela-system", Name: "addon-example"}} + Expect(k8sClient.Delete(ctx, &app)).Should(SatisfyAny(BeNil(), util.NotFoundMatcher{})) + }) + AfterEach(func() { + app := v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Namespace: "vela-system", Name: "addon-example"}} + Expect(k8sClient.Delete(ctx, &app)).Should(SatisfyAny(BeNil(), util.NotFoundMatcher{})) + + cd := v1beta1.ComponentDefinition{ObjectMeta: metav1.ObjectMeta{Namespace: "vela-system", Name: "helm-example"}} + Expect(k8sClient.Delete(ctx, &cd)).Should(SatisfyAny(BeNil(), util.NotFoundMatcher{})) + }) + + It("test dry-run enable addon from local dir", func() { + ctx := context.Background() + + r := localReader{dir: "./testdata/example", name: "addon-example"} + metas, err := r.ListAddonMeta() + Expect(err).Should(BeNil()) + + meta := metas[r.name] + UIData, err := GetUIDataFromReader(r, &meta, UIMetaOptions) + Expect(err).Should(BeNil()) + + pkg, err := GetInstallPackageFromReader(r, &meta, UIData) + Expect(err).Should(BeNil()) + + h := NewAddonInstaller(ctx, k8sClient, dc, apply.NewAPIApplicator(k8sClient), cfg, &Registry{Name: LocalAddonRegistryName}, map[string]interface{}{"example": "test-dry-run"}, nil, DryRunAddon) + + err = h.enableAddon(pkg) + Expect(err).Should(BeNil()) + + decoder := yaml3.NewDecoder(h.dryRunBuff) + for { + obj := &unstructured.Unstructured{Object: map[string]interface{}{}} + err := decoder.Decode(obj.Object) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + Expect(err).Should(BeNil()) + } + Expect(obj.GetNamespace()).Should(BeEquivalentTo(model.VelaSystemNS)) + Expect(k8sClient.Create(ctx, obj)).Should(BeNil()) + } + + app := v1beta1.Application{} + Expect(k8sClient.Get(ctx, types2.NamespacedName{Namespace: "vela-system", Name: "addon-example"}, &app)).Should(BeNil()) + }) +}) + var _ = Describe("test enable addon which applies the views independently", func() { BeforeEach(func() { app := v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Namespace: "vela-system", Name: "addon-test-view"}} diff --git a/pkg/addon/utils.go b/pkg/addon/utils.go index 7cdd5d134..2ee8dd83d 100644 --- a/pkg/addon/utils.go +++ b/pkg/addon/utils.go @@ -24,13 +24,13 @@ import ( "path/filepath" "strings" - errors "github.com/pkg/errors" + "github.com/pkg/errors" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - rest "k8s.io/client-go/rest" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -272,7 +272,12 @@ func SkipValidateVersion(installer *Installer) { installer.skipVersionValidate = true } -// OverrideDefinitions menas override definitions within this addon if some of them already exist +// DryRunAddon means only generate yaml for addon instead of installing it +func DryRunAddon(installer *Installer) { + installer.dryRun = true +} + +// OverrideDefinitions means override definitions within this addon if some of them already exist func OverrideDefinitions(installer *Installer) { installer.overrideDefs = true } @@ -476,7 +481,7 @@ func produceDefConflictError(conflictDefs map[string]string) error { return errors.New(errorInfo) } -// checkBondComponentExistt will check the ready-to-apply object(def or auxiliary outputs) whether bind to a component +// checkBondComponentExist will check the ready-to-apply object(def or auxiliary outputs) whether bind to a component // if the target component not exist, return false. func checkBondComponentExist(u unstructured.Unstructured, app v1beta1.Application) bool { var comp string diff --git a/references/cli/addon.go b/references/cli/addon.go index 446b76c0a..7edafbf35 100644 --- a/references/cli/addon.go +++ b/references/cli/addon.go @@ -79,6 +79,8 @@ var skipValidate bool var overrideDefs bool +var dryRun bool + // NewAddonCommand create `addon` command func NewAddonCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { cmd := &cobra.Command{ @@ -130,9 +132,10 @@ func NewAddonListCommand(c common.Args) *cobra.Command { func NewAddonEnableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command { ctx := context.Background() cmd := &cobra.Command{ - Use: "enable", - Short: "enable an addon", - Long: "enable an addon in cluster.", + Use: "enable", + Aliases: []string{"install"}, + Short: "enable an addon", + Long: "enable an addon in cluster.", Example: ` Enable addon by: vela addon enable Enable addon with specify version: @@ -196,6 +199,9 @@ func NewAddonEnableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Com return err } } + if dryRun { + return nil + } fmt.Printf("Addon %s enabled successfully.\n", name) AdditionalEndpointPrinter(ctx, c, k8sClient, name, false) return nil @@ -206,6 +212,7 @@ func NewAddonEnableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Com cmd.Flags().StringVarP(&addonClusters, types.ClustersArg, "c", "", "specify the runtime-clusters to enable") cmd.Flags().BoolVarP(&skipValidate, "skip-version-validating", "s", false, "skip validating system version requirement") cmd.Flags().BoolVarP(&overrideDefs, "override-definitions", "", false, "override existing definitions if conflict with those contained in this addon") + cmd.Flags().BoolVarP(&dryRun, FlagDryRun, "", false, "render all yaml files out without real execute it") return cmd } @@ -344,6 +351,7 @@ func parseAddonArgsToMap(args []string) (map[string]interface{}, error) { func NewAddonDisableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "disable", + Aliases: []string{"uninstall"}, Short: "disable an addon", Long: "disable an addon in cluster.", Example: "vela addon disable ", @@ -535,13 +543,7 @@ func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.Dis } for _, registry := range registries { - var opts []pkgaddon.InstallOption - if skipValidate { - opts = append(opts, pkgaddon.SkipValidateVersion) - } - if overrideDefs { - opts = append(opts, pkgaddon.OverrideDefinitions) - } + opts := addonOptions() err = pkgaddon.EnableAddon(ctx, name, version, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil, opts...) if errors.Is(err, pkgaddon.ErrNotExist) { continue @@ -571,8 +573,7 @@ func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.Dis return fmt.Errorf("addon: %s not found in registries", name) } -// enableAddonByLocal enable addon in local dir and return the addon name -func enableAddonByLocal(ctx context.Context, name string, dir string, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, args map[string]interface{}) error { +func addonOptions() []pkgaddon.InstallOption { var opts []pkgaddon.InstallOption if skipValidate { opts = append(opts, pkgaddon.SkipValidateVersion) @@ -580,6 +581,15 @@ func enableAddonByLocal(ctx context.Context, name string, dir string, k8sClient if overrideDefs { opts = append(opts, pkgaddon.OverrideDefinitions) } + if dryRun { + opts = append(opts, pkgaddon.DryRunAddon) + } + return opts +} + +// enableAddonByLocal enable addon in local dir and return the addon name +func enableAddonByLocal(ctx context.Context, name string, dir string, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, args map[string]interface{}) error { + opts := addonOptions() if err := pkgaddon.EnableAddonByLocalDir(ctx, name, dir, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, args, opts...); err != nil { return err } @@ -961,6 +971,9 @@ func listAddons(ctx context.Context, clt client.Client, registry string) (*uitab } func waitApplicationRunning(k8sClient client.Client, addonName string) error { + if dryRun { + return nil + } trackInterval := 5 * time.Second timeout := 600 * time.Second start := time.Now()