Skip to content

Commit

Permalink
Merge pull request #147 from fabriziopandini/multi-node-config
Browse files Browse the repository at this point in the history
Multi node config
  • Loading branch information
k8s-ci-robot authored Jan 8, 2019
2 parents 110e2e2 + c8ff127 commit b387501
Show file tree
Hide file tree
Showing 30 changed files with 1,537 additions and 248 deletions.
9 changes: 9 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ required = [
name = "k8s.io/code-generator"
branch = "master"


[[constraint]]
branch = "master"
name = "k8s.io/utils"
10 changes: 9 additions & 1 deletion cmd/kind/create/cluster/createcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,18 @@ func runE(flags *flagpole, cmd *cobra.Command, args []string) error {
return fmt.Errorf("aborting due to invalid configuration")
}

// TODO(fabrizio pandini): this check is temporary / WIP
// kind v1alpha2 config fully supports multi nodes, but the cluster creation logic implemented in
// pkg/cluster/contex.go does not (yet).
// As soon a multi node support is implemented in pkg/cluster/contex.go, this should go away
if len(cfg.AllReplicas()) > 1 {
return fmt.Errorf("multi node support is still a work in progress, currently only single node cluster are supported")
}

// create a cluster context and create the cluster
ctx := cluster.NewContext(flags.Name)
if flags.ImageName != "" {
cfg.Image = flags.ImageName
cfg.BootStrapControlPlane().Image = flags.ImageName
err := cfg.Validate()
if err != nil {
log.Errorf("Invalid flags, configuration failed validation: %v", err)
Expand Down
70 changes: 28 additions & 42 deletions pkg/cluster/config/encoding/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,56 +48,42 @@ func AddToScheme(scheme *runtime.Scheme) {
utilruntime.Must(scheme.SetVersionPriority(v1alpha2.SchemeGroupVersion))
}

// newDefaultedConfig creates a new, defaulted `kind` Config
// Defaulting uses Scheme registered defaulting functions
func newDefaultedConfig() *config.Config {
var cfg = &v1alpha2.Config{}

// apply defaults
Scheme.Default(cfg)

// converts to internal cfg
var internalCfg = &config.Config{}
Scheme.Convert(cfg, internalCfg, nil)

return internalCfg
}

// unmarshalConfig attempt to decode data into a `kind` Config; data can be
// one of the different API versions defined in the Scheme.
func unmarshalConfig(data []byte) (*config.Config, error) {
var cfg = &v1alpha2.Config{}

// decode data into a config object
_, _, err := Codecs.UniversalDecoder().Decode(data, nil, cfg)
if err != nil {
return nil, errors.Wrap(err, "decoding failure")
}

// apply defaults
Scheme.Default(cfg)

// converts to internal cfg
var internalCfg = &config.Config{}
Scheme.Convert(cfg, internalCfg, nil)

return internalCfg, nil
}

// Load reads the file at path and attempts to convert into a `kind` Config; the file
// can be one of the different API versions defined in scheme.
// If path == "" then the default config is returned
func Load(path string) (*config.Config, error) {
if path == "" {
return newDefaultedConfig(), nil
var latestPublicConfig = &v1alpha2.Config{}

if path != "" {
// read in file
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}

// decode data into a internal api Config object because
// to leverage on conversion functions for all the api versions
var cfg = &config.Config{}
err = runtime.DecodeInto(Codecs.UniversalDecoder(), contents, cfg)
if err != nil {
return nil, errors.Wrap(err, "decoding failure")
}

// converts back to the latest API version to apply defaults
Scheme.Convert(cfg, latestPublicConfig, nil)
}

// read in file
contents, err := ioutil.ReadFile(path)
if err != nil {
// apply defaults
Scheme.Default(latestPublicConfig)

// converts to internal config
var cfg = &config.Config{}
Scheme.Convert(latestPublicConfig, cfg, nil)

if err := cfg.DeriveInfo(); err != nil {
return nil, err
}

// unmarshal the file content into a `kind` Config
return unmarshalConfig(contents)
return cfg, nil
}
118 changes: 75 additions & 43 deletions pkg/cluster/config/encoding/scheme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,108 @@ limitations under the License.
package encoding

import (
"reflect"
"testing"
)

// TODO(fabrizio pandini): once we have multiple config API versions we
// will need more tests

func TestLoadCurrent(t *testing.T) {
cases := []struct {
Name string
Path string
ExpectError bool
TestName string
Path string
ExpectReplicas []string
ExpectError bool
}{
{
Name: "v1alpha1 valid minimal",
Path: "./testdata/v1alpha1/valid-minimal.yaml",
ExpectError: false,
TestName: "no config",
Path: "",
ExpectReplicas: []string{"control-plane"}, // no config (empty config path) should return a single node cluster
ExpectError: false,
},
{
TestName: "v1alpha1 minimal",
Path: "./testdata/v1alpha1/valid-minimal.yaml",
ExpectReplicas: []string{"control-plane"},
ExpectError: false,
},
{
TestName: "v1alpha1 with lifecyclehooks",
Path: "./testdata/v1alpha1/valid-with-lifecyclehooks.yaml",
ExpectReplicas: []string{"control-plane"},
ExpectError: false,
},
{
TestName: "v1alpha2 minimal",
Path: "./testdata/v1alpha2/valid-minimal.yaml",
ExpectReplicas: []string{"control-plane"},
ExpectError: false,
},
{
Name: "v1alpha1 valid with lifecyclehooks",
Path: "./testdata/v1alpha1/valid-with-lifecyclehooks.yaml",
ExpectError: false,
TestName: "v1alpha2 lifecyclehooks",
Path: "./testdata/v1alpha2/valid-with-lifecyclehooks.yaml",
ExpectReplicas: []string{"control-plane"},
ExpectError: false,
},
{
Name: "v1alpha2 valid minimal",
Path: "./testdata/v1alpha2/valid-minimal.yaml",
ExpectError: false,
TestName: "v1alpha2 config with 2 nodes",
Path: "./testdata/v1alpha2/valid-minimal-two-nodes.yaml",
ExpectReplicas: []string{"control-plane", "worker"},
ExpectError: false,
},
{
Name: "v1alpha2 valid with lifecyclehooks",
Path: "./testdata/v1alpha2/valid-with-lifecyclehooks.yaml",
ExpectError: false,
TestName: "v1alpha2 full HA",
Path: "./testdata/v1alpha2/valid-full-ha.yaml",
ExpectReplicas: []string{"etcd", "lb", "control-plane1", "control-plane2", "control-plane3", "worker1", "worker2"},
ExpectError: false,
},
{
Name: "invalid path",
TestName: "invalid path",
Path: "./testdata/not-a-file.bogus",
ExpectError: true,
},
{
Name: "invalid apiVersion",
TestName: "Invalid apiversion",
Path: "./testdata/invalid-apiversion.yaml",
ExpectError: true,
},
{
Name: "invalid yaml",
TestName: "Invalid kind",
Path: "./testdata/invalid-kind.yaml",
ExpectError: true,
},
{
TestName: "Invalid yaml",
Path: "./testdata/invalid-yaml.yaml",
ExpectError: true,
},
}
for _, tc := range cases {
_, err := Load(tc.Path)
if err != nil && !tc.ExpectError {
t.Errorf("case: '%s' got error loading and expected none: %v", tc.Name, err)
} else if err == nil && tc.ExpectError {
t.Errorf("case: '%s' got no error loading but expected one", tc.Name)
}
}
}
for _, c := range cases {
t.Run(c.TestName, func(t2 *testing.T) {
// Loading config and deriving infos
cfg, err := Load(c.Path)

func TestLoadDefault(t *testing.T) {
cfg, err := Load("")
if err != nil {
t.Errorf("got error loading default config but expected none: %v", err)
t.FailNow()
}
defaultConfig := newDefaultedConfig()
if !reflect.DeepEqual(cfg, defaultConfig) {
t.Errorf(
"Load(\"\") should match config.New() but does not: %v != %v",
cfg, defaultConfig,
)
// the error can be:
// - nil, in which case we should expect no errors or fail
if err != nil {
if !c.ExpectError {
t2.Fatalf("unexpected error while Loading config: %v", err)
}
return
}
// - not nil, in which case we should expect errors or fail
if err == nil {
if c.ExpectError {
t2.Fatalf("unexpected lack or error while Loading config")
}
}

if len(cfg.AllReplicas()) != len(c.ExpectReplicas) {
t2.Fatalf("expected %d replicas, saw %d", len(c.ExpectReplicas), len(cfg.AllReplicas()))
}

for i, name := range c.ExpectReplicas {
if cfg.AllReplicas()[i].Name != name {
t2.Errorf("expected %q node at position %d, saw %q", name, i, cfg.AllReplicas()[i].Name)
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# this file contains an invalid config api version for testing
kind: Config
kind: Node
apiVersion: not-valid
3 changes: 3 additions & 0 deletions pkg/cluster/config/encoding/testdata/invalid-kind.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# this file contains an invalid config kind for testing
kind: not-valid
apiVersion: kind.sigs.k8s.io/v1alpha2
10 changes: 10 additions & 0 deletions pkg/cluster/config/encoding/testdata/v1alpha2/valid-full-ha.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# technically valid, config file with a full ha cluster
kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha2
nodes:
- role: control-plane
replicas: 3
- role: worker
replicas: 2
- role: external-etcd
- role: external-load-balancer
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# technically valid, minimal config file with two nodes
kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha2
nodes:
- role: control-plane
- role: worker
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha2
nodeLifecycle:
preKubeadm:
- name: "pull an image"
command: [ "docker", "pull", "ubuntu" ]
- name: "pull another image"
command: [ "docker", "pull", "debian" ]
nodes:
- nodeLifecycle:
preKubeadm:
- name: "pull an image"
command: [ "docker", "pull", "ubuntu" ]
- name: "pull another image"
command: [ "docker", "pull", "debian" ]
16 changes: 14 additions & 2 deletions pkg/cluster/config/fuzzer/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,24 @@ import (
func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
fuzzConfig,
//fuzzNode,
}
}

func fuzzConfig(obj *config.Config, c fuzz.Continue) {
c.FuzzNoCustom(obj)

// Pinning values for fields that get defaults if fuzz value is empty string or nil (thus making the round trip test fail)
obj.Image = "fuzzimage:latest"
// Pinning values for fields that get defaults if fuzz value is empty string or nil
obj.Nodes = []config.Node{{
Image: "foo:bar",
Role: config.ControlPlaneRole,
}}
}

func fuzzNode(obj *config.Node, c fuzz.Continue) {
c.FuzzNoCustom(obj)

// Pinning values for fields that get defaults if fuzz value is empty string or nil
obj.Image = "foo:bar"
obj.Role = config.ControlPlaneRole
}
Loading

0 comments on commit b387501

Please sign in to comment.