diff --git a/cmd/cli/cmd/install.go b/cmd/cli/cmd/install.go index 4f61028ea..4e1d307fb 100644 --- a/cmd/cli/cmd/install.go +++ b/cmd/cli/cmd/install.go @@ -11,6 +11,7 @@ import ( "github.com/kubeshop/botkube/internal/cli" "github.com/kubeshop/botkube/internal/cli/heredoc" "github.com/kubeshop/botkube/internal/cli/install" + "github.com/kubeshop/botkube/internal/cli/install/helm" "github.com/kubeshop/botkube/internal/kubex" ) @@ -20,8 +21,8 @@ func NewInstall() *cobra.Command { installCmd := &cobra.Command{ Use: "install [OPTIONS]", - Short: "install Botkube into cluster", - Long: "Use this command to install the Botkube agent.", + Short: "install or upgrade Botkube in k8s cluster", + Long: "Use this command to install or upgrade the Botkube agent.", Aliases: []string{"instl", "deploy"}, Example: heredoc.WithCLIName(` # Install latest stable Botkube version @@ -34,9 +35,6 @@ func NewInstall() *cobra.Command { install --repo @local`, cli.Name), RunE: func(cmd *cobra.Command, args []string) error { config, err := kubex.LoadRestConfigWithMetaInformation() - if err != nil { - return err - } if err != nil { return errors.Wrap(err, "while creating k8s config") } @@ -52,11 +50,11 @@ func NewInstall() *cobra.Command { flags.BoolVarP(&opts.Watch, "watch", "w", true, "Watches the status of the Botkube installation until it finish or the defined `--timeout` occurs.") // common params for install and upgrade operation - flags.StringVar(&opts.HelmParams.Version, "version", install.LatestVersionTag, "Botkube version. Possible values @latest, 1.2.0, ...") - flags.StringVar(&opts.HelmParams.Namespace, "namespace", install.Namespace, "Botkube installation namespace.") - flags.StringVar(&opts.HelmParams.ReleaseName, "release-name", install.ReleaseName, "Botkube Helm chart release name.") - flags.StringVar(&opts.HelmParams.ChartName, "chart-name", install.HelmChartName, "Botkube Helm chart name.") - flags.StringVar(&opts.HelmParams.RepoLocation, "repo", install.HelmRepoStable, fmt.Sprintf("Botkube Helm chart repository location. It can be relative path to current working directory or URL. Use %s tag to select repository which holds the stable Helm chart versions.", install.StableVersionTag)) + flags.StringVar(&opts.HelmParams.Version, "version", helm.LatestVersionTag, "Botkube version. Possible values @latest, 1.2.0, ...") + flags.StringVar(&opts.HelmParams.Namespace, "namespace", helm.Namespace, "Botkube installation namespace.") + flags.StringVar(&opts.HelmParams.ReleaseName, "release-name", helm.ReleaseName, "Botkube Helm chart release name.") + flags.StringVar(&opts.HelmParams.ChartName, "chart-name", helm.HelmChartName, "Botkube Helm chart name.") + flags.StringVar(&opts.HelmParams.RepoLocation, "repo", helm.HelmRepoStable, fmt.Sprintf("Botkube Helm chart repository location. It can be relative path to current working directory or URL. Use %s tag to select repository which holds the stable Helm chart versions.", helm.StableVersionTag)) flags.BoolVar(&opts.HelmParams.DryRun, "dry-run", false, "Simulate an install") flags.BoolVar(&opts.HelmParams.Force, "force", false, "Force resource updates through a replacement strategy") flags.BoolVar(&opts.HelmParams.DisableHooks, "no-hooks", false, "Disable pre/post install/upgrade hooks") diff --git a/cmd/cli/cmd/migrate.go b/cmd/cli/cmd/migrate.go index 57308063a..20e14d366 100644 --- a/cmd/cli/cmd/migrate.go +++ b/cmd/cli/cmd/migrate.go @@ -9,6 +9,7 @@ import ( "github.com/fatih/color" semver "github.com/hashicorp/go-version" "github.com/pkg/browser" + "github.com/pkg/errors" "github.com/spf13/cobra" "go.szostok.io/version" corev1 "k8s.io/api/core/v1" @@ -17,6 +18,7 @@ import ( "github.com/kubeshop/botkube/internal/cli/heredoc" "github.com/kubeshop/botkube/internal/cli/migrate" "github.com/kubeshop/botkube/internal/cli/printer" + "github.com/kubeshop/botkube/internal/kubex" ) const ( @@ -54,13 +56,18 @@ func NewMigrate() *cobra.Command { `, cli.Name), RunE: func(cmd *cobra.Command, args []string) (err error) { + k8sConfig, err := kubex.LoadRestConfigWithMetaInformation() + if err != nil { + return errors.Wrap(err, "while creating k8s config") + } + status := printer.NewStatus(cmd.OutOrStdout(), "Migrating Botkube installation to Cloud") defer func() { status.End(err == nil) }() status.Step("Fetching Botkube configuration") - cfg, pod, err := migrate.GetConfigFromCluster(cmd.Context(), opts) + cfg, pod, err := migrate.GetConfigFromCluster(cmd.Context(), k8sConfig.K8s, opts) if err != nil { return err } @@ -107,7 +114,7 @@ func NewMigrate() *cobra.Command { } status.Step("Run Botkube migration") - instanceID, err := migrate.Run(cmd.Context(), status, cfg, opts) + instanceID, err := migrate.Run(cmd.Context(), status, cfg, k8sConfig, opts) if err != nil { return err } @@ -134,7 +141,10 @@ func NewMigrate() *cobra.Command { } flags := login.Flags() + kubex.RegisterKubeconfigFlag(flags) + flags.DurationVar(&opts.Timeout, "timeout", 10*time.Minute, `Maximum time during which the Botkube installation is being watched, where "0" means "infinite". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`) flags.StringVar(&opts.Token, "token", "", "Botkube Cloud authentication token") + flags.BoolVarP(&opts.Watch, "watch", "w", true, "Watches the status of the Botkube installation until it finish or the defined `--timeout` occurs.") flags.StringVar(&opts.InstanceName, "instance-name", "", "Botkube Cloud Instance name that will be created") flags.StringVar(&opts.CloudAPIURL, "cloud-api-url", "https://api.botkube.io/graphql", "Botkube Cloud API URL") flags.StringVar(&opts.CloudDashboardURL, "cloud-dashboard-url", "https://app.botkube.io", "Botkube Cloud URL") diff --git a/cmd/cli/cmd/uninstall.go b/cmd/cli/cmd/uninstall.go index c7531ba44..ea67dd24f 100644 --- a/cmd/cli/cmd/uninstall.go +++ b/cmd/cli/cmd/uninstall.go @@ -9,7 +9,7 @@ import ( "github.com/kubeshop/botkube/internal/cli" "github.com/kubeshop/botkube/internal/cli/heredoc" - "github.com/kubeshop/botkube/internal/cli/install" + "github.com/kubeshop/botkube/internal/cli/install/helm" "github.com/kubeshop/botkube/internal/cli/uninstall" "github.com/kubeshop/botkube/internal/kubex" ) @@ -46,8 +46,8 @@ func NewUninstall() *cobra.Command { kubex.RegisterKubeconfigFlag(flags) - flags.StringVar(&opts.HelmParams.ReleaseName, "release-name", install.ReleaseName, "Botkube Helm release name.") - flags.StringVar(&opts.HelmParams.ReleaseNamespace, "namespace", install.Namespace, "Botkube namespace.") + flags.StringVar(&opts.HelmParams.ReleaseName, "release-name", helm.ReleaseName, "Botkube Helm release name.") + flags.StringVar(&opts.HelmParams.ReleaseNamespace, "namespace", helm.Namespace, "Botkube namespace.") flags.BoolVarP(&opts.AutoApprove, "auto-approve", "y", false, "Skips interactive approval for deletion.") flags.BoolVar(&opts.HelmParams.DryRun, "dry-run", false, "simulate a uninstall") diff --git a/cmd/cli/docs/botkube.md b/cmd/cli/docs/botkube.md index 47c8f722a..2c628c9b2 100644 --- a/cmd/cli/docs/botkube.md +++ b/cmd/cli/docs/botkube.md @@ -36,7 +36,7 @@ botkube [flags] ### SEE ALSO -* [botkube install](botkube_install.md) - install Botkube into cluster +* [botkube install](botkube_install.md) - install or upgrade Botkube in k8s cluster * [botkube login](botkube_login.md) - Login to a Botkube Cloud * [botkube migrate](botkube_migrate.md) - Automatically migrates Botkube installation into Botkube Cloud * [botkube uninstall](botkube_uninstall.md) - uninstall Botkube from cluster diff --git a/cmd/cli/docs/botkube_install.md b/cmd/cli/docs/botkube_install.md index 4dcea4dab..ee4a6d914 100644 --- a/cmd/cli/docs/botkube_install.md +++ b/cmd/cli/docs/botkube_install.md @@ -4,11 +4,11 @@ title: botkube install ## botkube install -install Botkube into cluster +install or upgrade Botkube in k8s cluster ### Synopsis -Use this command to install the Botkube agent. +Use this command to install or upgrade the Botkube agent. ``` botkube install [OPTIONS] [flags] diff --git a/cmd/cli/docs/botkube_migrate.md b/cmd/cli/docs/botkube_migrate.md index 8afa05945..502e35b14 100644 --- a/cmd/cli/docs/botkube_migrate.md +++ b/cmd/cli/docs/botkube_migrate.md @@ -44,11 +44,14 @@ botkube migrate [OPTIONS] [flags] --cloud-dashboard-url string Botkube Cloud URL (default "https://app.botkube.io") -h, --help help for migrate --instance-name string Botkube Cloud Instance name that will be created + --kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster. -l, --label string Label of Botkube pod (default "app=botkube") -n, --namespace string Namespace of Botkube pod (default "botkube") -q, --skip-connect Skips connecting to Botkube Cloud after migration --skip-open-browser Skips opening web browser after migration + --timeout duration Maximum time during which the Botkube installation is being watched, where "0" means "infinite". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". (default 10m0s) --token string Botkube Cloud authentication token + -w, --watch --timeout Watches the status of the Botkube installation until it finish or the defined --timeout occurs. (default true) ``` ### Options inherited from parent commands diff --git a/go.mod b/go.mod index f9d09c999..415ad0bae 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/vrischmann/envconfig v1.3.0 github.com/xeipuuv/gojsonschema v1.2.0 + github.com/xyproto/randomstring v1.0.5 go.szostok.io/version v1.2.0 golang.org/x/exp v0.0.0-20230307190834-24139beb5833 golang.org/x/oauth2 v0.8.0 diff --git a/go.sum b/go.sum index 5581216c3..a3ee8530d 100644 --- a/go.sum +++ b/go.sum @@ -1210,6 +1210,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/cli/install/config.go b/internal/cli/install/config.go index addf52670..5ce627df6 100644 --- a/internal/cli/install/config.go +++ b/internal/cli/install/config.go @@ -6,25 +6,6 @@ import ( "github.com/kubeshop/botkube/internal/cli/install/helm" ) -const ( - // StableVersionTag tag used to select stable Helm chart repository. - StableVersionTag = "@stable" - // LocalVersionTag tag used to select local Helm chart repository. - LocalVersionTag = "@local" - // LatestVersionTag tag used to select the latest version from the Helm chart repository. - LatestVersionTag = "@latest" - // Namespace in which Botkube is installed. - Namespace = "botkube" - // ReleaseName defines Botkube Helm chart release name. - ReleaseName = "botkube" - // HelmRepoStable URL of the stable Botkube Helm charts repository. - HelmRepoStable = "https://charts.botkube.io/" - // HelmChartName represents Botkube Helm chart name in a given Helm repository. - HelmChartName = "botkube" - // LocalChartsPath path to Helm charts in botkube repository. - LocalChartsPath = "./helm/" -) - // Config holds parameters for Botkube installation on cluster. type Config struct { Kubeconfig string diff --git a/internal/cli/install/helm/config.go b/internal/cli/install/helm/config.go index c2898570c..bcf42706d 100644 --- a/internal/cli/install/helm/config.go +++ b/internal/cli/install/helm/config.go @@ -4,6 +4,25 @@ import ( "helm.sh/helm/v3/pkg/cli/values" ) +const ( + // StableVersionTag tag used to select stable Helm chart repository. + StableVersionTag = "@stable" + // LocalVersionTag tag used to select local Helm chart repository. + LocalVersionTag = "@local" + // LatestVersionTag tag used to select the latest version from the Helm chart repository. + LatestVersionTag = "@latest" + // Namespace in which Botkube is installed. + Namespace = "botkube" + // ReleaseName defines Botkube Helm chart release name. + ReleaseName = "botkube" + // HelmRepoStable URL of the stable Botkube Helm charts repository. + HelmRepoStable = "https://charts.botkube.io/" + // HelmChartName represents Botkube Helm chart name in a given Helm repository. + HelmChartName = "botkube" + // LocalChartsPath path to Helm charts in botkube repository. + LocalChartsPath = "./helm/" +) + // Config holds Helm configuration parameters. type Config struct { ReleaseName string diff --git a/internal/cli/install/install.go b/internal/cli/install/install.go index cb05e6805..b72e769ba 100644 --- a/internal/cli/install/install.go +++ b/internal/cli/install/install.go @@ -39,21 +39,21 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt }() switch opts.HelmParams.RepoLocation { - case StableVersionTag: - status.Debugf("Resolved %s tag into %s...", StableVersionTag, HelmRepoStable) - opts.HelmParams.RepoLocation = HelmRepoStable - case LocalVersionTag: - status.Debugf("Resolved %s tag into %s...", LocalVersionTag, LocalChartsPath) - opts.HelmParams.RepoLocation = LocalChartsPath + case helm.StableVersionTag: + status.Debugf("Resolved %s tag into %s...", helm.StableVersionTag, helm.HelmRepoStable) + opts.HelmParams.RepoLocation = helm.HelmRepoStable + case helm.LocalVersionTag: + status.Debugf("Resolved %s tag into %s...", helm.LocalVersionTag, helm.LocalChartsPath) + opts.HelmParams.RepoLocation = helm.LocalChartsPath opts.HelmParams.Version = "" } - if opts.HelmParams.Version == LatestVersionTag { + if opts.HelmParams.Version == helm.LatestVersionTag { ver, err := helm.GetLatestVersion(opts.HelmParams.RepoLocation, opts.HelmParams.ChartName) if err != nil { return err } - status.Debugf("Resolved %s tag into %s...", LatestVersionTag, ver) + status.Debugf("Resolved %s tag into %s...", helm.LatestVersionTag, ver) opts.HelmParams.Version = ver } diff --git a/internal/cli/migrate/converter.go b/internal/cli/migrate/converter.go index d27ec7635..c6a508b3f 100644 --- a/internal/cli/migrate/converter.go +++ b/internal/cli/migrate/converter.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/xyproto/randomstring" + "github.com/kubeshop/botkube/internal/ptr" gqlModel "github.com/kubeshop/botkube/internal/remote/graphql" bkconfig "github.com/kubeshop/botkube/pkg/config" @@ -12,31 +14,60 @@ import ( ) // Converter converts OS config into GraphQL create input. -type Converter struct{} +type Converter struct { + pluginNames map[string]string +} // NewConverter returns a new Converter instance. func NewConverter() *Converter { - return &Converter{} + return &Converter{ + pluginNames: map[string]string{}, + } } // ConvertActions converts Actions. -func (c *Converter) ConvertActions(actions bkconfig.Actions) []*gqlModel.ActionCreateUpdateInput { +func (c *Converter) ConvertActions(actions bkconfig.Actions, sources map[string]bkconfig.Sources, executors map[string]bkconfig.Executors) []*gqlModel.ActionCreateUpdateInput { var out []*gqlModel.ActionCreateUpdateInput for name, act := range actions { + bindings, ok := c.checkActionBindingExists(act, sources, executors) + if !ok { + continue + } out = append(out, &gqlModel.ActionCreateUpdateInput{ Name: name, DisplayName: act.DisplayName, Enabled: act.Enabled, Command: act.Command, - Bindings: &gqlModel.ActionCreateUpdateInputBindings{ - Sources: act.Bindings.Sources, - Executors: act.Bindings.Executors, - }, + Bindings: bindings, }) } return out } +func (c *Converter) checkActionBindingExists(act bkconfig.Action, sources map[string]bkconfig.Sources, executors map[string]bkconfig.Executors) (*gqlModel.ActionCreateUpdateInputBindings, bool) { + sourcesGenerated := make([]string, 0, len(act.Bindings.Sources)) + for _, source := range act.Bindings.Sources { + if _, ok := sources[source]; !ok { + return nil, false + } + name := c.getOrGeneratePluginName(source) + sourcesGenerated = append(sourcesGenerated, name) + } + executorsGenerated := make([]string, 0, len(act.Bindings.Executors)) + for _, executor := range act.Bindings.Executors { + if _, ok := executors[executor]; !ok { + return nil, false + } + name := c.getOrGeneratePluginName(executor) + executorsGenerated = append(executorsGenerated, name) + } + + return &gqlModel.ActionCreateUpdateInputBindings{ + Sources: sourcesGenerated, + Executors: executorsGenerated, + }, true +} + // ConvertAliases converts Aliases. func (c *Converter) ConvertAliases(aliases bkconfig.Aliases, instanceID string) []*gqlModel.AliasCreateInput { var out []*gqlModel.AliasCreateInput @@ -89,7 +120,7 @@ func (c *Converter) convertExecutors(executors map[string]bkconfig.Executors) ([ Type: gqlModel.PluginTypeExecutor, Configurations: []*gqlModel.PluginConfigurationInput{ { - Name: cfgName, + Name: c.getOrGeneratePluginName(cfgName), Configuration: string(rawCfg), Rbac: c.convertRbac(p.Context), }, @@ -120,7 +151,7 @@ func (c *Converter) convertSources(sources map[string]bkconfig.Sources) ([]*gqlM Type: gqlModel.PluginTypeSource, Configurations: []*gqlModel.PluginConfigurationInput{ { - Name: cfgName, + Name: c.getOrGeneratePluginName(cfgName), Configuration: string(rawCfg), Rbac: c.convertRbac(p.Context), }, @@ -173,8 +204,8 @@ func (c *Converter) convertSlackPlatform(name string, slack bkconfig.SocketSlack channels = append(channels, &gqlModel.ChannelBindingsByNameCreateInput{ Name: ch.Name, Bindings: &gqlModel.BotBindingsCreateInput{ - Sources: toSlicePointers(ch.Bindings.Sources), - Executors: toSlicePointers(ch.Bindings.Executors), + Sources: c.toGeneratedNamesSlice(ch.Bindings.Sources), + Executors: c.toGeneratedNamesSlice(ch.Bindings.Executors), }, NotificationsDisabled: ptr.FromType(ch.Notification.Disabled), }) @@ -194,8 +225,8 @@ func (c *Converter) convertDiscordPlatform(name string, discord bkconfig.Discord channels = append(channels, &gqlModel.ChannelBindingsByIDCreateInput{ ID: ch.ID, Bindings: &gqlModel.BotBindingsCreateInput{ - Sources: toSlicePointers(ch.Bindings.Sources), - Executors: toSlicePointers(ch.Bindings.Executors), + Sources: c.toGeneratedNamesSlice(ch.Bindings.Sources), + Executors: c.toGeneratedNamesSlice(ch.Bindings.Executors), }, NotificationsDisabled: ptr.FromType(ch.Notification.Disabled), }) @@ -215,8 +246,8 @@ func (c *Converter) convertMattermostPlatform(name string, matt bkconfig.Matterm channels = append(channels, &gqlModel.ChannelBindingsByNameCreateInput{ Name: ch.Name, Bindings: &gqlModel.BotBindingsCreateInput{ - Sources: toSlicePointers(ch.Bindings.Sources), - Executors: toSlicePointers(ch.Bindings.Executors), + Sources: c.toGeneratedNamesSlice(ch.Bindings.Sources), + Executors: c.toGeneratedNamesSlice(ch.Bindings.Executors), }, NotificationsDisabled: ptr.FromType(ch.Notification.Disabled), }) @@ -232,10 +263,20 @@ func (c *Converter) convertMattermostPlatform(name string, matt bkconfig.Matterm } } -func toSlicePointers[T any](in []T) []*T { - var out []*T - for idx := range in { - out = append(out, &in[idx]) +func (c *Converter) getOrGeneratePluginName(plugin string) string { + if name, ok := c.pluginNames[plugin]; ok { + return name + } + name := fmt.Sprintf("%s_%s", plugin, randomstring.CookieFriendlyString(5)) + c.pluginNames[plugin] = name + return name +} + +func (c *Converter) toGeneratedNamesSlice(in []string) []*string { + out := make([]*string, 0, len(in)) + for _, name := range in { + generated := c.getOrGeneratePluginName(name) + out = append(out, &generated) } return out } diff --git a/internal/cli/migrate/migrate.go b/internal/cli/migrate/migrate.go index 19cb02114..b2c9bd055 100644 --- a/internal/cli/migrate/migrate.go +++ b/internal/cli/migrate/migrate.go @@ -4,25 +4,27 @@ import ( "context" "fmt" "os" - "os/exec" - "strings" "time" "github.com/AlecAivazis/survey/v2" "github.com/hasura/go-graphql-client" - "github.com/muesli/reflow/indent" + "github.com/mattn/go-shellwords" "github.com/pkg/errors" + flag "github.com/spf13/pflag" "golang.org/x/oauth2" + "helm.sh/helm/v3/pkg/cli/values" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/rest" "github.com/kubeshop/botkube/internal/cli" cliconfig "github.com/kubeshop/botkube/internal/cli/config" + "github.com/kubeshop/botkube/internal/cli/install" + "github.com/kubeshop/botkube/internal/cli/install/helm" "github.com/kubeshop/botkube/internal/cli/printer" - "github.com/kubeshop/botkube/internal/ptr" + "github.com/kubeshop/botkube/internal/kubex" gqlModel "github.com/kubeshop/botkube/internal/remote/graphql" bkconfig "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/multierror" @@ -33,10 +35,11 @@ const ( configMapName = "botkube-config-exporter" instanceDetailsURLFmt = "%s/instances/%s" + platformNameOther = "Other" ) // Run runs the migration process. -func Run(ctx context.Context, status *printer.StatusPrinter, config []byte, opts Options) (string, error) { +func Run(ctx context.Context, status *printer.StatusPrinter, config []byte, k8sCfg *kubex.ConfigWithMeta, opts Options) (string, error) { authToken := opts.Token if authToken == "" { cfg, err := cliconfig.New() @@ -52,10 +55,10 @@ func Run(ctx context.Context, status *printer.StatusPrinter, config []byte, opts return "", err } - return migrate(ctx, status, opts, botkubeClusterConfig, authToken) + return migrate(ctx, status, opts, botkubeClusterConfig, k8sCfg, authToken) } -func migrate(ctx context.Context, status *printer.StatusPrinter, opts Options, botkubeClusterConfig *bkconfig.Config, token string) (string, error) { +func migrate(ctx context.Context, status *printer.StatusPrinter, opts Options, botkubeClusterConfig *bkconfig.Config, k8sCfg *kubex.ConfigWithMeta, token string) (string, error) { converter := NewConverter() src := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, @@ -74,7 +77,7 @@ func migrate(ctx context.Context, status *printer.StatusPrinter, opts Options, b } status.Step("Converted %d plugins", pluginsCount) - actions := converter.ConvertActions(botkubeClusterConfig.Actions) + actions := converter.ConvertActions(botkubeClusterConfig.Actions, botkubeClusterConfig.Sources, botkubeClusterConfig.Executors) status.Step("Converted %d actions", len(actions)) platforms := converter.ConvertPlatforms(botkubeClusterConfig.Communications) @@ -92,8 +95,8 @@ func migrate(ctx context.Context, status *printer.StatusPrinter, opts Options, b status.Step("Creating %q Cloud Instance", instanceName) var mutation struct { CreateDeployment struct { - ID string `json:"id"` - HelmCommand *string `json:"helmCommand"` + ID string `json:"id"` + InstallUpgradeInstructions []*gqlModel.InstallUpgradeInstructionsForPlatform `json:"installUpgradeInstructions"` } `graphql:"createDeployment(input: $input)"` } err = client.Mutate(ctx, &mutation, map[string]interface{}{ @@ -127,6 +130,7 @@ func migrate(ctx context.Context, status *printer.StatusPrinter, opts Options, b continue } } + status.End(true) if errs.ErrorOrNil() != nil { return "", fmt.Errorf("while migrating aliases: %w%s", errs.ErrorOrNil(), errStateMessage(opts.CloudDashboardURL, mutation.CreateDeployment.ID)) @@ -137,44 +141,19 @@ func migrate(ctx context.Context, status *printer.StatusPrinter, opts Options, b return mutation.CreateDeployment.ID, nil } - helmCmd := ptr.ToValue(mutation.CreateDeployment.HelmCommand) - cmds := []string{ - "helm repo add botkube https://charts.botkube.io", - "helm repo update botkube", - helmCmd, - } - bldr := strings.Builder{} - for _, cmd := range cmds { - msg := fmt.Sprintf("$ %s\n\n", cmd) - bldr.WriteString(indent.String(msg, 4)) - } - status.InfoWithBody("Connect Botkube instance", bldr.String()) - - shouldUpgrade, err := shouldUpgradeInstallation(opts) + params, err := parseHelmCommand(mutation.CreateDeployment.InstallUpgradeInstructions, opts.AutoApprove) if err != nil { - return "", err + return "", errors.Wrap(err, "while parsing helm command") } - if !shouldUpgrade { - status.Infof("Skipping command execution. Remember to run it manually to finish the migration process.") - return mutation.CreateDeployment.ID, nil + installConfig := install.Config{ + HelmParams: params, + Watch: opts.Watch, + Timeout: opts.Timeout, } - - status.Infof("Running helm upgrade") - for _, cmd := range cmds { - //nolint:gosec //subprocess launched with variable - cmd := exec.Command("/bin/sh", "-c", cmd) - cmd.Stderr = NewIndentWriter(os.Stderr, 4) - cmd.Stdout = NewIndentWriter(os.Stdout, 4) - - if err = cmd.Run(); err != nil { - return "", err - } - fmt.Println() - fmt.Println() + if err := install.Install(ctx, os.Stdout, k8sCfg, installConfig); err != nil { + return "", errors.Wrap(err, "while installing Botkube") } - status.End(true) - return mutation.CreateDeployment.ID, nil } @@ -201,8 +180,8 @@ func getInstanceName(opts Options) (string, error) { return opts.InstanceName, nil } -func GetConfigFromCluster(ctx context.Context, opts Options) ([]byte, *corev1.Pod, error) { - k8sCli, err := newK8sClient() +func GetConfigFromCluster(ctx context.Context, k8sCfg *rest.Config, opts Options) ([]byte, *corev1.Pod, error) { + k8sCli, err := kubernetes.NewForConfig(k8sCfg) if err != nil { return nil, nil, err } @@ -227,14 +206,6 @@ func GetConfigFromCluster(ctx context.Context, opts Options) ([]byte, *corev1.Po return config, botkubePod, nil } -func newK8sClient() (*kubernetes.Clientset, error) { - k8sConfig, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) - if err != nil { - return nil, err - } - return kubernetes.NewForConfig(k8sConfig) -} - func getBotkubePod(ctx context.Context, k8sCli *kubernetes.Clientset, opts Options) (*corev1.Pod, error) { pods, err := k8sCli.CoreV1().Pods(opts.Namespace).List(ctx, metav1.ListOptions{LabelSelector: opts.Label}) if err != nil { @@ -324,6 +295,40 @@ func waitForMigrationJob(ctx context.Context, k8sCli *kubernetes.Clientset, opts } } +func parseHelmCommand(instructions []*gqlModel.InstallUpgradeInstructionsForPlatform, autoApprove bool) (helm.Config, error) { + var raw string + for _, i := range instructions { + if i.PlatformName == platformNameOther { + raw = i.InstallUpgradeCommand + } + } + tokenized, err := shellwords.Parse(raw) + if err != nil { + return helm.Config{}, errors.Wrap(err, "could not tokenize helm command") + } + + var version string + var vals []string + flagSet := flag.NewFlagSet("helm cmd", flag.ExitOnError) + flagSet.StringVar(&version, "version", "", "") + flagSet.StringArrayVar(&vals, "set", []string{}, "") + if err := flagSet.Parse(tokenized); err != nil { + return helm.Config{}, errors.Wrap(err, "could not register flags") + } + + return helm.Config{ + Version: version, + Values: values.Options{ + Values: vals, + }, + Namespace: helm.Namespace, + ReleaseName: helm.ReleaseName, + ChartName: helm.HelmChartName, + RepoLocation: helm.HelmRepoStable, + AutoApprove: autoApprove, + }, nil +} + func readConfigFromCM(ctx context.Context, k8sCli *kubernetes.Clientset, opts Options) ([]byte, error) { configMap, err := k8sCli.CoreV1().ConfigMaps(opts.Namespace).Get(ctx, configMapName, metav1.GetOptions{}) if err != nil { @@ -342,22 +347,3 @@ func errStateMessage(dashboardURL, instanceID string) string { return fmt.Sprintf("\n\nMigration process failed. Navigate to %s to continue configuring newly created instance.\n"+ "Alternatively, delete the instance from the link above and try again.", fmt.Sprintf(instanceDetailsURLFmt, dashboardURL, instanceID)) } - -func shouldUpgradeInstallation(opts Options) (bool, error) { - if opts.AutoApprove { - return true, nil - } - - run := false - prompt := &survey.Confirm{ - Message: "Would you like to continue?", - Default: true, - } - - err := survey.AskOne(prompt, &run) - if err != nil { - return false, errors.Wrap(err, "while asking for confirmation") - } - - return run, nil -} diff --git a/internal/cli/migrate/opts.go b/internal/cli/migrate/opts.go index 893a2459f..1bd137ddb 100644 --- a/internal/cli/migrate/opts.go +++ b/internal/cli/migrate/opts.go @@ -1,9 +1,13 @@ package migrate -import "time" +import ( + "time" +) // Options holds migrate possible configuration options. type Options struct { + Timeout time.Duration + Watch bool Token string InstanceName string `survey:"instanceName"` CloudDashboardURL string