Skip to content

Commit

Permalink
[FEATURE] Config file compatible with Kustomize (#945)
Browse files Browse the repository at this point in the history
  • Loading branch information
erikgb authored Jan 31, 2022
1 parent 08bf145 commit 9a2a3ec
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 71 deletions.
3 changes: 2 additions & 1 deletion docs/usage/configfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ Since the config options and the config file are changing quite a bit, it's hard
# k3d configuration file, saved as e.g. /home/me/myk3dcluster.yaml
apiVersion: k3d.io/v1alpha4 # this will change in the future as we make everything more stable
kind: Simple # internally, we also have a Cluster config, which is not yet available externally
name: mycluster # name that you want to give to your cluster (will still be prefixed with `k3d-`)
metadata:
name: mycluster # name that you want to give to your cluster (will still be prefixed with `k3d-`)
servers: 1 # same as `--servers 1`
agents: 2 # same as `--agents 2`
kubeAPI: # same as `--api-port myhost.my.domain:6445` (where the name would resolve to 127.0.0.1)
Expand Down
3 changes: 2 additions & 1 deletion docs/usage/registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ If you're using a `SimpleConfig` file to configure your k3d cluster, you may as
```yaml
apiVersion: k3d.io/v1alpha4
kind: Simple
name: test
metadata:
name: test
servers: 1
agents: 2
registries:
Expand Down
8 changes: 6 additions & 2 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func TestReadSimpleConfig(t *testing.T) {
APIVersion: "k3d.io/v1alpha4",
Kind: "Simple",
},
Name: "test",
ObjectMeta: configtypes.ObjectMeta{
Name: "test",
},
Servers: 1,
Agents: 2,
ExposeAPI: exposedAPI,
Expand Down Expand Up @@ -268,7 +270,9 @@ func TestReadSimpleConfigRegistries(t *testing.T) {
APIVersion: "k3d.io/v1alpha4",
Kind: "Simple",
},
Name: "test",
ObjectMeta: configtypes.ObjectMeta{
Name: "test",
},
Servers: 1,
Agents: 1,
Registries: conf.SimpleConfigRegistries{
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestValidateSchemaFail(t *testing.T) {
t.Errorf("Validation of config file %s against the default schema passed where we expected a failure", cfgPath)
}

expectedErrorText := `- name: Invalid type. Expected: string, given: integer
expectedErrorText := `- metadata.name: Invalid type. Expected: string, given: integer
`

if err.Error() != expectedErrorText {
Expand Down
97 changes: 57 additions & 40 deletions pkg/config/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,59 +26,76 @@ import (
"testing"

"github.com/go-test/deep"
"github.com/rancher/k3d/v5/pkg/config/v1alpha3"
"github.com/rancher/k3d/v5/pkg/config/v1alpha4"
l "github.com/rancher/k3d/v5/pkg/logger"
"github.com/spf13/viper"
)

func TestMigrateV1Alpha2ToV1Alpha3(t *testing.T) {

actualPath := "test_assets/config_test_simple_migration_v1alpha2.yaml"
expectedPath := "test_assets/config_test_simple_migration_v1alpha3.yaml"

actualViper := viper.New()
expectedViper := viper.New()
func TestMigrate(t *testing.T) {
tests := map[string]struct {
targetVersion string
actualPath string
expectedPath string
}{
"V1Alpha2ToV1Alpha3": {
targetVersion: v1alpha3.ApiVersion,
actualPath: "test_assets/config_test_simple_migration_v1alpha2.yaml",
expectedPath: "test_assets/config_test_simple_migration_v1alpha3.yaml",
},
"V1Alpha2ToV1Alpha4": {
targetVersion: v1alpha4.ApiVersion,
actualPath: "test_assets/config_test_simple_migration_v1alpha2.yaml",
expectedPath: "test_assets/config_test_simple_migration_v1alpha4.yaml",
},
"V1Alpha3ToV1Alpha4": {
targetVersion: v1alpha4.ApiVersion,
actualPath: "test_assets/config_test_simple_migration_v1alpha3.yaml",
expectedPath: "test_assets/config_test_simple_migration_v1alpha4.yaml",
},
}

actualViper.SetConfigType("yaml")
expectedViper.SetConfigType("yaml")
for name, tc := range tests {
t.Run(name, func(t *testing.T) {

actualViper.SetConfigFile(actualPath)
expectedViper.SetConfigFile(expectedPath)
actualViper := viper.New()
expectedViper := viper.New()

if err := actualViper.ReadInConfig(); err != nil {
t.Fatal(err)
}
actualViper.SetConfigType("yaml")
expectedViper.SetConfigType("yaml")

if err := expectedViper.ReadInConfig(); err != nil {
t.Fatal(err)
}
actualViper.SetConfigFile(tc.actualPath)
expectedViper.SetConfigFile(tc.expectedPath)

actualCfg, err := FromViper(actualViper)
if err != nil {
t.Fatal(err)
}
if err := actualViper.ReadInConfig(); err != nil {
t.Fatal(err)
}

if actualCfg.GetAPIVersion() != DefaultConfigApiVersion {
actualCfg, err = Migrate(actualCfg, DefaultConfigApiVersion)
if err != nil {
l.Log().Fatalln(err)
}
}
if err := expectedViper.ReadInConfig(); err != nil {
t.Fatal(err)
}

expectedCfg, err := FromViper(expectedViper)
if err != nil {
t.Fatal(err)
}
actualCfg, err := FromViper(actualViper)
if err != nil {
t.Fatal(err)
}

if diff := deep.Equal(actualCfg, expectedCfg); diff != nil {
t.Fatalf("Actual\n%#v\ndoes not match expected\n%+v\nDiff:\n%#v", actualCfg, expectedCfg, diff)
}
if actualCfg.GetAPIVersion() != tc.targetVersion {
actualCfg, err = Migrate(actualCfg, tc.targetVersion)
if err != nil {
l.Log().Fatalln(err)
}
}

}
expectedCfg, err := FromViper(expectedViper)
if err != nil {
t.Fatal(err)
}

func TestMigrateV1Alpha2ToV1Alpha4(t *testing.T) {
t.Log("not implemented") // TODO: test migration v1alpha2 to v1alpha4
}
if diff := deep.Equal(actualCfg, expectedCfg); diff != nil {
t.Fatalf("Actual\n%#v\ndoes not match expected\n%+v\nDiff:\n%#v", actualCfg, expectedCfg, diff)
}

func TestMigrateV1Alpha3ToV1Alpha4(t *testing.T) {
t.Log("not implemented") // TODO: test migration v1alpha3 to v1alpha4
})
}
}
3 changes: 2 additions & 1 deletion pkg/config/test_assets/config_test_registries.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apiVersion: k3d.io/v1alpha4
kind: Simple
name: test
metadata:
name: test
servers: 1
agents: 1
registries:
Expand Down
3 changes: 2 additions & 1 deletion pkg/config/test_assets/config_test_simple.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apiVersion: k3d.io/v1alpha4
kind: Simple
name: test
metadata:
name: test
servers: 1
agents: 2
kubeAPI:
Expand Down
3 changes: 2 additions & 1 deletion pkg/config/test_assets/config_test_simple_2.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
apiVersion: k3d.io/v1alpha4
kind: Simple
name: supertest
metadata:
name: supertest
agents: 8
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apiVersion: k3d.io/v1alpha4
kind: Simple
name: 1234
metadata:
name: 1234
servers: 1
agents: 2
kubeAPI:
Expand Down
56 changes: 56 additions & 0 deletions pkg/config/test_assets/config_test_simple_migration_v1alpha4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
apiVersion: k3d.io/v1alpha4
kind: Simple
metadata:
name: test
servers: 3
agents: 2
kubeAPI:
hostIP: "0.0.0.0"
hostPort: "6446"
#image: rancher/k3s:latest
volumes:
- volume: /my/path:/some/path
nodeFilters:
- all
ports:
- port: 80:80
nodeFilters:
- loadbalancer
- port: 0.0.0.0:443:443
nodeFilters:
- loadbalancer
env:
- envVar: bar=baz,bob
nodeFilters:
- all
registries:
create:
name: k3d-test-registry
host: "0.0.0.0"
hostPort: random
config: |
mirrors:
"my.company.registry":
endpoint:
- http://my.company.registry:5000
options:
k3d:
wait: true
timeout: "360s" # should be pretty high for multi-server clusters to allow for a proper startup routine
disableLoadbalancer: false
disableImageVolume: false
k3s:
extraArgs:
- arg: --tls-san=127.0.0.1
nodeFilters:
- server:*
kubeconfig:
updateDefaultKubeconfig: true
switchCurrentContext: true
runtime:
labels:
- label: foo=bar
nodeFilters:
- server:0
- loadbalancer
5 changes: 5 additions & 0 deletions pkg/config/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type TypeMeta struct {
APIVersion string `mapstructure:"apiVersion,omitempty" yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"`
}

// ObjectMeta got its name from the Kubernetes counterpart.
type ObjectMeta struct {
Name string `mapstructure:"name,omitempty" yaml:"name,omitempty" json:"name,omitempty"`
}

// Config interface.
type Config interface {
GetKind() string
Expand Down
44 changes: 44 additions & 0 deletions pkg/config/v1alpha4/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ THE SOFTWARE.
package v1alpha4

import (
"encoding/json"
"fmt"

configtypes "github.com/rancher/k3d/v5/pkg/config/types"
Expand Down Expand Up @@ -52,5 +53,48 @@ func MigrateV1Alpha2(input configtypes.Config) (configtypes.Config, error) {
func MigrateV1Alpha3(input configtypes.Config) (configtypes.Config, error) {
l.Log().Debugln("Migrating v1alpha3 to v1alpha4")

/*
* We're migrating matching fields between versions by marshalling to JSON and back
*/
injson, err := json.Marshal(input)
if err != nil {
return nil, err
}

/*
* Migrate config of `kind: Simple`
*/
if input.GetKind() == "Simple" {
cfgIntermediate := v1alpha3.SimpleConfig{}

if err := json.Unmarshal(injson, &cfgIntermediate); err != nil {
return nil, err
}

intermediateJSON, err := json.Marshal(cfgIntermediate)
if err != nil {
return nil, err
}
cfg := SimpleConfig{}
if err := json.Unmarshal(intermediateJSON, &cfg); err != nil {
return nil, err
}

cfg.Name = cfgIntermediate.Name

/*
* Finalizing
*/

cfg.APIVersion = ApiVersion

l.Log().Debugf("Migrated config: %+v", cfg)

return cfg, nil

}

l.Log().Debugf("No migration needed for %s#%s -> %s#%s", input.GetAPIVersion(), input.GetKind(), ApiVersion, input.GetKind())

return input, nil
}
16 changes: 11 additions & 5 deletions pkg/config/v1alpha4/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@
],
"default": "Simple"
},
"name": {
"description": "Name of the cluster (must be a valid hostname and will be prefixed with 'k3d-'). Example: 'mycluster'.",
"type": "string",
"format": "hostname"
"metadata": {
"type": "object",
"properties": {
"name": {
"description": "Name of the cluster (must be a valid hostname and will be prefixed with 'k3d-'). Example: 'mycluster'.",
"type": "string",
"format": "hostname"
}
},
"additionalProperties": false
},
"servers": {
"type": "number",
Expand Down Expand Up @@ -220,7 +226,7 @@
"type": "string"
},
"hostPidMode": {
"type":"boolean",
"type": "boolean",
"default": false
},
"labels": {
Expand Down
30 changes: 15 additions & 15 deletions pkg/config/v1alpha4/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,21 @@ type SimpleConfigRegistries struct {

// SimpleConfig describes the toplevel k3d configuration file.
type SimpleConfig struct {
config.TypeMeta `mapstructure:",squash" yaml:",inline"`
Name string `mapstructure:"name" yaml:"name,omitempty" json:"name,omitempty"`
Servers int `mapstructure:"servers" yaml:"servers,omitempty" json:"servers,omitempty"` //nolint:lll // default 1
Agents int `mapstructure:"agents" yaml:"agents,omitempty" json:"agents,omitempty"` //nolint:lll // default 0
ExposeAPI SimpleExposureOpts `mapstructure:"kubeAPI" yaml:"kubeAPI,omitempty" json:"kubeAPI,omitempty"`
Image string `mapstructure:"image" yaml:"image,omitempty" json:"image,omitempty"`
Network string `mapstructure:"network" yaml:"network,omitempty" json:"network,omitempty"`
Subnet string `mapstructure:"subnet" yaml:"subnet,omitempty" json:"subnet,omitempty"`
ClusterToken string `mapstructure:"token" yaml:"clusterToken,omitempty" json:"clusterToken,omitempty"` // default: auto-generated
Volumes []VolumeWithNodeFilters `mapstructure:"volumes" yaml:"volumes,omitempty" json:"volumes,omitempty"`
Ports []PortWithNodeFilters `mapstructure:"ports" yaml:"ports,omitempty" json:"ports,omitempty"`
Options SimpleConfigOptions `mapstructure:"options" yaml:"options,omitempty" json:"options,omitempty"`
Env []EnvVarWithNodeFilters `mapstructure:"env" yaml:"env,omitempty" json:"env,omitempty"`
Registries SimpleConfigRegistries `mapstructure:"registries" yaml:"registries,omitempty" json:"registries,omitempty"`
HostAliases []k3d.HostAlias `mapstructure:"hostAliases" yaml:"hostAliases,omitempty" json:"hostAliases,omitempty"`
config.TypeMeta `mapstructure:",squash" yaml:",inline"`
config.ObjectMeta `mapstructure:"metadata" yaml:"metadata,omitempty" json:"metadata,omitempty"`
Servers int `mapstructure:"servers" yaml:"servers,omitempty" json:"servers,omitempty"` //nolint:lll // default 1
Agents int `mapstructure:"agents" yaml:"agents,omitempty" json:"agents,omitempty"` //nolint:lll // default 0
ExposeAPI SimpleExposureOpts `mapstructure:"kubeAPI" yaml:"kubeAPI,omitempty" json:"kubeAPI,omitempty"`
Image string `mapstructure:"image" yaml:"image,omitempty" json:"image,omitempty"`
Network string `mapstructure:"network" yaml:"network,omitempty" json:"network,omitempty"`
Subnet string `mapstructure:"subnet" yaml:"subnet,omitempty" json:"subnet,omitempty"`
ClusterToken string `mapstructure:"token" yaml:"clusterToken,omitempty" json:"clusterToken,omitempty"` // default: auto-generated
Volumes []VolumeWithNodeFilters `mapstructure:"volumes" yaml:"volumes,omitempty" json:"volumes,omitempty"`
Ports []PortWithNodeFilters `mapstructure:"ports" yaml:"ports,omitempty" json:"ports,omitempty"`
Options SimpleConfigOptions `mapstructure:"options" yaml:"options,omitempty" json:"options,omitempty"`
Env []EnvVarWithNodeFilters `mapstructure:"env" yaml:"env,omitempty" json:"env,omitempty"`
Registries SimpleConfigRegistries `mapstructure:"registries" yaml:"registries,omitempty" json:"registries,omitempty"`
HostAliases []k3d.HostAlias `mapstructure:"hostAliases" yaml:"hostAliases,omitempty" json:"hostAliases,omitempty"`
}

// SimpleExposureOpts provides a simplified syntax compared to the original k3d.ExposureOpts
Expand Down
Loading

0 comments on commit 9a2a3ec

Please sign in to comment.