Commit 6a8ace5c authored by fabriziopandini's avatar fabriziopandini

add phase runner

parent 8012b958
......@@ -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"],
......
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"],
)
/*
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
/*
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()
}
/*
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)
}
/*
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)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment