refactor config, add machinery similar to a kubernetes API

This commit is contained in:
Benjamin Elder
2018-09-02 00:47:40 -07:00
parent 031ce4c0cb
commit 703522b863
22 changed files with 975 additions and 139 deletions

View File

@@ -22,6 +22,8 @@ import (
"github.com/spf13/cobra"
"k8s.io/test-infra/kind/pkg/cluster"
"k8s.io/test-infra/kind/pkg/cluster/config"
"k8s.io/test-infra/kind/pkg/cluster/config/encoding"
)
type flags struct {
@@ -49,15 +51,15 @@ func NewCommand() *cobra.Command {
func run(flags *flags, cmd *cobra.Command, args []string) {
// TODO(bentheelder): make this more configurable
// load the config
config, err := cluster.LoadCreateConfig(flags.Config)
cfg, err := encoding.Load(flags.Config)
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
// validate the config
err = config.Validate()
err = cfg.Validate()
if err != nil {
log.Error("Invalid configuration!")
configErrors := err.(cluster.ConfigErrors)
configErrors := err.(*config.Errors)
for _, problem := range configErrors.Errors() {
log.Error(problem)
}
@@ -68,7 +70,7 @@ func run(flags *flags, cmd *cobra.Command, args []string) {
if err != nil {
log.Fatalf("Failed to create cluster context! %v", err)
}
err = ctx.Create(config)
err = ctx.Create(cfg.ToCurrent())
if err != nil {
log.Fatalf("Failed to create cluster: %v", err)
}

View File

@@ -24,6 +24,7 @@ A non-exhaustive list of tasks (in no-particular order) includes:
- [ ] support multiple overlay networks
- [x] support advanced configuration via config file
- [x] kubeadm config template override
- [ ] node lifecycle hooks
- [ ] more advanced network configuration (not docker0)
- [ ] support for other CRI within the "node" containers (containerd, cri-o)
- [ ] switch from `exec.Command("docker", ...)` to the Docker client library

View File

@@ -24,10 +24,10 @@ import (
"regexp"
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/pkg/errors"
"k8s.io/test-infra/kind/pkg/cluster/config"
"k8s.io/test-infra/kind/pkg/cluster/kubeadm"
"k8s.io/test-infra/kind/pkg/exec"
)
@@ -37,7 +37,7 @@ const ClusterLabelKey = "io.k8s.test-infra.kind-cluster"
// Context is used to create / manipulate kubernetes-in-docker clusters
type Context struct {
Name string
name string
}
// similar to valid docker container names, but since we will prefix
@@ -60,20 +60,25 @@ func NewContext(name string) (ctx *Context, err error) {
)
}
return &Context{
Name: name,
name: name,
}, nil
}
// ClusterLabel returns the docker object label that will be applied
// to cluster "node" containers
func (c *Context) ClusterLabel() string {
return fmt.Sprintf("%s=%s", ClusterLabelKey, c.Name)
return fmt.Sprintf("%s=%s", ClusterLabelKey, c.name)
}
// Name returns the context's name
func (c *Context) Name() string {
return c.name
}
// ClusterName returns the Kubernetes cluster name based on the context name
// currently this is .Name prefixed with "kind-"
func (c *Context) ClusterName() string {
return fmt.Sprintf("kind-%s", c.Name)
return fmt.Sprintf("kind-%s", c.name)
}
// KubeConfigPath returns the path to where the Kubeconfig would be placed
@@ -84,12 +89,12 @@ func (c *Context) KubeConfigPath() string {
configDir := filepath.Join(os.Getenv("HOME"), ".kube")
// note that the file name however does not, we do not want to overwite
// the standard config, though in the future we may (?) merge them
fileName := fmt.Sprintf("kind-config-%s", c.Name)
fileName := fmt.Sprintf("kind-config-%s", c.name)
return filepath.Join(configDir, fileName)
}
// Create provisions and starts a kubernetes-in-docker cluster
func (c *Context) Create(config *CreateConfig) error {
func (c *Context) Create(config *config.Config) error {
// validate config first
if err := config.Validate(); err != nil {
return err
@@ -97,7 +102,7 @@ func (c *Context) Create(config *CreateConfig) error {
// TODO(bentheelder): multiple nodes ...
kubeadmConfig, err := c.provisionControlPlane(
fmt.Sprintf("kind-%s-control-plane", c.Name),
fmt.Sprintf("kind-%s-control-plane", c.name),
config,
)
@@ -130,7 +135,10 @@ func (c *Context) Delete() error {
// provisionControlPlane provisions the control plane node
// and the cluster kubeadm config
func (c *Context) provisionControlPlane(nodeName string, config *CreateConfig) (kubeadmConfigPath string, err error) {
func (c *Context) provisionControlPlane(
nodeName string,
config *config.Config,
) (kubeadmConfigPath string, err error) {
// create the "node" container (docker run, but it is paused, see createNode)
node, err := createNode(nodeName, c.ClusterLabel())
if err != nil {

View File

@@ -1,110 +0,0 @@
/*
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 cluster
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"github.com/ghodss/yaml"
)
// CreateConfig contains cluster creation config
type CreateConfig struct {
// NumNodes is the number of nodes to create (currently only one is supported)
NumNodes int `json:"numNodes"`
// KubeadmConfigTemplate allows overriding the default template in
// cluster/kubeadm
KubeadmConfigTemplate string `json:"kubeadmConfigTemplate"`
}
// NewCreateConfig returns a new default CreateConfig
func NewCreateConfig() *CreateConfig {
return &CreateConfig{
NumNodes: 1,
}
}
// LoadCreateConfig reads the file at path and attempts to load it as
// a yaml encoding of CreateConfig, falling back to json if this fails.
// It returns an error if reading the files fails, or if both yaml and json fail
// If path is "" then a default config is returned instead
func LoadCreateConfig(path string) (config *CreateConfig, err error) {
if path == "" {
return NewCreateConfig(), nil
}
// read in file
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// first try yaml
config = &CreateConfig{}
yamlErr := yaml.Unmarshal(contents, config)
if yamlErr == nil {
return config, nil
}
// then try json
config = &CreateConfig{}
jsonErr := json.Unmarshal(contents, config)
if jsonErr == nil {
return config, nil
}
return nil, fmt.Errorf("could not read as yaml: %v or json: %v", yamlErr, jsonErr)
}
// Validate returns a ConfigErrors with an entry for each problem
// with the config, or nil if there are none
func (c *CreateConfig) Validate() error {
errs := []error{}
// TODO(bentheelder): support multiple nodes
if c.NumNodes != 1 {
errs = append(errs, fmt.Errorf(
"%d nodes requested but only clusters with one node are supported currently",
c.NumNodes,
))
}
if len(errs) > 0 {
return ConfigErrors{errs}
}
return nil
}
// ConfigErrors implements error and contains all config errors
// This is returned by Config.Validate
type ConfigErrors struct {
errors []error
}
// assert ConfigErrors implements error
var _ error = &ConfigErrors{}
func (c ConfigErrors) Error() string {
var buff bytes.Buffer
for _, err := range c.errors {
buff.WriteString(err.Error())
buff.WriteRune('\n')
}
return buff.String()
}
// Errors returns the slice of errors contained by ConfigErrors
func (c ConfigErrors) Errors() []error {
return c.errors
}

35
pkg/cluster/config/any.go Normal file
View File

@@ -0,0 +1,35 @@
/*
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 config
/*
This file contains interfaces and code related to all API versions
*/
// Any represents any API version of Config
type Any interface {
// Validate should return an error of type `*Errors` if config is invalid
Validate() error
// ToCurrent should convert a config version to the version in this package
ToCurrent() *Config
// ApplyDefaults should set unset fields to defaults
ApplyDefaults()
// Kind should return "Config"
Kind() string
// APIVersion should return the apiVersion for this config
APIVersion() string
}

View File

@@ -0,0 +1,23 @@
/*
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 config
// ToCurrent converts Config to config.Config
// It is implemented to meet config.Any, and just deep copies on this type
func (c *Config) ToCurrent() *Config {
return c.DeepCopy()
}

View File

@@ -0,0 +1,79 @@
/*
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 config
// TODO(bentheelder): consider kubernetes deep-copy gen
// In the meantime the pattern is:
// - handle nil receiver
// - create a new(OutType)
// - *out = *in to copy plain fields
// - copy pointer fields by calling their DeepCopy
// - copy slices / maps by allocating a new one and performing a copy loop
// DeepCopy returns a deep copy
func (in *Config) DeepCopy() *Config {
if in == nil {
return nil
}
out := new(Config)
*out = *in
out.NodeLifecycle = in.NodeLifecycle.DeepCopy()
return out
}
// DeepCopy returns a deep copy
func (in *NodeLifecycle) DeepCopy() *NodeLifecycle {
if in == nil {
return nil
}
out := new(NodeLifecycle)
if in.PreBoot != nil {
out.PreBoot = make([]LifecycleHook, len(in.PreBoot))
for i := range in.PreBoot {
out.PreBoot[i] = *(in.PreBoot[i].DeepCopy())
}
}
if in.PreKubeadm != nil {
out.PreKubeadm = make([]LifecycleHook, len(in.PreKubeadm))
for i := range in.PreKubeadm {
out.PreKubeadm[i] = *(in.PreKubeadm[i].DeepCopy())
}
}
if in.PostKubeadm != nil {
out.PostKubeadm = make([]LifecycleHook, len(in.PostKubeadm))
for i := range in.PostKubeadm {
out.PostKubeadm[i] = *(in.PostKubeadm[i].DeepCopy())
}
}
return out
}
// DeepCopy returns a deep copy
func (in *LifecycleHook) DeepCopy() *LifecycleHook {
if in == nil {
return nil
}
out := new(LifecycleHook)
*out = *in
if in.Args != nil {
out.Args = make([]string, len(in.Args))
for i := range in.Args {
out.Args[i] = in.Args[i]
}
}
return out
}

View File

@@ -0,0 +1,73 @@
/*
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 config
import (
"reflect"
"testing"
)
func TestDeepCopy(t *testing.T) {
cases := []struct {
TestName string
Config *Config
}{
{
TestName: "Canonical config",
Config: New(),
},
{
TestName: "Config with NodeLifecyle hooks",
Config: func() *Config {
cfg := New()
cfg.NodeLifecycle = &NodeLifecycle{
PreBoot: []LifecycleHook{
{
Command: "ps",
Args: []string{"aux"},
},
},
PreKubeadm: []LifecycleHook{
{
Name: "docker ps",
Command: "docker",
Args: []string{"ps"},
},
},
PostKubeadm: []LifecycleHook{
{
Name: "docker ps again",
Command: "docker",
Args: []string{"ps", "-a"},
},
},
}
return cfg
}(),
},
}
for _, tc := range cases {
original := tc.Config
deepCopy := tc.Config.DeepCopy()
if !reflect.DeepEqual(original, deepCopy) {
t.Errorf(
"case: '%s' deep copy did not equal original: %+v != %+v",
tc.TestName, original, deepCopy,
)
}
}
}

View File

@@ -0,0 +1,31 @@
/*
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 config
// New returns a new default Config
func New() *Config {
cfg := &Config{}
cfg.ApplyDefaults()
return cfg
}
// ApplyDefaults replaces unset fields with defaults
func (c *Config) ApplyDefaults() {
if c.NumNodes == 0 {
c.NumNodes = 1
}
}

19
pkg/cluster/config/doc.go Normal file
View File

@@ -0,0 +1,19 @@
/*
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 config implements the current apiVersion of the `kind` Config
// along with some common abstractions
package config

View File

@@ -0,0 +1,139 @@
/*
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 encoding implements apiVersion aware functionality to
// Marshal / Unmarshal / Load Config
package encoding
import (
"bytes"
"fmt"
"io/ioutil"
"github.com/ghodss/yaml"
"k8s.io/test-infra/kind/pkg/cluster/config"
)
// Load reads the file at path and attempts to load it as a yaml Config
// after detecting the apiVersion in the file
// (or defaulting to the current version if none is specified)
// If path == "" then the default config for the current version is returned
func Load(path string) (config.Any, error) {
if path == "" {
return config.New(), nil
}
// read in file
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// read in some config version
// TODO(bentheelder): we do not use or respect `kind:` at all
// possibly we should require something like `kind: "Config"`
cfg, err := Unmarshal(contents)
if err != nil {
return nil, err
}
return cfg, err
}
// LoadCurrent is equivalent to Load followed by cfg.ToCurrent()
func LoadCurrent(path string) (*config.Config, error) {
cfg, err := Load(path)
if err != nil {
return nil, err
}
return cfg.ToCurrent(), nil
}
// used to sniff a config for it's api version
type configOnlyVersion struct {
APIVersion string `json:"apiVersion,omitempty"`
}
// helper to sniff and validate apiVersion
func detectVersion(raw []byte) (version string, err error) {
c := configOnlyVersion{}
if err := yaml.Unmarshal(raw, &c); err != nil {
return "", err
}
switch c.APIVersion {
// default to the current api version if unspecified, or explicitly specified
case config.APIVersion, "":
return config.APIVersion, nil
}
return "", fmt.Errorf("invalid version: %v", c.APIVersion)
}
// Unmarshal is an apiVersion aware yaml.Unmarshall for config.Any
func Unmarshal(raw []byte) (config.Any, error) {
if raw == nil {
return nil, fmt.Errorf("nil input")
}
// sniff and validate version
version, err := detectVersion(raw)
if err != nil {
return nil, err
}
// load version
var cfg config.Any
switch version {
case config.APIVersion:
cfg = &config.Config{}
}
err = yaml.Unmarshal(raw, cfg)
if err != nil {
return nil, err
}
// apply defaults before returning
cfg.ApplyDefaults()
return cfg, nil
}
// used by `Marshal` to encode the config header
type configHeader struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion"`
}
// Marshal marshals any config with kind and apiVersion header
func Marshal(cfg config.Any) ([]byte, error) {
if cfg == nil {
return nil, fmt.Errorf("nil input")
}
var buff bytes.Buffer
// write kind, apiVersion header
b, err := yaml.Marshal(configHeader{
Kind: cfg.Kind(),
APIVersion: cfg.APIVersion(),
})
if err != nil {
return nil, err
}
// NOTE: buff.Write can only fail with a panic if it cannot allocate
buff.Write(b)
// write actual config contents
b, err = yaml.Marshal(cfg)
if err != nil {
return nil, err
}
// don't write a `{}` when the config has no set fields
if !bytes.Equal(b, []byte("{}\n")) {
buff.Write(b)
}
return buff.Bytes(), nil
}

View File

@@ -0,0 +1,262 @@
/*
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 encoding
import (
"reflect"
"testing"
"k8s.io/test-infra/kind/pkg/cluster/config"
)
// TODO(bentheelder): once we have multiple config API versions we
// will need more tests for Load and LoadCurrent
func TestLoadCurrent(t *testing.T) {
cases := []struct {
Name string
Path string
ExpectError bool
}{
{
Name: "valid minimal",
Path: "./testdata/valid-minimal.yaml",
ExpectError: false,
},
{
Name: "valid with lifecyclehooks",
Path: "./testdata/valid-with-lifecyclehooks.yaml",
ExpectError: false,
},
{
Name: "invalid path",
Path: "./testdata/not-a-file.bogus",
ExpectError: true,
},
{
Name: "invalid apiVersion",
Path: "./testdata/invalid-apiversion.yaml",
ExpectError: true,
},
{
Name: "invalid yaml",
Path: "./testdata/invalid-yaml.yaml",
ExpectError: true,
},
}
for _, tc := range cases {
_, err := LoadCurrent(tc.Path)
if err != nil && !tc.ExpectError {
t.Errorf("case: '%s' got error`Load`ing and expected none: %v", tc.Name, err)
} else if err == nil && tc.ExpectError {
t.Errorf("case: '%s' got no error `Load`ing but expected one", tc.Name)
}
}
}
func TestLoadDefault(t *testing.T) {
cfg, err := Load("")
if err != nil {
t.Errorf("got error `Load`ing default config but expected none: %v", err)
t.FailNow()
}
defaultConfig := config.New()
if !reflect.DeepEqual(cfg, defaultConfig) {
t.Errorf(
"Load(\"\") should match config.New() but does not: %v != %v",
cfg, defaultConfig,
)
t.FailNow()
}
}
func TestEncodingRoundTrip(t *testing.T) {
cfg := config.New()
marshaled, err := Marshal(cfg)
if err != nil {
t.Errorf("got error `Marshal`ing default config: %v", err)
t.FailNow()
}
roundTripConfig, err := Unmarshal(marshaled)
if err != nil {
t.Errorf("got error `Unmarshal`ing default config: %v, raw = %s", err, marshaled)
t.FailNow()
}
if !reflect.DeepEqual(cfg, roundTripConfig) {
t.Errorf("default config does not match after Unmarshal(Marshal()), %v != %v", cfg, roundTripConfig)
t.FailNow()
}
}
func TestUnmarshal(t *testing.T) {
defaultConfig, err := Marshal(config.New())
if err != nil {
t.Errorf("Error setting up default config for test: %v", err)
t.FailNow()
}
cases := []struct {
Name string
Raw []byte
ExpectedConfig config.Any
ExpectError bool
}{
{
Name: "default config",
Raw: defaultConfig,
ExpectedConfig: config.New(),
ExpectError: false,
},
{
Name: "config with lifecycle",
Raw: []byte(`kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha1
nodeLifecycle:
preKubeadm:
- name: "pull an image"
command: "docker"
args: [ "pull", "ubuntu" ]
- name: "pull another image"
command: "docker"
args: [ "pull", "debian" ]
`),
ExpectedConfig: func() config.Any {
cfg := &config.Config{
NodeLifecycle: &config.NodeLifecycle{
PreKubeadm: []config.LifecycleHook{
{
Name: "pull an image",
Command: "docker",
Args: []string{"pull", "ubuntu"},
},
{
Name: "pull another image",
Command: "docker",
Args: []string{"pull", "debian"},
},
},
},
}
cfg.ApplyDefaults()
return cfg
}(),
ExpectError: false,
},
{
Name: "Invalid apiVersion 🤔",
Raw: []byte("kind: KindConfig\napiVersion: 🤔"),
ExpectError: true,
},
{
Name: "generically invalid yaml",
Raw: []byte("\""),
ExpectError: true,
},
{
Name: "invalid config yaml",
Raw: []byte("numNodes: too-many"),
ExpectError: true,
},
{
Name: "nil input",
Raw: nil,
ExpectError: true,
},
}
for _, tc := range cases {
cfg, err := Unmarshal(tc.Raw)
if err != nil && !tc.ExpectError {
t.Errorf(
"case: '%s' got error `Unmarshal`ing and expected none: %v",
tc.Name, err,
)
} else if err == nil && tc.ExpectError {
t.Errorf(
"case: '%s' got no error `Unmarshal`ing but expected one",
tc.Name,
)
}
if !reflect.DeepEqual(cfg, tc.ExpectedConfig) {
t.Errorf(
"case: '%s' `Unmarshal` result does not match expected: %v != %v",
tc.Name, cfg, tc.ExpectedConfig,
)
}
}
}
func TestUnmarshalDefaulting(t *testing.T) {
// marshal an unset config
emptyConfig, err := Marshal(&config.Config{})
if err != nil {
t.Errorf("Error setting up default config for test: %v", err)
t.FailNow()
}
// create a config with defaulted values
defaulted := &config.Config{}
defaulted.ApplyDefaults()
// unmarshal the unset config
unmarshaledEmpty, err := Unmarshal(emptyConfig)
if err != nil {
t.Errorf("Error `Unmarshal`ing default config: %v", err)
t.FailNow()
}
// verify that the unset config should match the default config
if !reflect.DeepEqual(unmarshaledEmpty, defaulted) {
t.Errorf(
"defaulted config does not match unmarshaled empty config: %v != %v",
defaulted, unmarshaledEmpty,
)
t.FailNow()
}
}
// un-json.Marshal-able type
type unencodable <-chan int
// "implement" config.Any
func (u unencodable) ApplyDefaults() {}
func (u unencodable) ToCurrent() *config.Config { return nil }
func (u unencodable) Validate() error { return nil }
func (u unencodable) Kind() string { return "bogus" }
func (u unencodable) APIVersion() string { return "bogus" }
func TestMarshal(t *testing.T) {
cases := []struct {
Name string
Config config.Any
ExpectError bool
}{
{
Name: "nil config",
Config: nil,
ExpectError: true,
},
{
Name: "wrong struct",
Config: make(unencodable),
ExpectError: true,
},
}
for _, tc := range cases {
_, err := Marshal(tc.Config)
if err != nil && !tc.ExpectError {
t.Errorf("case: '%s' got error `Marshal`ing and expected none: %v", tc.Name, err)
} else if err == nil && tc.ExpectError {
t.Errorf("case: '%s' got no error `Marshal`ing but expected one", tc.Name)
}
}
}

View File

@@ -0,0 +1,3 @@
# this file contains an invalid config api version for testing
kind: Config
apiVersion: not-valid

View File

@@ -0,0 +1,2 @@
# intentionally invalid yaml file for testing
":

View File

@@ -0,0 +1,3 @@
# technically valid, minimal config file
kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha1

View File

@@ -0,0 +1,10 @@
kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha1
nodeLifecycle:
preKubeadm:
- name: "pull an image"
command: "docker"
args: [ "pull", "ubuntu" ]
- name: "pull another image"
command: "docker"
args: [ "pull", "debian" ]

View File

@@ -0,0 +1,48 @@
/*
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 config
import "bytes"
// Errors implements error and contains all config errors
// This is returned by Config.Validate
type Errors struct {
errors []error
}
// NewErrors returns a new Errors from a slice of error
func NewErrors(errors []error) *Errors {
return &Errors{errors}
}
// assert Errors implements error
var _ error = &Errors{}
// Error implements the error interface
func (e *Errors) Error() string {
var buff bytes.Buffer
for _, err := range e.errors {
buff.WriteString(err.Error())
buff.WriteRune('\n')
}
return buff.String()
}
// Errors returns the slice of errors contained by Errors
func (e *Errors) Errors() []error {
return e.errors
}

View File

@@ -0,0 +1,58 @@
/*
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 config
// NOTE: if you change these types you likely need to update
// Validate() and DeepCopy() at minimum
// Config contains cluster creation config
// This is the current internal config type used by cluster
// Other API versions can be converted to this struct with Convert()
type Config struct {
// NumNodes is the number of nodes to create (currently only one is supported)
NumNodes int `json:"numNodes,omitempty"`
// KubeadmConfigTemplate allows overriding the default template in
// cluster/kubeadm
KubeadmConfigTemplate string `json:"kubeadmConfigTemplate,omitempty"`
// NodeLifecycle contains LifecycleHooks for phases of node provisioning
NodeLifecycle *NodeLifecycle `json:"nodeLifecycle,omitempty"`
}
// ensure current version implements the common interface for
// conversion, validation, etc.
var _ Any = &Config{}
// NodeLifecycle contains LifecycleHooks for phases of node provisioning
// Within each phase these hooks run in the order specified
type NodeLifecycle struct {
// PreBoot hooks run before starting systemd
PreBoot []LifecycleHook `json:"preBoot,omitempty"`
// PreKubeadm hooks run before `kubeadm`
PreKubeadm []LifecycleHook `json:"preKubeadm,omitempty"`
// PostKubeadm hooks run after `kubeadm`
PostKubeadm []LifecycleHook `json:"postKubeadm,omitempty"`
}
// LifecycleHook represents a command to run at points in the node lifecycle
type LifecycleHook struct {
// Name is used to improve logging (optional)
Name string `json:"name,omitempty"`
// Command is the command to run on the node
Command string `json:"command"`
// Args are the arguments to the command (optional)
Args []string `json:"args,omitempty"`
}

View File

@@ -0,0 +1,68 @@
/*
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 config
import "fmt"
// Validate returns a ConfigErrors with an entry for each problem
// with the config, or nil if there are none
func (c *Config) Validate() error {
errs := []error{}
// TODO(bentheelder): support multiple nodes
if c.NumNodes != 1 {
errs = append(errs, fmt.Errorf(
"%d nodes requested but only clusters with one node are supported currently",
c.NumNodes,
))
}
if c.NodeLifecycle != nil {
for _, hook := range c.NodeLifecycle.PreBoot {
if hook.Command == "" {
errs = append(errs, fmt.Errorf(
"preBoot hooks must set command to a non-empty value",
))
// we don't need to repeat this error and we don't
// have any others for this field
break
}
}
for _, hook := range c.NodeLifecycle.PreKubeadm {
if hook.Command == "" {
errs = append(errs, fmt.Errorf(
"preKubeadm hooks must set command to a non-empty value",
))
// we don't need to repeat this error and we don't
// have any others for this field
break
}
}
for _, hook := range c.NodeLifecycle.PostKubeadm {
if hook.Command == "" {
errs = append(errs, fmt.Errorf(
"postKubeadm hooks must set command to a non-empty value",
))
// we don't need to repeat this error and we don't
// have any others for this field
break
}
}
}
if len(errs) > 0 {
return NewErrors(errs)
}
return nil
}

View File

@@ -14,28 +14,75 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cluster
package config
import "testing"
func TestCreateConfigValidate(t *testing.T) {
func TestConfigValidate(t *testing.T) {
cases := []struct {
TestName string
Config *CreateConfig
Config *Config
ExpectedErrors int
}{
{
TestName: "Canonical config",
Config: NewCreateConfig(),
Config: New(),
ExpectedErrors: 0,
},
{
TestName: "Invalid number of nodes (not yet supported",
Config: &CreateConfig{
Config: &Config{
NumNodes: 2,
},
ExpectedErrors: 1,
},
{
TestName: "Invalid PreBoot hook",
Config: func() *Config {
cfg := New()
cfg.NodeLifecycle = &NodeLifecycle{
PreBoot: []LifecycleHook{
{
Command: "",
},
},
}
return cfg
}(),
ExpectedErrors: 1,
},
{
TestName: "Invalid PreKubeadm hook",
Config: func() *Config {
cfg := New()
cfg.NodeLifecycle = &NodeLifecycle{
PreKubeadm: []LifecycleHook{
{
Name: "pull an image",
Command: "",
},
},
}
return cfg
}(),
ExpectedErrors: 1,
},
{
TestName: "Invalid PostKubeadm hook",
Config: func() *Config {
cfg := New()
cfg.NodeLifecycle = &NodeLifecycle{
PostKubeadm: []LifecycleHook{
{
Name: "pull an image",
Command: "",
},
},
}
return cfg
}(),
ExpectedErrors: 1,
},
}
for _, tc := range cases {
@@ -48,8 +95,8 @@ func TestCreateConfigValidate(t *testing.T) {
}
continue
}
// - not castable to ConfigErrors, in which case we have the wrong error type ...
configErrors, ok := err.(ConfigErrors)
// - not castable to *Errors, in which case we have the wrong error type ...
configErrors, ok := err.(*Errors)
if !ok {
t.Errorf("config.Validate should only return nil or ConfigErrors{...}, got: %v for case: %s", err, tc.TestName)
continue

View File

@@ -0,0 +1,33 @@
/*
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 config
// APIVersion is the kubernetes-style API apiVersion for this Config package
const APIVersion = "kind.sigs.k8s.io/v1alpha1"
// Kind is the kubernetes-style API kind identifier for Config
const Kind = "Config"
// Kind returns the `kind:` for Config
func (c *Config) Kind() string {
return Kind
}
// APIVersion returns the `apiVersion:` for Config
func (c *Config) APIVersion() string {
return APIVersion
}

View File

@@ -31,19 +31,21 @@ type ConfigData struct {
KubernetesVersion string
// UnifiedControlPlaneImage - optional
UnifiedControlPlaneImage string
// AutoDerivedConfigData is populated by DeriveFields()
AutoDerivedConfigData
// DerivedConfigData is populated by Derive()
// These auto-generated fields are available to Config templates,
// but not meant to be set by hand
DerivedConfigData
}
// AutoDerivedConfigData fields are automatically derived by
// ConfigData.DeriveFieldsif they are not specified / zero valued
type AutoDerivedConfigData struct {
// DerivedConfigData fields are automatically derived by
// ConfigData.Derive if they are not specified / zero valued
type DerivedConfigData struct {
// DockerStableTag is automatically derived from KubernetesVersion
DockerStableTag string
}
// DeriveFields automatically derives DockerStableTag if not specified
func (c *ConfigData) DeriveFields() {
// Derive automatically derives DockerStableTag if not specified
func (c *ConfigData) Derive() {
if c.DockerStableTag == "" {
c.DockerStableTag = strings.Replace(c.KubernetesVersion, "+", "_", -1)
}
@@ -77,7 +79,7 @@ func Config(templateSource string, data ConfigData) (config string, err error) {
return "", errors.Wrap(err, "failed to parse config template")
}
// derive any automatic fields if not supplied
data.DeriveFields()
data.Derive()
// execute the template
var buff bytes.Buffer
err = t.Execute(&buff, data)