Skip to content
This repository has been archived by the owner on Sep 24, 2021. It is now read-only.

Commit

Permalink
cloudinit adapter clean up
Browse files Browse the repository at this point in the history
Signed-off-by: Chuck Ha <chuckh@vmware.com>
  • Loading branch information
chuckha committed Aug 21, 2019
1 parent 7401fe6 commit dc02a65
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 88 deletions.
87 changes: 42 additions & 45 deletions cloudinit/kindadapter.go
Expand Up @@ -27,37 +27,40 @@ import (
"sigs.k8s.io/kind/pkg/exec"
)

var supportedCloudCongfigActions = map[string]cloudCongfigActionBuilder{
"write_files": newWriteFilesAction,
"runcmd": newRunCmdAction,
}
const (
// Supported cloud config modules
writefiles = "write_files"
runcmd = "runcmd"
)

type cloudCongfigActionBuilder func() cloudCongfigAction
type actionFactory struct{}

func (a *actionFactory) action(name string) action {
switch name {
case writefiles:
return newWriteFilesAction()
case runcmd:
return newRunCmdAction()
default:
// TODO Add a logger during the refactor and log this unknown module
return newUnknown(name)
}
}

type cloudCongfigAction interface {
type action interface {
Unmarshal(userData []byte) error
Run(cmder exec.Cmder) ([]string, error)
}

func cloudCongfigActionFactory(actionName string) (cloudCongfigAction, error) {
if actionBuilder, ok := supportedCloudCongfigActions[actionName]; ok {
action := actionBuilder()
return action, nil
}

return nil, errors.Errorf("cloud config module %q is not supported", actionName)
}

// Run the given userData (a cloud config script) on the given node
func Run(userData []byte, cmder exec.Cmder) ([]string, error) {
// validate userData is a valid yaml, as required by the cloud config specification
var m map[string]interface{}
if err := yaml.Unmarshal(userData, &m); err != nil {
return nil, errors.Wrapf(errors.WithStack(err), "userData should be a valid yaml, as required by the cloud config specification")
func Run(cloudConfig []byte, cmder exec.Cmder) ([]string, error) {
// validate cloudConfigScript is a valid yaml, as required by the cloud config specification
if err := yaml.Unmarshal(cloudConfig, &map[string]interface{}{}); err != nil {
return nil, errors.Wrapf(err, "cloud-config is not valid yaml")
}

// parse the cloud config yaml into a slice of cloud config actions.
actions, err := getCloudCongfigActionSlices(userData)
actions, err := getActions(cloudConfig)
if err != nil {
return nil, err
}
Expand All @@ -75,54 +78,48 @@ func Run(userData []byte, cmder exec.Cmder) ([]string, error) {
return lines, nil
}

// getCloudCongfigActionSlices parse the cloud config yaml into a slice of cloud config actions.
// NB. it is necessary to parse manually because a cloud config is a map of unstructured elements;
// using a standard yaml parser and make it UnMarshal to map[string]interface{} can't work because
// [1] map does not guarantee the order of items, while the adapter should preserve the order of actions
// [2] map does not allow to repeat the same action module in two point of the cloud init sequence, while
// this might happen in real cloud inits
func getCloudCongfigActionSlices(userData []byte) ([]cloudCongfigAction, error) {
var cloudConfigActionRegEx = regexp.MustCompile(`^[a-zA-Z_]*:`)
var err error
var lines []string
var action cloudCongfigAction
var actionSlice []cloudCongfigAction
// getActions parses the cloud config yaml into a slice of actions to run.
// Parsing manually is required because the order of the cloud config's actions must be maintained.
func getActions(userData []byte) ([]action, error) {
actionRegEx := regexp.MustCompile(`^[a-zA-Z_]*:`)
lines := make([]string, 0)
actions := make([]action, 0)
actionFactory := &actionFactory{}

var act action

// scans the file searching for keys/top level actions.
scanner := bufio.NewScanner(bytes.NewReader(userData))
for scanner.Scan() {
line := scanner.Text()
// if the line is key/top level action
if cloudConfigActionRegEx.MatchString(line) {
if actionRegEx.MatchString(line) {
// converts the file fragment scanned up to now into the current action, if any
if action != nil {
if act != nil {
actionBlock := strings.Join(lines, "\n")
if err := action.Unmarshal([]byte(actionBlock)); err != nil {
if err := act.Unmarshal([]byte(actionBlock)); err != nil {
return nil, errors.WithStack(err)
}
actionSlice = append(actionSlice, action)
actions = append(actions, act)
lines = lines[:0]
}

// creates the new action
actionName := strings.TrimSuffix(line, ":")
action, err = cloudCongfigActionFactory(actionName)
if err != nil {
return nil, err
}
act = actionFactory.action(actionName)
}

lines = append(lines, line)
}

// converts the last file fragment scanned into the current action, if any
if action != nil {
if act != nil {
actionBlock := strings.Join(lines, "\n")
if err := action.Unmarshal([]byte(actionBlock)); err != nil {
if err := act.Unmarshal([]byte(actionBlock)); err != nil {
return nil, errors.WithStack(err)
}
actionSlice = append(actionSlice, action)
actions = append(actions, act)
}

return actionSlice, scanner.Err()
return actions, scanner.Err()
}
28 changes: 14 additions & 14 deletions cloudinit/rumcmd.go
Expand Up @@ -26,6 +26,11 @@ import (
"sigs.k8s.io/kind/pkg/exec"
)

const (
prompt = "capd@docker$"
errorPrefix = "ERROR!"
)

// Cmd defines a runcmd command
type Cmd struct {
Cmd string
Expand Down Expand Up @@ -62,30 +67,25 @@ func (c *Cmd) UnmarshalJSON(data []byte) error {
return nil
}

// runCmdAction defines a cloud init action that replicates the behavior of the cloud init rundcmd module
type runCmdAction struct {
// runCmd defines a cloud init action that replicates the behavior of the cloud init rundcmd module
type runCmd struct {
Cmds []Cmd `json:"runcmd,"`
}

var _ cloudCongfigAction = &runCmdAction{}

func newRunCmdAction() cloudCongfigAction {
return &runCmdAction{}
func newRunCmdAction() action {
return &runCmd{}
}

// Unmarshal the runCmdAction
func (a *runCmdAction) Unmarshal(userData []byte) error {
// Unmarshal the runCmd
func (a *runCmd) Unmarshal(userData []byte) error {
if err := yaml.Unmarshal(userData, a); err != nil {
return errors.Wrapf(errors.WithStack(err), "error parsing write_files action: %s", userData)
return errors.Wrapf(err, "error parsing run_cmd action: %s", userData)
}
return nil
}

const prompt = "capd@docker$"
const errorPrefix = "ERROR!"

// Run the runCmdAction
func (a *runCmdAction) Run(cmder exec.Cmder) ([]string, error) {
// Run the runCmd
func (a *runCmd) Run(cmder exec.Cmder) ([]string, error) {
var lines []string
for _, c := range a.Cmds {
// kubeadm in docker requires to ignore some errors, and this requires to modify the cmd generate by CABPK by default...
Expand Down
14 changes: 7 additions & 7 deletions cloudinit/runcmd_test.go
Expand Up @@ -32,7 +32,7 @@ func TestRunCmdUnmarshal(t *testing.T) {
runcmd:
- [ ls, -l, / ]
- "ls -l /"`
r := runCmdAction{}
r := runCmd{}
err := r.Unmarshal([]byte(cloudData))
if err != nil {
t.Fatal(err)
Expand All @@ -56,13 +56,13 @@ runcmd:
func TestRunCmdRun(t *testing.T) {
var useCases = []struct {
name string
r runCmdAction
r runCmd
expectedlines []string
expectedError bool
}{
{
name: "two command pass",
r: runCmdAction{
r: runCmd{
Cmds: []Cmd{
{Cmd: "foo", Args: []string{"bar"}},
{Cmd: "baz", Args: []string{"bbb"}},
Expand All @@ -77,7 +77,7 @@ func TestRunCmdRun(t *testing.T) {
},
{
name: "first command fails",
r: runCmdAction{
r: runCmd{
Cmds: []Cmd{
{Cmd: "fail", Args: []string{"bar"}}, // fail force fakeCmd to fail
{Cmd: "baz", Args: []string{"bbb"}},
Expand All @@ -92,7 +92,7 @@ func TestRunCmdRun(t *testing.T) {
},
{
name: "second command fails",
r: runCmdAction{
r: runCmd{
Cmds: []Cmd{
{Cmd: "foo", Args: []string{"bar"}},
{Cmd: "fail", Args: []string{"qux"}}, // fail force fakeCmd to fail
Expand All @@ -108,7 +108,7 @@ func TestRunCmdRun(t *testing.T) {
},
{
name: "hack kubeadm ingore errors",
r: runCmdAction{
r: runCmd{
Cmds: []Cmd{
{Cmd: "/bin/sh", Args: []string{"-c", "kubeadm init --config /tmp/kubeadm.yaml"}},
},
Expand Down Expand Up @@ -200,7 +200,7 @@ func TestHackKubeadmIgnoreErrors(t *testing.T) {
runcmd:
- kubeadm init --config=/tmp/kubeadm.yaml
- [ kubeadm, join, --config=/tmp/kubeadm-controlplane-join-config.yaml ]`
r := runCmdAction{}
r := runCmd{}
err := r.Unmarshal([]byte(cloudData))
if err != nil {
t.Fatal(err)
Expand Down
61 changes: 61 additions & 0 deletions cloudinit/unknown.go
@@ -0,0 +1,61 @@
/*
Copyright 2019 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 cloudinit

import (
"encoding/json"

"github.com/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
)

type unknown struct {
module string
lines []string
}

func newUnknown(module string) action {
return &unknown{module: module}
}

// Unmarshal will unmarshall unknown actions and slurp the value
func (u *unknown) Unmarshal(data []byte) error {
// try unmarshalling to a slice of strings
var s1 []string
if err := json.Unmarshal(data, &s1); err != nil {
if _, ok := err.(*json.UnmarshalTypeError); !ok {
return errors.WithStack(err)
}
} else {
u.lines = s1
return nil
}

// If it's not a slice of strings it should be one string value
var s2 string
if err := json.Unmarshal(data, &s2); err != nil {
return errors.WithStack(err)
}

u.lines = []string{s2}
return nil
}

// Run will do nothing since the cloud config module is unknown.
func (u *unknown) Run(_ exec.Cmder) ([]string, error) {
return u.lines, nil
}
50 changes: 50 additions & 0 deletions cloudinit/unknown_test.go
@@ -0,0 +1,50 @@
/*
Copyright 2019 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 cloudinit

import (
"reflect"
"testing"
)

func TestUnknown_Run(t *testing.T) {
u := &unknown{
lines: []string{},
}
lines, err := u.Run(nil)
if err != nil {
t.Fatal("err should always be nil")
}
if len(lines) != 0 {
t.Fatalf("return exactly what was parsed. did not expect anything here but got: %v", lines)
}
}

func TestUnknown_Unmarshal(t *testing.T) {
u := &unknown{}
expected := []string{"test 1", "test 2", "test 3"}
input := `["test 1", "test 2", "test 3"]`
if err := u.Unmarshal([]byte(input)); err != nil {
t.Fatalf("should not return an error but got: %v", err)
}
if len(u.lines) != len(expected) {
t.Fatalf("expected exactly %v lines but got %v", 3, u.lines)
}
if !reflect.DeepEqual(u.lines, expected) {
t.Fatalf("lines should be %v but it is %v", expected, u.lines)
}
}

0 comments on commit dc02a65

Please sign in to comment.