diff --git a/action/apply.go b/action/apply.go index 35ca81bf..7821f844 100644 --- a/action/apply.go +++ b/action/apply.go @@ -2,23 +2,18 @@ package action import ( "fmt" - "os" + "io" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" log "github.com/sirupsen/logrus" ) type Apply struct { - // Config is the k0sctl config - Config *v1beta1.Cluster - // Concurrency is the number of concurrent actions to run - Concurrency int - // ConcurrentUploads is the number of concurrent uploads to run - ConcurrentUploads int + // Manager is the phase manager + Manager *phase.Manager // DisableDowngradeCheck skips the downgrade check DisableDowngradeCheck bool // NoWait skips waiting for the cluster to be ready @@ -27,27 +22,20 @@ type Apply struct { NoDrain bool // RestoreFrom is the path to a cluster backup archive to restore the state from RestoreFrom string - // KubeconfigOut is the path to write the kubeconfig to - KubeconfigOut string + // KubeconfigOut is a writer to write the kubeconfig to + KubeconfigOut io.Writer // KubeconfigAPIAddress is the API address to use in the kubeconfig KubeconfigAPIAddress string - // LogFile is the path where log will be found from - LogFile *os.File } func (a Apply) Run() error { start := time.Now() - if a.Config == nil { - return fmt.Errorf("config is nil") - } - phase.NoWait = a.NoWait - manager := phase.Manager{Config: a.Config, Concurrency: a.Concurrency, ConcurrentUploads: a.ConcurrentUploads} lockPhase := &phase.Lock{} - manager.AddPhase( + a.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, lockPhase, @@ -77,13 +65,11 @@ func (a Apply) Run() error { &phase.RunHooks{Stage: "after", Action: "apply"}, ) - var kubeCfgPhase *phase.GetKubeconfig - if a.KubeconfigOut != "" { - kubeCfgPhase = &phase.GetKubeconfig{APIAddress: a.KubeconfigAPIAddress} - manager.AddPhase(kubeCfgPhase) + if a.KubeconfigOut != nil { + a.Manager.AddPhase(&phase.GetKubeconfig{APIAddress: a.KubeconfigAPIAddress}) } - manager.AddPhase( + a.Manager.AddPhase( &phase.Unlock{Cancel: lockPhase.Cancel}, &phase.Disconnect{}, ) @@ -92,20 +78,15 @@ func (a Apply) Run() error { var result error - if result = manager.Run(); result != nil { - analytics.Client.Publish("apply-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) - if lf := a.LogFile; lf != nil { - log.Errorf("apply failed - log file saved to %s", lf.Name()) - } + if result = a.Manager.Run(); result != nil { + analytics.Client.Publish("apply-failure", map[string]interface{}{"clusterID": a.Manager.Config.Spec.K0s.Metadata.ClusterID}) return result } - analytics.Client.Publish("apply-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) - if a.KubeconfigOut != "" { - if err := os.WriteFile(a.KubeconfigOut, []byte(manager.Config.Metadata.Kubeconfig), 0644); err != nil { + analytics.Client.Publish("apply-success", map[string]interface{}{"duration": time.Since(start), "clusterID": a.Manager.Config.Spec.K0s.Metadata.ClusterID}) + if a.KubeconfigOut != nil { + if _, err := a.KubeconfigOut.Write([]byte(a.Manager.Config.Metadata.Kubeconfig)); err != nil { log.Warnf("failed to write kubeconfig to %s: %v", a.KubeconfigOut, err) - } else { - log.Infof("kubeconfig written to %s", a.KubeconfigOut) } } @@ -114,7 +95,7 @@ func (a Apply) Run() error { log.Infof(phase.Colorize.Green(text).String()) uninstalled := false - for _, host := range manager.Config.Spec.Hosts { + for _, host := range a.Manager.Config.Spec.Hosts { if host.Reset { uninstalled = true } @@ -123,9 +104,9 @@ func (a Apply) Run() error { log.Info("There were nodes that got uninstalled during the apply phase. Please remove them from your k0sctl config file") } - log.Infof("k0s cluster version %s is now installed", manager.Config.Spec.K0s.Version) + log.Infof("k0s cluster version %s is now installed", a.Manager.Config.Spec.K0s.Version) - if a.KubeconfigOut != "" { + if a.KubeconfigOut != nil { log.Infof("Tip: To access the cluster you can now fetch the admin kubeconfig using:") log.Infof(" " + phase.Colorize.Cyan("k0sctl kubeconfig").String()) } diff --git a/action/backup.go b/action/backup.go index e4656d61..73c26acd 100644 --- a/action/backup.go +++ b/action/backup.go @@ -2,29 +2,24 @@ package action import ( "fmt" - "os" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" log "github.com/sirupsen/logrus" ) type Backup struct { - Config *v1beta1.Cluster - Concurrency int - ConcurrentUploads int - LogFile *os.File + // Manager is the phase manager + Manager *phase.Manager } func (b Backup) Run() error { start := time.Now() - manager := phase.Manager{Config: b.Config, Concurrency: b.Concurrency} lockPhase := &phase.Lock{} - manager.AddPhase( + b.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, lockPhase, @@ -40,15 +35,12 @@ func (b Backup) Run() error { analytics.Client.Publish("backup-start", map[string]interface{}{}) - if err := manager.Run(); err != nil { - analytics.Client.Publish("backup-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) - if b.LogFile != nil { - log.Errorf("backup failed - log file saved to %s", b.LogFile.Name()) - } + if err := b.Manager.Run(); err != nil { + analytics.Client.Publish("backup-failure", map[string]interface{}{"clusterID": b.Manager.Config.Spec.K0s.Metadata.ClusterID}) return err } - analytics.Client.Publish("backup-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("backup-success", map[string]interface{}{"duration": time.Since(start), "clusterID": b.Manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) diff --git a/action/config_edit.go b/action/config_edit.go index 5830265d..62715a7b 100644 --- a/action/config_edit.go +++ b/action/config_edit.go @@ -15,11 +15,10 @@ import ( ) type ConfigEdit struct { - Config *v1beta1.Cluster - Concurrency int - Stdout io.Writer - Stderr io.Writer - Stdin io.Reader + Config *v1beta1.Cluster + Stdout io.Writer + Stderr io.Writer + Stdin io.Reader } func (c ConfigEdit) Run() error { diff --git a/action/kubeconfig.go b/action/kubeconfig.go index 5a378e4b..63b7c9c4 100644 --- a/action/kubeconfig.go +++ b/action/kubeconfig.go @@ -1,39 +1,29 @@ package action import ( - "fmt" - "github.com/k0sproject/k0sctl/phase" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" ) type Kubeconfig struct { - Config *v1beta1.Cluster - Concurrency int + // Manager is the phase manager + Manager *phase.Manager KubeconfigAPIAddress string Kubeconfig string } func (k *Kubeconfig) Run() error { - if k.Config == nil { - return fmt.Errorf("config is nil") - } - - c := k.Config - // Change so that the internal config has only single controller host as we // do not need to connect to all nodes - c.Spec.Hosts = cluster.Hosts{c.Spec.K0sLeader()} - manager := phase.Manager{Config: k.Config, Concurrency: k.Concurrency} + k.Manager.Config.Spec.Hosts = cluster.Hosts{k.Manager.Config.Spec.K0sLeader()} - manager.AddPhase( + k.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, &phase.GetKubeconfig{APIAddress: k.KubeconfigAPIAddress}, &phase.Disconnect{}, ) - return manager.Run() + return k.Manager.Run() } diff --git a/action/reset.go b/action/reset.go index 8bc0909c..3736ecb8 100644 --- a/action/reset.go +++ b/action/reset.go @@ -2,12 +2,12 @@ package action import ( "fmt" + "io" "os" "time" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/phase" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" log "github.com/sirupsen/logrus" "github.com/AlecAivazis/survey/v2" @@ -15,18 +15,15 @@ import ( ) type Reset struct { - Config *v1beta1.Cluster - Concurrency int - Force bool + // Manager is the phase manager + Manager *phase.Manager + Stdout io.Writer + Force bool } func (r Reset) Run() error { - if r.Config == nil { - return fmt.Errorf("config is nil") - } - if !r.Force { - if !isatty.IsTerminal(os.Stdout.Fd()) { + if stdoutFile, ok := r.Stdout.(*os.File); ok && !isatty.IsTerminal(stdoutFile.Fd()) { return fmt.Errorf("reset requires --force") } confirmed := false @@ -41,13 +38,12 @@ func (r Reset) Run() error { start := time.Now() - manager := phase.Manager{Config: r.Config, Concurrency: r.Concurrency} - for _, h := range r.Config.Spec.Hosts { + for _, h := range r.Manager.Config.Spec.Hosts { h.Reset = true } lockPhase := &phase.Lock{} - manager.AddPhase( + r.Manager.AddPhase( &phase.Connect{}, &phase.DetectOS{}, lockPhase, @@ -71,12 +67,12 @@ func (r Reset) Run() error { analytics.Client.Publish("reset-start", map[string]interface{}{}) - if err := manager.Run(); err != nil { - analytics.Client.Publish("reset-failure", map[string]interface{}{"clusterID": r.Config.Spec.K0s.Metadata.ClusterID}) + if err := r.Manager.Run(); err != nil { + analytics.Client.Publish("reset-failure", map[string]interface{}{"clusterID": r.Manager.Config.Spec.K0s.Metadata.ClusterID}) return err } - analytics.Client.Publish("reset-success", map[string]interface{}{"duration": time.Since(start), "clusterID": r.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("reset-success", map[string]interface{}{"duration": time.Since(start), "clusterID": r.Manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) diff --git a/cmd/apply.go b/cmd/apply.go index f2972cba..a4570d5f 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -1,10 +1,12 @@ package cmd import ( + "fmt" + "io" "os" "github.com/k0sproject/k0sctl/action" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" + "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) @@ -49,33 +51,34 @@ var applyCommand = &cli.Command{ analyticsFlag, upgradeCheckFlag, }, - Before: actions(initLogging, startCheckUpgrade, initConfig, displayLogo, initAnalytics, displayCopyright, warnOldCache), + Before: actions(initLogging, startCheckUpgrade, initConfig, initManager, displayLogo, initAnalytics, displayCopyright, warnOldCache), After: actions(reportCheckUpgrade, closeAnalytics), Action: func(ctx *cli.Context) error { - logWriter, err := LogFile() - if err != nil { - return err - } - - var lf *os.File + var kubeconfigOut io.Writer - if l, ok := logWriter.(*os.File); ok && l != nil { - lf = l + if kc := ctx.String("kubeconfig-out"); kc != "" { + out, err := os.OpenFile(kc, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return fmt.Errorf("failed to open kubeconfig-out file: %w", err) + } + defer out.Close() + kubeconfigOut = out } - applier := action.Apply{ + applyAction := action.Apply{ + Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), + KubeconfigOut: kubeconfigOut, + KubeconfigAPIAddress: ctx.String("kubeconfig-api-address"), NoWait: ctx.Bool("no-wait"), NoDrain: ctx.Bool("no-drain"), - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), - Concurrency: ctx.Int("concurrency"), - ConcurrentUploads: ctx.Int("concurrent-uploads"), DisableDowngradeCheck: ctx.Bool("disable-downgrade-check"), - KubeconfigOut: ctx.String("kubeconfig-out"), - KubeconfigAPIAddress: ctx.String("kubeconfig-api-address"), RestoreFrom: ctx.String("restore-from"), - LogFile: lf, } - return applier.Run() + if err := applyAction.Run(); err != nil { + return fmt.Errorf("apply failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) + } + + return nil }, } diff --git a/cmd/backup.go b/cmd/backup.go index ea10ea49..dabb1d8b 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -1,10 +1,10 @@ package cmd import ( - "os" + "fmt" "github.com/k0sproject/k0sctl/action" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" + "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) @@ -20,26 +20,17 @@ var backupCommand = &cli.Command{ analyticsFlag, upgradeCheckFlag, }, - Before: actions(initLogging, startCheckUpgrade, initConfig, displayLogo, initAnalytics, displayCopyright), + Before: actions(initLogging, startCheckUpgrade, initConfig, initManager, displayLogo, initAnalytics, displayCopyright), After: actions(reportCheckUpgrade, closeAnalytics), Action: func(ctx *cli.Context) error { - logWriter, err := LogFile() - if err != nil { - return err - } - - var lf *os.File - - if l, ok := logWriter.(*os.File); ok && l != nil { - lf = l + backupAction := action.Backup{ + Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), } - backupAction := action.Backup{ - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), - Concurrency: ctx.Int("concurrency"), - LogFile: lf, + if err := backupAction.Run(); err != nil { + return fmt.Errorf("backup failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } - return backupAction.Run() + return nil }, } diff --git a/cmd/config_edit.go b/cmd/config_edit.go index c2d1b3d1..a744b864 100644 --- a/cmd/config_edit.go +++ b/cmd/config_edit.go @@ -22,11 +22,10 @@ var configEditCommand = &cli.Command{ After: actions(reportCheckUpgrade, closeAnalytics), Action: func(ctx *cli.Context) error { configEditAction := action.ConfigEdit{ - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), - Concurrency: ctx.Int("concurrency"), - Stdout: ctx.App.Writer, - Stderr: ctx.App.ErrWriter, - Stdin: ctx.App.Reader, + Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), + Stdout: ctx.App.Writer, + Stderr: ctx.App.ErrWriter, + Stdin: ctx.App.Reader, } return configEditAction.Run() diff --git a/cmd/config_status.go b/cmd/config_status.go index 16bd0dd4..079354f8 100644 --- a/cmd/config_status.go +++ b/cmd/config_status.go @@ -27,10 +27,9 @@ var configStatusCommand = &cli.Command{ After: actions(reportCheckUpgrade, closeAnalytics), Action: func(ctx *cli.Context) error { configStatusAction := action.ConfigStatus{ - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), - Concurrency: ctx.Int("concurrency"), - Format: ctx.String("output"), - Writer: ctx.App.Writer, + Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), + Format: ctx.String("output"), + Writer: ctx.App.Writer, } return configStatusAction.Run() diff --git a/cmd/flags.go b/cmd/flags.go index fa9fbc24..e7a49f24 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -29,6 +29,8 @@ import ( ) type ctxConfigKey struct{} +type ctxManagerKey struct{} +type ctxLogFileKey struct{} var ( debugFlag = &cli.BoolFlag{ @@ -194,6 +196,25 @@ func closeAnalytics(_ *cli.Context) error { return nil } +func initManager(ctx *cli.Context) error { + c, ok := ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster) + if c == nil || !ok { + return fmt.Errorf("cluster config not available in context") + } + + manager, err := phase.NewManager(c) + if err != nil { + return fmt.Errorf("failed to initialize phase manager: %w", err) + } + + manager.Concurrency = ctx.Int("concurrency") + manager.ConcurrentUploads = ctx.Int("concurrent-uploads") + + ctx.Context = context.WithValue(ctx.Context, ctxManagerKey{}, manager) + + return nil +} + // initLogging initializes the logger func initLogging(ctx *cli.Context) error { log.SetLevel(log.TraceLevel) @@ -201,7 +222,7 @@ func initLogging(ctx *cli.Context) error { initScreenLogger(logLevelFromCtx(ctx, log.InfoLevel)) exec.DisableRedact = ctx.Bool("no-redact") rig.SetLogger(log.StandardLogger()) - return initFileLogger() + return initFileLogger(ctx) } // initSilentLogging initializes the logger in silent mode @@ -212,7 +233,7 @@ func initSilentLogging(ctx *cli.Context) error { exec.DisableRedact = ctx.Bool("no-redact") initScreenLogger(logLevelFromCtx(ctx, log.FatalLevel)) rig.SetLogger(log.StandardLogger()) - return initFileLogger() + return initFileLogger(ctx) } func logLevelFromCtx(ctx *cli.Context, defaultLevel log.Level) log.Level { @@ -229,18 +250,19 @@ func initScreenLogger(lvl log.Level) { log.AddHook(screenLoggerHook(lvl)) } -func initFileLogger() error { +func initFileLogger(ctx *cli.Context) error { lf, err := LogFile() if err != nil { return err } log.AddHook(fileLoggerHook(lf)) + ctx.Context = context.WithValue(ctx.Context, ctxLogFileKey{}, lf.Name()) return nil } const logPath = "k0sctl/k0sctl.log" -func LogFile() (io.Writer, error) { +func LogFile() (*os.File, error) { fn, err := xdg.SearchCacheFile(logPath) if err != nil { fn, err = xdg.CacheFile(logPath) diff --git a/cmd/kubeconfig.go b/cmd/kubeconfig.go index 0d3e0cc1..63e87102 100644 --- a/cmd/kubeconfig.go +++ b/cmd/kubeconfig.go @@ -5,7 +5,7 @@ import ( "github.com/k0sproject/k0sctl/action" "github.com/k0sproject/k0sctl/analytics" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" + "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) @@ -19,36 +19,27 @@ var kubeconfigCommand = &cli.Command{ Value: "", }, configFlag, - concurrencyFlag, debugFlag, traceFlag, redactFlag, analyticsFlag, }, - Before: actions(initSilentLogging, initConfig, initAnalytics), + Before: actions(initSilentLogging, initConfig, initManager, initAnalytics), After: func(_ *cli.Context) error { analytics.Client.Close() return nil }, Action: func(ctx *cli.Context) error { - var cfg *v1beta1.Cluster - if c, ok := ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster); ok { - cfg = c - } else { - return fmt.Errorf("config is nil") - } - kubeconfigAction := action.Kubeconfig{ - Config: cfg, - Concurrency: ctx.Int("concurrency"), + Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), KubeconfigAPIAddress: ctx.String("address"), } if err := kubeconfigAction.Run(); err != nil { - return err + return fmt.Errorf("getting kubeconfig failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } - _, err := fmt.Fprintf(ctx.App.Writer, "%s\n", cfg.Metadata.Kubeconfig) + _, err := fmt.Fprintf(ctx.App.Writer, "%s\n", kubeconfigAction.Manager.Config.Metadata.Kubeconfig) return err }, } diff --git a/cmd/reset.go b/cmd/reset.go index 771f19ea..01b5878b 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -1,8 +1,10 @@ package cmd import ( + "fmt" + "github.com/k0sproject/k0sctl/action" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" + "github.com/k0sproject/k0sctl/phase" "github.com/urfave/cli/v2" ) @@ -24,15 +26,19 @@ var resetCommand = &cli.Command{ Aliases: []string{"f"}, }, }, - Before: actions(initLogging, startCheckUpgrade, initConfig, initAnalytics, displayCopyright), + Before: actions(initLogging, startCheckUpgrade, initConfig, initManager, initAnalytics, displayCopyright), After: actions(reportCheckUpgrade, closeAnalytics), Action: func(ctx *cli.Context) error { resetAction := action.Reset{ - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), - Concurrency: ctx.Int("concurrency"), - Force: ctx.Bool("force"), + Manager: ctx.Context.Value(ctxManagerKey{}).(*phase.Manager), + Force: ctx.Bool("force"), + Stdout: ctx.App.Writer, + } + + if err := resetAction.Run(); err != nil { + return fmt.Errorf("reset failed - log file saved to %s: %w", ctx.Context.Value(ctxLogFileKey{}).(string), err) } - return resetAction.Run() + return nil }, } diff --git a/phase/manager.go b/phase/manager.go index b9492362..85f4a5d7 100644 --- a/phase/manager.go +++ b/phase/manager.go @@ -1,6 +1,8 @@ package phase import ( + "fmt" + "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" "github.com/logrusorgru/aurora" @@ -54,6 +56,15 @@ type Manager struct { ConcurrentUploads int } +// NewManager creates a new Manager +func NewManager(config *v1beta1.Cluster) (*Manager, error) { + if config == nil { + return nil, fmt.Errorf("config is nil") + } + + return &Manager{Config: config}, nil +} + // AddPhase adds a Phase to Manager func (m *Manager) AddPhase(p ...phase) { m.phases = append(m.phases, p...)