Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi node config #147

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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