Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract logic from cmd into reusable actions #497

Merged
merged 6 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions action/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package action

import (
"fmt"
"io"
"time"

"github.com/k0sproject/k0sctl/analytics"
"github.com/k0sproject/k0sctl/phase"

log "github.com/sirupsen/logrus"
)

type Apply struct {
// Manager is the phase manager
Manager *phase.Manager
// DisableDowngradeCheck skips the downgrade check
DisableDowngradeCheck bool
// Force allows forced installation in case of certain failures
Force bool
// NoWait skips waiting for the cluster to be ready
NoWait bool
// NoDrain skips draining worker nodes
NoDrain bool
// RestoreFrom is the path to a cluster backup archive to restore the state from
RestoreFrom 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
}

func (a Apply) Run() error {
start := time.Now()

phase.NoWait = a.NoWait
phase.Force = a.Force

lockPhase := &phase.Lock{}

a.Manager.AddPhase(
&phase.Connect{},
&phase.DetectOS{},
lockPhase,
&phase.PrepareHosts{},
&phase.GatherFacts{},
&phase.DownloadBinaries{},
&phase.UploadFiles{},
&phase.ValidateHosts{},
&phase.GatherK0sFacts{},
&phase.ValidateFacts{SkipDowngradeCheck: a.DisableDowngradeCheck},
&phase.UploadBinaries{},
&phase.DownloadK0s{},
&phase.InstallBinaries{},
&phase.RunHooks{Stage: "before", Action: "apply"},
&phase.PrepareArm{},
&phase.ConfigureK0s{},
&phase.Restore{
RestoreFrom: a.RestoreFrom,
},
&phase.InitializeK0s{},
&phase.InstallControllers{},
&phase.InstallWorkers{},
&phase.UpgradeControllers{},
&phase.UpgradeWorkers{NoDrain: a.NoDrain},
&phase.ResetWorkers{NoDrain: a.NoDrain},
&phase.ResetControllers{NoDrain: a.NoDrain},
&phase.RunHooks{Stage: "after", Action: "apply"},
)

if a.KubeconfigOut != nil {
a.Manager.AddPhase(&phase.GetKubeconfig{APIAddress: a.KubeconfigAPIAddress})
}

a.Manager.AddPhase(
&phase.Unlock{Cancel: lockPhase.Cancel},
&phase.Disconnect{},
)

analytics.Client.Publish("apply-start", map[string]interface{}{})

var result error

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": 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)
}
}

duration := time.Since(start).Truncate(time.Second)
text := fmt.Sprintf("==> Finished in %s", duration)
log.Infof(phase.Colorize.Green(text).String())

uninstalled := false
for _, host := range a.Manager.Config.Spec.Hosts {
if host.Reset {
uninstalled = true
}
}
if uninstalled {
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", a.Manager.Config.Spec.K0s.Version)

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())
}

return nil
}
49 changes: 49 additions & 0 deletions action/backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package action

import (
"fmt"
"time"

"github.com/k0sproject/k0sctl/analytics"
"github.com/k0sproject/k0sctl/phase"
log "github.com/sirupsen/logrus"
)

type Backup struct {
// Manager is the phase manager
Manager *phase.Manager
}

func (b Backup) Run() error {
start := time.Now()

lockPhase := &phase.Lock{}

b.Manager.AddPhase(
&phase.Connect{},
&phase.DetectOS{},
lockPhase,
&phase.PrepareHosts{},
&phase.GatherFacts{},
&phase.GatherK0sFacts{},
&phase.RunHooks{Stage: "before", Action: "backup"},
&phase.Backup{},
&phase.RunHooks{Stage: "after", Action: "backup"},
&phase.Unlock{Cancel: lockPhase.Cancel},
&phase.Disconnect{},
)

analytics.Client.Publish("backup-start", map[string]interface{}{})

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": b.Manager.Config.Spec.K0s.Metadata.ClusterID})

duration := time.Since(start).Truncate(time.Second)
text := fmt.Sprintf("==> Finished in %s", duration)
log.Infof(phase.Colorize.Green(text).String())
return nil
}
105 changes: 105 additions & 0 deletions action/config_edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package action

import (
"fmt"
"io"
"os"

"github.com/k0sproject/k0sctl/analytics"
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
"github.com/k0sproject/rig/exec"

osexec "os/exec"

"github.com/mattn/go-isatty"
)

type ConfigEdit struct {
Config *v1beta1.Cluster
Stdout io.Writer
Stderr io.Writer
Stdin io.Reader
}

func (c ConfigEdit) Run() error {
stdoutFile, ok := c.Stdout.(*os.File)

if !ok || !isatty.IsTerminal(stdoutFile.Fd()) {
return fmt.Errorf("output is not a terminal")
}

analytics.Client.Publish("config-edit-start", map[string]interface{}{})

editor, err := shellEditor()
if err != nil {
return err
}

h := c.Config.Spec.K0sLeader()

if err := h.Connect(); err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
defer h.Disconnect()

if err := h.ResolveConfigurer(); err != nil {
return err
}

oldCfg, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "-n kube-system get clusterconfig k0s -o yaml"), exec.Sudo(h))
if err != nil {
return fmt.Errorf("%s: %w", h, err)
}

tmpFile, err := os.CreateTemp("", "k0s-config.*.yaml")
if err != nil {
return err
}
defer func() { _ = os.Remove(tmpFile.Name()) }()

if _, err := tmpFile.WriteString(oldCfg); err != nil {
return err
}

if err := tmpFile.Close(); err != nil {
return err
}

cmd := osexec.Command(editor, tmpFile.Name())
cmd.Stdin = c.Stdin
cmd.Stdout = c.Stdout
cmd.Stderr = c.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to start editor (%s): %w", cmd.String(), err)
}

newCfgBytes, err := os.ReadFile(tmpFile.Name())
if err != nil {
return err
}
newCfg := string(newCfgBytes)

if newCfg == oldCfg {
return fmt.Errorf("configuration was not changed, aborting")
}

if err := h.Exec(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "apply -n kube-system -f -"), exec.Stdin(newCfg), exec.Sudo(h)); err != nil {
return err
}

return nil
}

func shellEditor() (string, error) {
if v := os.Getenv("VISUAL"); v != "" {
return v, nil
}
if v := os.Getenv("EDITOR"); v != "" {
return v, nil
}
if path, err := osexec.LookPath("vi"); err == nil {
return path, nil
}

return "", fmt.Errorf("could not detect shell editor ($VISUAL, $EDITOR)")
}
44 changes: 44 additions & 0 deletions action/config_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package action

import (
"fmt"
"io"

"github.com/k0sproject/k0sctl/analytics"
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
"github.com/k0sproject/rig/exec"
)

type ConfigStatus struct {
Config *v1beta1.Cluster
Concurrency int
Format string
Writer io.Writer
}

func (c ConfigStatus) Run() error {
analytics.Client.Publish("config-status-start", map[string]interface{}{})

h := c.Config.Spec.K0sLeader()

if err := h.Connect(); err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
defer h.Disconnect()

if err := h.ResolveConfigurer(); err != nil {
return err
}
format := c.Format
if format != "" {
format = "-o " + format
}

output, err := h.ExecOutput(h.Configurer.KubectlCmdf(h, h.K0sDataDir(), "-n kube-system get event --field-selector involvedObject.name=k0s %s", format), exec.Sudo(h))
if err != nil {
return fmt.Errorf("%s: %w", h, err)
}
fmt.Fprintln(c.Writer, output)

return nil
}
29 changes: 29 additions & 0 deletions action/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package action

import (
"github.com/k0sproject/k0sctl/phase"
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
)

type Kubeconfig struct {
// Manager is the phase manager
Manager *phase.Manager
KubeconfigAPIAddress string

Kubeconfig string
}

func (k *Kubeconfig) Run() error {
// Change so that the internal config has only single controller host as we
// do not need to connect to all nodes
k.Manager.Config.Spec.Hosts = cluster.Hosts{k.Manager.Config.Spec.K0sLeader()}

k.Manager.AddPhase(
&phase.Connect{},
&phase.DetectOS{},
&phase.GetKubeconfig{APIAddress: k.KubeconfigAPIAddress},
&phase.Disconnect{},
)

return k.Manager.Run()
}
Loading
Loading