mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 14:07:14 +00:00
add phase runner
This commit is contained in:
parent
8012b9583e
commit
6a8ace5c65
@ -97,6 +97,7 @@ filegroup(
|
|||||||
srcs = [
|
srcs = [
|
||||||
":package-srcs",
|
":package-srcs",
|
||||||
"//cmd/kubeadm/app/cmd/phases/certs:all-srcs",
|
"//cmd/kubeadm/app/cmd/phases/certs:all-srcs",
|
||||||
|
"//cmd/kubeadm/app/cmd/phases/workflow:all-srcs",
|
||||||
],
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
|
39
cmd/kubeadm/app/cmd/phases/workflow/BUILD
Normal file
39
cmd/kubeadm/app/cmd/phases/workflow/BUILD
Normal file
@ -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"],
|
||||||
|
)
|
47
cmd/kubeadm/app/cmd/phases/workflow/doc.go
Normal file
47
cmd/kubeadm/app/cmd/phases/workflow/doc.go
Normal file
@ -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
|
109
cmd/kubeadm/app/cmd/phases/workflow/doc_test.go
Normal file
109
cmd/kubeadm/app/cmd/phases/workflow/doc_test.go
Normal file
@ -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()
|
||||||
|
}
|
57
cmd/kubeadm/app/cmd/phases/workflow/phase.go
Normal file
57
cmd/kubeadm/app/cmd/phases/workflow/phase.go
Normal file
@ -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)
|
||||||
|
}
|
382
cmd/kubeadm/app/cmd/phases/workflow/runner.go
Normal file
382
cmd/kubeadm/app/cmd/phases/workflow/runner.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
279
cmd/kubeadm/app/cmd/phases/workflow/runner_test.go
Normal file
279
cmd/kubeadm/app/cmd/phases/workflow/runner_test.go
Normal file
@ -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
Block a user