mirror of https://github.com/k3s-io/k3s
Merge pull request #69684 from fabriziopandini/kubeadm-add-phase-runner2
kubeadm add phase runnerpull/58/head
commit
7d45044c11
|
@ -97,6 +97,7 @@ filegroup(
|
|||
srcs = [
|
||||
":package-srcs",
|
||||
"//cmd/kubeadm/app/cmd/phases/certs:all-srcs",
|
||||
"//cmd/kubeadm/app/cmd/phases/workflow:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"phase.go",
|
||||
"runner.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"doc_test.go",
|
||||
"runner_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Package workflow implements a workflow manager to be used for
|
||||
implementing composable kubeadm workflows.
|
||||
|
||||
Composable kubeadm workflows are built by an ordered sequence of phases;
|
||||
each phase can have it's own, nested, ordered sequence of sub phases.
|
||||
For instance
|
||||
|
||||
preflight Run master pre-flight checks
|
||||
certs Generates all PKI assets necessary to establish the control plane
|
||||
/ca Generates a self-signed kubernetes CA to provision identities for Kubernetes components
|
||||
/apiserver Generates an API server serving certificate and key
|
||||
...
|
||||
kubeconfig Generates all kubeconfig files necessary to establish the control plane
|
||||
/admin Generates a kubeconfig file for the admin to use and for kubeadm itself
|
||||
/kubelet Generates a kubeconfig file for the kubelet to use.
|
||||
...
|
||||
...
|
||||
|
||||
Phases are designed to be reusable across different kubeadm workflows thus allowing
|
||||
e.g. reuse of phase certs in both kubeadm init and kubeadm join --control-plane workflows.
|
||||
|
||||
Each workflow can be defined and managed using a Runner, that will run all
|
||||
the phases according to the given order; nested phases will be executed immediately
|
||||
after their parent phase.
|
||||
|
||||
The Runner behavior can be changed by setting the RunnerOptions, typically
|
||||
exposed as kubeadm command line flags, thus allowing to filter the list of phases
|
||||
to be executed.
|
||||
*/
|
||||
package workflow
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var myWorkflowRunner = NewRunner()
|
||||
|
||||
type myWorkflowData struct {
|
||||
data string
|
||||
}
|
||||
|
||||
func (c *myWorkflowData) Data() string {
|
||||
return c.data
|
||||
}
|
||||
|
||||
type myPhaseData interface {
|
||||
Data() string
|
||||
}
|
||||
|
||||
func ExamplePhase() {
|
||||
// Create a phase
|
||||
var myPhase1 = Phase{
|
||||
Name: "myPhase1",
|
||||
Short: "A phase of a kubeadm composable workflow...",
|
||||
Run: func(data RunData) error {
|
||||
// transform data into a typed data struct
|
||||
d, ok := data.(myPhaseData)
|
||||
if !ok {
|
||||
return errors.New("invalid RunData type")
|
||||
}
|
||||
|
||||
// implement your phase logic...
|
||||
fmt.Printf("%v", d.Data())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create another phase
|
||||
var myPhase2 = Phase{
|
||||
Name: "myPhase2",
|
||||
Short: "Another phase of a kubeadm composable workflow...",
|
||||
Run: func(data RunData) error {
|
||||
// transform data into a typed data struct
|
||||
d, ok := data.(myPhaseData)
|
||||
if !ok {
|
||||
return errors.New("invalid RunData type")
|
||||
}
|
||||
|
||||
// implement your phase logic...
|
||||
fmt.Printf("%v", d.Data())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Adds the new phases to the workflow
|
||||
// Phases will be executed the same order they are added to the workflow
|
||||
myWorkflowRunner.AppendPhase(myPhase1)
|
||||
myWorkflowRunner.AppendPhase(myPhase2)
|
||||
}
|
||||
|
||||
func ExampleRunner_Run() {
|
||||
// Create a phase
|
||||
var myPhase = Phase{
|
||||
Name: "myPhase",
|
||||
Short: "A phase of a kubeadm composable workflow...",
|
||||
Run: func(data RunData) error {
|
||||
// transform data into a typed data struct
|
||||
d, ok := data.(myPhaseData)
|
||||
if !ok {
|
||||
return errors.New("invalid RunData type")
|
||||
}
|
||||
|
||||
// implement your phase logic...
|
||||
fmt.Printf("%v", d.Data())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Adds the new phase to the workflow
|
||||
var myWorkflowRunner = NewRunner()
|
||||
myWorkflowRunner.AppendPhase(myPhase)
|
||||
|
||||
// Defines the method that creates the runtime data shared
|
||||
// among all the phases included in the workflow
|
||||
myWorkflowRunner.SetDataInitializer(func() (RunData, error) {
|
||||
return myWorkflowData{data: "some data"}, nil
|
||||
})
|
||||
|
||||
// Runs the workflow
|
||||
myWorkflowRunner.Run()
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package workflow
|
||||
|
||||
// Phase provides an implementation of a workflow phase that allows
|
||||
// creation of new phases by simply instantiating a variable of this type.
|
||||
type Phase struct {
|
||||
// name of the phase.
|
||||
// Phase name should be unique among peer phases (phases belonging to
|
||||
// the same workflow or phases belonging to the same parent phase).
|
||||
Name string
|
||||
|
||||
// Short description of the phase.
|
||||
Short string
|
||||
|
||||
// Long returns the long description of the phase.
|
||||
Long string
|
||||
|
||||
// Example returns the example for the phase.
|
||||
Example string
|
||||
|
||||
// Hidden define if the phase should be hidden in the workflow help.
|
||||
// e.g. PrintFilesIfDryRunning phase in the kubeadm init workflow is candidate for being hidden to the users
|
||||
Hidden bool
|
||||
|
||||
// Phases defines a nested, ordered sequence of phases.
|
||||
Phases []Phase
|
||||
|
||||
// Run defines a function implementing the phase action.
|
||||
// It is recommended to implent type assertion, e.g. using golang type switch,
|
||||
// for validating the RunData type.
|
||||
Run func(data RunData) error
|
||||
|
||||
// RunIf define a function that implements a condition that should be checked
|
||||
// before executing the phase action.
|
||||
// If this function return nil, the phase action is always executed.
|
||||
RunIf func(data RunData) (bool, error)
|
||||
}
|
||||
|
||||
// AppendPhase adds the given phase to the nested, ordered sequence of phases.
|
||||
func (t *Phase) AppendPhase(phase Phase) {
|
||||
t.Phases = append(t.Phases, phase)
|
||||
}
|
|
@ -0,0 +1,382 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// phaseSeparator defines the separator to be used when concatenating nested
|
||||
// phase names
|
||||
const phaseSeparator = "/"
|
||||
|
||||
// RunnerOptions defines the options supported during the execution of a
|
||||
// kubeadm composable workflows
|
||||
type RunnerOptions struct {
|
||||
// FilterPhases defines the list of phases to be executed (if empty, all).
|
||||
FilterPhases []string
|
||||
|
||||
// SkipPhases defines the list of phases to be excluded by execution (if empty, none).
|
||||
SkipPhases []string
|
||||
}
|
||||
|
||||
// RunData defines the data shared among all the phases included in the workflow, that is any type.
|
||||
type RunData = interface{}
|
||||
|
||||
// Runner implements management of composable kubeadm workflows.
|
||||
type Runner struct {
|
||||
// Options that regulate the runner behavior.
|
||||
Options RunnerOptions
|
||||
|
||||
// Phases composing the workflow to be managed by the runner.
|
||||
Phases []Phase
|
||||
|
||||
// runDataInitializer defines a function that creates the runtime data shared
|
||||
// among all the phases included in the workflow
|
||||
runDataInitializer func() (RunData, error)
|
||||
|
||||
// runData is part of the internal state of the runner and it is used for implementing
|
||||
// a singleton in the InitData methods (thus avoiding to initialize data
|
||||
// more than one time)
|
||||
runData RunData
|
||||
|
||||
// phaseRunners is part of the internal state of the runner and provides
|
||||
// a list of wrappers to phases composing the workflow with contextual
|
||||
// information supporting phase execution.
|
||||
phaseRunners []*phaseRunner
|
||||
}
|
||||
|
||||
// phaseRunner provides a wrapper to a Phase with the addition of a set
|
||||
// of contextual information derived by the workflow managed by the Runner.
|
||||
// TODO: If we ever decide to get more sophisticated we can swap this type with a well defined dag or tree library.
|
||||
type phaseRunner struct {
|
||||
// Phase provide access to the phase implementation
|
||||
Phase
|
||||
|
||||
// provide access to the parent phase in the workflow managed by the Runner.
|
||||
parent *phaseRunner
|
||||
|
||||
// level define the level of nesting of this phase into the workflow managed by
|
||||
// the Runner.
|
||||
level int
|
||||
|
||||
// selfPath contains all the elements of the path that identify the phase into
|
||||
// the workflow managed by the Runner.
|
||||
selfPath []string
|
||||
|
||||
// generatedName is the full name of the phase, that corresponds to the absolute
|
||||
// path of the phase in the the workflow managed by the Runner.
|
||||
generatedName string
|
||||
|
||||
// use is the phase usage string that will be printed in the workflow help.
|
||||
// It corresponds to the relative path of the phase in the workflow managed by the Runner.
|
||||
use string
|
||||
}
|
||||
|
||||
// NewRunner return a new runner for composable kubeadm workflows.
|
||||
func NewRunner() *Runner {
|
||||
return &Runner{
|
||||
Phases: []Phase{},
|
||||
}
|
||||
}
|
||||
|
||||
// AppendPhase adds the given phase to the ordered sequence of phases managed by the runner.
|
||||
func (e *Runner) AppendPhase(t Phase) {
|
||||
e.Phases = append(e.Phases, t)
|
||||
}
|
||||
|
||||
// computePhaseRunFlags return a map defining which phase should be run and which not.
|
||||
// PhaseRunFlags are computed according to RunnerOptions.
|
||||
func (e *Runner) computePhaseRunFlags() (map[string]bool, error) {
|
||||
// Initialize support data structure
|
||||
phaseRunFlags := map[string]bool{}
|
||||
phaseHierarchy := map[string][]string{}
|
||||
e.visitAll(func(p *phaseRunner) error {
|
||||
// Initialize phaseRunFlags assuming that all the phases should be run.
|
||||
phaseRunFlags[p.generatedName] = true
|
||||
|
||||
// Initialize phaseHierarchy for the current phase (the list of phases
|
||||
// depending on the current phase
|
||||
phaseHierarchy[p.generatedName] = []string{}
|
||||
|
||||
// Register current phase as part of its own parent hierarchy
|
||||
parent := p.parent
|
||||
for parent != nil {
|
||||
phaseHierarchy[parent.generatedName] = append(phaseHierarchy[parent.generatedName], p.generatedName)
|
||||
parent = parent.parent
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// If a filter option is specified, set all phaseRunFlags to false except for
|
||||
// the phases included in the filter and their hierarchy of nested phases.
|
||||
if len(e.Options.FilterPhases) > 0 {
|
||||
for i := range phaseRunFlags {
|
||||
phaseRunFlags[i] = false
|
||||
}
|
||||
for _, f := range e.Options.FilterPhases {
|
||||
if _, ok := phaseRunFlags[f]; !ok {
|
||||
return phaseRunFlags, fmt.Errorf("invalid phase name: %s", f)
|
||||
}
|
||||
phaseRunFlags[f] = true
|
||||
for _, c := range phaseHierarchy[f] {
|
||||
phaseRunFlags[c] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a phase skip option is specified, set the corresponding phaseRunFlags
|
||||
// to false and apply the same change to the underlying hierarchy
|
||||
for _, f := range e.Options.SkipPhases {
|
||||
if _, ok := phaseRunFlags[f]; !ok {
|
||||
return phaseRunFlags, fmt.Errorf("invalid phase name: %s", f)
|
||||
}
|
||||
phaseRunFlags[f] = false
|
||||
for _, c := range phaseHierarchy[f] {
|
||||
phaseRunFlags[c] = false
|
||||
}
|
||||
}
|
||||
|
||||
return phaseRunFlags, nil
|
||||
}
|
||||
|
||||
// SetDataInitializer allows to setup a function that initialize the runtime data shared
|
||||
// among all the phases included in the workflow.
|
||||
func (e *Runner) SetDataInitializer(builder func() (RunData, error)) {
|
||||
e.runDataInitializer = builder
|
||||
}
|
||||
|
||||
// InitData triggers the creation of runtime data shared among all the phases included in the workflow.
|
||||
// This action can be executed explicitly out, when it is necessary to get the RunData
|
||||
// before actually executing Run, or implicitly when invoking Run.
|
||||
func (e *Runner) InitData() (RunData, error) {
|
||||
if e.runData == nil && e.runDataInitializer != nil {
|
||||
var err error
|
||||
if e.runData, err = e.runDataInitializer(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return e.runData, nil
|
||||
}
|
||||
|
||||
// Run the kubeadm composable kubeadm workflows.
|
||||
func (e *Runner) Run() error {
|
||||
e.prepareForExecution()
|
||||
|
||||
// determine which phase should be run according to RunnerOptions
|
||||
phaseRunFlags, err := e.computePhaseRunFlags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// builds the runner data
|
||||
var data RunData
|
||||
if data, err = e.InitData(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = e.visitAll(func(p *phaseRunner) error {
|
||||
// if the phase should not be run, skip the phase.
|
||||
if run, ok := phaseRunFlags[p.generatedName]; !run || !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the phase defines a condition to be checked before executing the phase action.
|
||||
if p.RunIf != nil {
|
||||
// Check the condition and returns if the condition isn't satisfied (or fails)
|
||||
ok, err := p.RunIf(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error execution run condition for phase %s: %v", p.generatedName, err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the phase action (if defined)
|
||||
if p.Run != nil {
|
||||
if err := p.Run(data); err != nil {
|
||||
return fmt.Errorf("error execution phase %s: %v", p.generatedName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Help returns text with the list of phases included in the workflow.
|
||||
func (e *Runner) Help(cmdUse string) string {
|
||||
e.prepareForExecution()
|
||||
|
||||
// computes the max length of for each phase use line
|
||||
maxLength := 0
|
||||
e.visitAll(func(p *phaseRunner) error {
|
||||
if !p.Hidden {
|
||||
length := len(p.use)
|
||||
if maxLength < length {
|
||||
maxLength = length
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// prints the list of phases indented by level and formatted using the maxlength
|
||||
// the list is enclosed in a mardown code block for ensuring better readability in the public web site
|
||||
line := fmt.Sprintf("The %q command executes the following internal workflow:\n", cmdUse)
|
||||
line += "```\n"
|
||||
offset := 2
|
||||
e.visitAll(func(p *phaseRunner) error {
|
||||
if !p.Hidden {
|
||||
padding := maxLength - len(p.use) + offset
|
||||
line += strings.Repeat(" ", offset*p.level) // indentation
|
||||
line += p.use // name + aliases
|
||||
line += strings.Repeat(" ", padding) // padding right up to max length (+ offset for spacing)
|
||||
line += p.Short // phase short description
|
||||
line += "\n"
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
line += "```"
|
||||
return line
|
||||
}
|
||||
|
||||
// BindToCommand bind the Runner to a cobra command by altering
|
||||
// command help, adding phase related flags and by adding phases subcommands
|
||||
// Please note that this command needs to be done once all the phases are added to the Runner.
|
||||
func (e *Runner) BindToCommand(cmd *cobra.Command) {
|
||||
if len(e.Phases) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// alters the command description to show available phases
|
||||
if cmd.Long != "" {
|
||||
cmd.Long = fmt.Sprintf("%s\n\n%s\n", cmd.Long, e.Help(cmd.Use))
|
||||
} else {
|
||||
cmd.Long = fmt.Sprintf("%s\n\n%s\n", cmd.Short, e.Help(cmd.Use))
|
||||
}
|
||||
|
||||
// adds phase related flags
|
||||
cmd.Flags().StringSliceVar(&e.Options.SkipPhases, "skip-phases", nil, "List of phases to be skipped")
|
||||
|
||||
// adds the phases subcommand
|
||||
phaseCommand := &cobra.Command{
|
||||
Use: "phase",
|
||||
Short: fmt.Sprintf("use this command to invoke single phase of the %s workflow", cmd.Name()),
|
||||
Args: cobra.NoArgs, // this forces cobra to fail if a wrong phase name is passed
|
||||
}
|
||||
|
||||
cmd.AddCommand(phaseCommand)
|
||||
|
||||
// generate all the nested subcommands for invoking single phases
|
||||
subcommands := map[string]*cobra.Command{}
|
||||
e.visitAll(func(p *phaseRunner) error {
|
||||
// creates nested phase subcommand
|
||||
var phaseCmd = &cobra.Command{
|
||||
Use: strings.ToLower(p.Name),
|
||||
Short: p.Short,
|
||||
Long: p.Long,
|
||||
Example: p.Example,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
e.Options.FilterPhases = []string{p.generatedName}
|
||||
if err := e.Run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
Args: cobra.NoArgs, // this forces cobra to fail if a wrong phase name is passed
|
||||
}
|
||||
|
||||
// makes the new command inherits flags from the main command
|
||||
cmd.LocalNonPersistentFlags().VisitAll(func(f *pflag.Flag) {
|
||||
phaseCmd.Flags().AddFlag(f)
|
||||
})
|
||||
|
||||
// adds the command to parent
|
||||
if p.level == 0 {
|
||||
phaseCommand.AddCommand(phaseCmd)
|
||||
} else {
|
||||
subcommands[p.parent.generatedName].AddCommand(phaseCmd)
|
||||
}
|
||||
|
||||
subcommands[p.generatedName] = phaseCmd
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// visitAll provides a utility method for visiting all the phases in the workflow
|
||||
// in the execution order and executing a func on each phase.
|
||||
// Nested phase are visited immediately after their parent phase.
|
||||
func (e *Runner) visitAll(fn func(*phaseRunner) error) error {
|
||||
for _, currentRunner := range e.phaseRunners {
|
||||
if err := fn(currentRunner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareForExecution initialize the internal state of the Runner (the list of phaseRunner).
|
||||
func (e *Runner) prepareForExecution() {
|
||||
e.phaseRunners = []*phaseRunner{}
|
||||
var parentRunner *phaseRunner
|
||||
for _, phase := range e.Phases {
|
||||
addPhaseRunner(e, parentRunner, phase)
|
||||
}
|
||||
}
|
||||
|
||||
// addPhaseRunner adds the phaseRunner for a given phase to the phaseRunners list
|
||||
func addPhaseRunner(e *Runner, parentRunner *phaseRunner, phase Phase) {
|
||||
// computes contextual information derived by the workflow managed by the Runner.
|
||||
generatedName := strings.ToLower(phase.Name)
|
||||
use := generatedName
|
||||
selfPath := []string{generatedName}
|
||||
|
||||
if parentRunner != nil {
|
||||
generatedName = strings.Join([]string{parentRunner.generatedName, generatedName}, phaseSeparator)
|
||||
use = fmt.Sprintf("%s%s", phaseSeparator, use)
|
||||
selfPath = append(parentRunner.selfPath, selfPath...)
|
||||
}
|
||||
|
||||
// creates the phaseRunner
|
||||
currentRunner := &phaseRunner{
|
||||
Phase: phase,
|
||||
parent: parentRunner,
|
||||
level: len(selfPath) - 1,
|
||||
selfPath: selfPath,
|
||||
generatedName: generatedName,
|
||||
use: use,
|
||||
}
|
||||
|
||||
// adds to the phaseRunners list
|
||||
e.phaseRunners = append(e.phaseRunners, currentRunner)
|
||||
|
||||
// iterate for the nested, ordered list of phases, thus storing
|
||||
// phases in the expected executing order (child phase are stored immediately after their parent phase).
|
||||
for _, childPhase := range phase.Phases {
|
||||
addPhaseRunner(e, currentRunner, childPhase)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func phaseBuilder(name string, phases ...Phase) Phase {
|
||||
return Phase{
|
||||
Name: name,
|
||||
Short: fmt.Sprintf("long description for %s ...", name),
|
||||
Phases: phases,
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputePhaseRunFlags(t *testing.T) {
|
||||
|
||||
var usecases = []struct {
|
||||
name string
|
||||
options RunnerOptions
|
||||
expected map[string]bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "no options > all phases",
|
||||
options: RunnerOptions{},
|
||||
expected: map[string]bool{"foo": true, "foo/bar": true, "foo/baz": true, "qux": true},
|
||||
},
|
||||
{
|
||||
name: "options can filter phases",
|
||||
options: RunnerOptions{FilterPhases: []string{"foo/baz", "qux"}},
|
||||
expected: map[string]bool{"foo": false, "foo/bar": false, "foo/baz": true, "qux": true},
|
||||
},
|
||||
{
|
||||
name: "options can filter phases - hierarchy is considered",
|
||||
options: RunnerOptions{FilterPhases: []string{"foo"}},
|
||||
expected: map[string]bool{"foo": true, "foo/bar": true, "foo/baz": true, "qux": false},
|
||||
},
|
||||
{
|
||||
name: "options can skip phases",
|
||||
options: RunnerOptions{SkipPhases: []string{"foo/bar", "qux"}},
|
||||
expected: map[string]bool{"foo": true, "foo/bar": false, "foo/baz": true, "qux": false},
|
||||
},
|
||||
{
|
||||
name: "options can skip phases - hierarchy is considered",
|
||||
options: RunnerOptions{SkipPhases: []string{"foo"}},
|
||||
expected: map[string]bool{"foo": false, "foo/bar": false, "foo/baz": false, "qux": true},
|
||||
},
|
||||
{
|
||||
name: "skip options have higher precedence than filter options",
|
||||
options: RunnerOptions{
|
||||
FilterPhases: []string{"foo"}, // "foo", "foo/bar", "foo/baz" true
|
||||
SkipPhases: []string{"foo/bar"}, // "foo/bar" false
|
||||
},
|
||||
expected: map[string]bool{"foo": true, "foo/bar": false, "foo/baz": true, "qux": false},
|
||||
},
|
||||
{
|
||||
name: "invalid filter option",
|
||||
options: RunnerOptions{FilterPhases: []string{"invalid"}},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid skip option",
|
||||
options: RunnerOptions{SkipPhases: []string{"invalid"}},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, u := range usecases {
|
||||
t.Run(u.name, func(t *testing.T) {
|
||||
var w = Runner{
|
||||
Phases: []Phase{
|
||||
phaseBuilder("foo",
|
||||
phaseBuilder("bar"),
|
||||
phaseBuilder("baz"),
|
||||
),
|
||||
phaseBuilder("qux"),
|
||||
},
|
||||
}
|
||||
|
||||
w.prepareForExecution()
|
||||
w.Options = u.options
|
||||
actual, err := w.computePhaseRunFlags()
|
||||
if (err != nil) != u.expectedError {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(actual, u.expected) {
|
||||
t.Errorf("\nactual:\n\t%v\nexpected:\n\t%v\n", actual, u.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func phaseBuilder1(name string, runIf func(data RunData) (bool, error), phases ...Phase) Phase {
|
||||
return Phase{
|
||||
Name: name,
|
||||
Short: fmt.Sprintf("long description for %s ...", name),
|
||||
Phases: phases,
|
||||
Run: runBuilder(name),
|
||||
RunIf: runIf,
|
||||
}
|
||||
}
|
||||
|
||||
var callstack []string
|
||||
|
||||
func runBuilder(name string) func(data RunData) error {
|
||||
return func(data RunData) error {
|
||||
callstack = append(callstack, name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func runConditionTrue(data RunData) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func runConditionFalse(data RunData) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func TestRunOrderAndConditions(t *testing.T) {
|
||||
var w = Runner{
|
||||
Phases: []Phase{
|
||||
phaseBuilder1("foo", nil,
|
||||
phaseBuilder1("bar", runConditionTrue),
|
||||
phaseBuilder1("baz", runConditionFalse),
|
||||
),
|
||||
phaseBuilder1("qux", runConditionTrue),
|
||||
},
|
||||
}
|
||||
|
||||
var usecases = []struct {
|
||||
name string
|
||||
options RunnerOptions
|
||||
expectedOrder []string
|
||||
}{
|
||||
{
|
||||
name: "Run respect runCondition",
|
||||
expectedOrder: []string{"foo", "bar", "qux"},
|
||||
},
|
||||
{
|
||||
name: "Run takes options into account",
|
||||
options: RunnerOptions{FilterPhases: []string{"foo"}, SkipPhases: []string{"foo/baz"}},
|
||||
expectedOrder: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
for _, u := range usecases {
|
||||
t.Run(u.name, func(t *testing.T) {
|
||||
callstack = []string{}
|
||||
w.Options = u.options
|
||||
err := w.Run()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(callstack, u.expectedOrder) {
|
||||
t.Errorf("\ncallstack:\n\t%v\nexpected:\n\t%v\n", callstack, u.expectedOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func phaseBuilder2(name string, runIf func(data RunData) (bool, error), run func(data RunData) error, phases ...Phase) Phase {
|
||||
return Phase{
|
||||
Name: name,
|
||||
Short: fmt.Sprintf("long description for %s ...", name),
|
||||
Phases: phases,
|
||||
Run: run,
|
||||
RunIf: runIf,
|
||||
}
|
||||
}
|
||||
|
||||
func runPass(data RunData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runFails(data RunData) error {
|
||||
return errors.New("run fails")
|
||||
}
|
||||
|
||||
func runConditionPass(data RunData) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func runConditionFails(data RunData) (bool, error) {
|
||||
return false, errors.New("run condition fails")
|
||||
}
|
||||
|
||||
func TestRunHandleErrors(t *testing.T) {
|
||||
var w = Runner{
|
||||
Phases: []Phase{
|
||||
phaseBuilder2("foo", runConditionPass, runPass),
|
||||
phaseBuilder2("bar", runConditionPass, runFails),
|
||||
phaseBuilder2("baz", runConditionFails, runPass),
|
||||
},
|
||||
}
|
||||
|
||||
var usecases = []struct {
|
||||
name string
|
||||
options RunnerOptions
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "no errors",
|
||||
options: RunnerOptions{FilterPhases: []string{"foo"}},
|
||||
},
|
||||
{
|
||||
name: "run fails",
|
||||
options: RunnerOptions{FilterPhases: []string{"bar"}},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "run condition fails",
|
||||
options: RunnerOptions{FilterPhases: []string{"baz"}},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, u := range usecases {
|
||||
t.Run(u.name, func(t *testing.T) {
|
||||
w.Options = u.options
|
||||
err := w.Run()
|
||||
if (err != nil) != u.expectedError {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func phaseBuilder3(name string, hidden bool, phases ...Phase) Phase {
|
||||
return Phase{
|
||||
Name: name,
|
||||
Short: fmt.Sprintf("long description for %s ...", name),
|
||||
Phases: phases,
|
||||
Hidden: hidden,
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
var w = Runner{
|
||||
Phases: []Phase{
|
||||
phaseBuilder3("foo", false,
|
||||
phaseBuilder3("bar", false),
|
||||
phaseBuilder3("baz", true),
|
||||
),
|
||||
phaseBuilder3("qux", false),
|
||||
},
|
||||
}
|
||||
|
||||
expected := "The \"myCommand\" command executes the following internal workflow:\n" +
|
||||
"```\n" +
|
||||
"foo long description for foo ...\n" +
|
||||
" /bar long description for bar ...\n" +
|
||||
"qux long description for qux ...\n" +
|
||||
"```"
|
||||
|
||||
actual := w.Help("myCommand")
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("\nactual:\n\t%v\nexpected:\n\t%v\n", actual, expected)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue