diff --git a/cmd/apply.go b/cmd/apply.go index 537df418..79bb1121 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -76,6 +76,7 @@ var applyCommand = &cli.Command{ &phase.ValidateHosts{}, &phase.GatherK0sFacts{}, &phase.ValidateFacts{SkipDowngradeCheck: ctx.Bool("disable-downgrade-check")}, + &phase.RunHooks{Stage: "before", Action: "apply"}, &phase.ConfigureK0s{}, &phase.Restore{ RestoreFrom: ctx.String("restore-from"), @@ -87,6 +88,7 @@ var applyCommand = &cli.Command{ &phase.UpgradeWorkers{ NoDrain: ctx.Bool("no-drain"), }, + &phase.RunHooks{Stage: "after", Action: "apply"}, &phase.Disconnect{}, ) diff --git a/cmd/backup.go b/cmd/backup.go index a3228cf2..f4dc09cf 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -47,7 +47,9 @@ var backupCommand = &cli.Command{ &phase.DetectOS{}, &phase.GatherFacts{}, &phase.GatherK0sFacts{}, + &phase.RunHooks{Stage: "before", Action: "backup"}, &phase.Backup{}, + &phase.RunHooks{Stage: "after", Action: "backup"}, &phase.Disconnect{}, ) diff --git a/cmd/reset.go b/cmd/reset.go index 62d5b97a..3461d31f 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -70,7 +70,9 @@ var resetCommand = &cli.Command{ &phase.DetectOS{}, &phase.PrepareHosts{}, &phase.GatherK0sFacts{}, + &phase.RunHooks{Stage: "before", Action: "reset"}, &phase.Reset{}, + &phase.RunHooks{Stage: "after", Action: "reset"}, &phase.Disconnect{}, ) diff --git a/config/cluster/hook.go b/config/cluster/hook.go new file mode 100644 index 00000000..6fcfc066 --- /dev/null +++ b/config/cluster/hook.go @@ -0,0 +1,12 @@ +package cluster + +// Hooks define a list of hooks such as hooks["apply"]["before"] = ["ls -al", "rm foo.txt"] +type Hooks map[string]map[string][]string + +// ForActionAndStage return hooks for given action and stage +func (h Hooks) ForActionAndStage(action, stage string) []string { + if len(h[action]) > 0 { + return h[action][stage] + } + return nil +} diff --git a/config/cluster/host.go b/config/cluster/host.go index 821ad4cf..705e9361 100644 --- a/config/cluster/host.go +++ b/config/cluster/host.go @@ -28,6 +28,7 @@ type Host struct { InstallFlags Flags `yaml:"installFlags,omitempty"` Files []UploadFile `yaml:"files,omitempty"` OSIDOverride string `yaml:"os,omitempty"` + Hooks Hooks `yaml:"hooks,omitempty"` Metadata HostMetadata `yaml:"-"` Configurer configurer `yaml:"-"` diff --git a/phase/runhooks.go b/phase/runhooks.go new file mode 100644 index 00000000..3a332657 --- /dev/null +++ b/phase/runhooks.go @@ -0,0 +1,86 @@ +package phase + +import ( + "fmt" + "strings" + "sync" + + "github.com/k0sproject/k0sctl/config" + "github.com/k0sproject/k0sctl/config/cluster" +) + +var _ phase = &RunHooks{} + +// RunHooks phase runs a set of hooks configured for the host +type RunHooks struct { + Action string + Stage string + + steps map[*cluster.Host][]string +} + +// Title for the phase +func (p *RunHooks) Title() string { + return fmt.Sprintf("Run %s %s Hooks", strings.Title(p.Stage), strings.Title(p.Action)) +} + +// Prepare digs out the hosts with steps from the config +func (p *RunHooks) Prepare(config *config.Cluster) error { + p.steps = make(map[*cluster.Host][]string) + for _, h := range config.Spec.Hosts { + if len(h.Hooks) > 0 { + p.steps[h] = h.Hooks.ForActionAndStage(p.Action, p.Stage) + } + } + + return nil +} + +// ShouldRun is true when there are hosts that need to be connected +func (p *RunHooks) ShouldRun() bool { + return len(p.steps) > 0 +} + +// Run does all the prep work on the hosts in parallel +func (p *RunHooks) Run() error { + var wg sync.WaitGroup + var errors []string + type erritem struct { + host string + err error + } + ec := make(chan erritem, 1) + + wg.Add(len(p.steps)) + + for h, steps := range p.steps { + go func(h *cluster.Host, steps []string) { + for _, s := range steps { + err := h.Exec(s) + if err != nil { + ec <- erritem{h.String(), err} + return // do not exec remaining steps if one fails + } + } + + ec <- erritem{h.String(), nil} + }(h, steps) + } + + go func() { + for e := range ec { + if e.err != nil { + errors = append(errors, fmt.Sprintf("%s: %s", e.host, e.err.Error())) + } + wg.Done() + } + }() + + wg.Wait() + + if len(errors) > 0 { + return fmt.Errorf("failed on %d hosts:\n - %s", len(errors), strings.Join(errors, "\n - ")) + } + + return nil +} diff --git a/smoke-test/k0sctl.yaml.tpl b/smoke-test/k0sctl.yaml.tpl index 961f6e41..db284c83 100644 --- a/smoke-test/k0sctl.yaml.tpl +++ b/smoke-test/k0sctl.yaml.tpl @@ -9,6 +9,12 @@ spec: address: "127.0.0.1" port: 9022 keyPath: ./id_rsa_k0s + hooks: + apply: + before: + - "date > /before-apply.hook" + after: + - "date > /after-apply.hook" - role: worker uploadBinary: true os: "$OS_OVERRIDE"