From 8a718c497945ef1e61392d60e0df513c6f1a5f68 Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Mon, 8 May 2023 15:36:35 +0300 Subject: [PATCH] Make more testable, add some tests, fix pidcheck bug Signed-off-by: Kimmo Lehto --- pkg/config/cfgvars.go | 34 +++- pkg/config/cfgvars_test.go | 166 ++++++++++++++++ pkg/config/cli.go | 4 +- pkg/config/config_test.go | 385 ------------------------------------- pkg/config/runtime.go | 41 +++- pkg/config/runtime_test.go | 127 ++++++++++++ 6 files changed, 354 insertions(+), 403 deletions(-) create mode 100644 pkg/config/cfgvars_test.go delete mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/runtime_test.go diff --git a/pkg/config/cfgvars.go b/pkg/config/cfgvars.go index 6113f1b629cd..515648ec4e82 100644 --- a/pkg/config/cfgvars.go +++ b/pkg/config/cfgvars.go @@ -32,7 +32,6 @@ import ( "github.com/avast/retry-go" "github.com/imdario/mergo" - "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -92,7 +91,18 @@ func (c *CfgVars) DeepCopy() *CfgVars { type CfgVarOption func(*CfgVars) -func WithCommand(cmd *cobra.Command) CfgVarOption { +// FlagSet represents pflag.FlagSet that can be received from cobra.Command.Flags() +type flagSet interface { + GetString(name string) (string, error) + GetBool(name string) (bool, error) +} + +// Command represents cobra.Command +type command interface { + Flags() flagSet +} + +func WithCommand(cmd command) CfgVarOption { return func(c *CfgVars) { flags := cmd.Flags() @@ -114,6 +124,8 @@ func WithCommand(cmd *cobra.Command) CfgVarOption { if f, err := flags.GetBool("single"); err == nil && f { c.DefaultStorageType = v1beta1.KineStorageType + } else { + c.DefaultStorageType = v1beta1.EtcdStorageType } } } @@ -127,17 +139,17 @@ func DefaultCfgVars() *CfgVars { return vars } -// NewCfgVars returns a new CfgVars struct -func NewCfgVars(cmd *cobra.Command, dirs ...string) (*CfgVars, error) { +// NewCfgVars returns a new CfgVars struct. +func NewCfgVars(cobraCmd command, dirs ...string) (*CfgVars, error) { var dataDir string if len(dirs) > 0 { dataDir = dirs[0] } - if cmd != nil { - if f := cmd.Flags().Lookup("data-dir"); f != nil { - dataDir = f.Value.String() + if cobraCmd != nil { + if val, err := cobraCmd.Flags().GetString("data-dir"); err == nil && val != "" { + dataDir = val } } @@ -188,8 +200,8 @@ func NewCfgVars(cmd *cobra.Command, dirs ...string) (*CfgVars, error) { origin: CfgVarsOriginDefault, } - if cmd != nil { - WithCommand(cmd)(vars) + if cobraCmd != nil { + WithCommand(cobraCmd)(vars) } return vars, nil @@ -213,6 +225,8 @@ func (c *CfgVars) defaultStorageSpec() *v1beta1.StorageSpec { return v1beta1.DefaultStorageSpec() } +var defaultConfigPath = constant.K0sConfigPathDefault + func (c *CfgVars) NodeConfig() (*v1beta1.ClusterConfig, error) { if c.nodeConfig != nil { return c.nodeConfig, nil @@ -229,7 +243,7 @@ func (c *CfgVars) NodeConfig() (*v1beta1.ClusterConfig, error) { var nodeConfig *v1beta1.ClusterConfig cfgContent, err := os.ReadFile(c.StartupConfigPath) - if errors.Is(err, os.ErrNotExist) && c.StartupConfigPath == constant.K0sConfigPathDefault { + if errors.Is(err, os.ErrNotExist) && c.StartupConfigPath == defaultConfigPath { nodeConfig = v1beta1.DefaultClusterConfig(c.defaultStorageSpec()) } else if err == nil { cfg, err := v1beta1.ConfigFromString(string(cfgContent), c.defaultStorageSpec()) diff --git a/pkg/config/cfgvars_test.go b/pkg/config/cfgvars_test.go new file mode 100644 index 000000000000..b2ea97ec9086 --- /dev/null +++ b/pkg/config/cfgvars_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2023 k0s 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" + "reflect" + "strconv" + "testing" + + "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + "github.com/k0sproject/k0s/pkg/constant" + "github.com/stretchr/testify/assert" +) + +type FakeFlagSet struct { + values map[string]string +} + +func (f *FakeFlagSet) GetString(name string) (string, error) { + if v, ok := f.values[name]; ok { + return v, nil + } + return "", fmt.Errorf("flag %q not found", name) +} + +func (f *FakeFlagSet) GetBool(name string) (bool, error) { + if v, ok := f.values[name]; ok { + b, err := strconv.ParseBool(v) + if err != nil { + return false, fmt.Errorf("invalid value for flag %q: %s", name, v) + } + return b, nil + } + return false, fmt.Errorf("flag %q not found", name) +} + +type FakeCommand struct { + flagSet flagSet +} + +func (f *FakeCommand) Flags() flagSet { + return f.flagSet +} + +func TestWithCommand(t *testing.T) { + // Create a fake flag set with some values + fakeFlags := &FakeFlagSet{ + values: map[string]string{ + "data-dir": "/path/to/data", + "config": "/path/to/config", + "status-socket": "/path/to/socket", + "enable-dynamic-config": "true", + }, + } + + // Create a fake command that returns the fake flag set + fakeCmd := &FakeCommand{ + flagSet: fakeFlags, + } + + // Create a CfgVars struct and apply the options + c := &CfgVars{} + WithCommand(fakeCmd)(c) + + assert.Equal(t, "/path/to/data", c.DataDir) + assert.Equal(t, "/path/to/config", c.StartupConfigPath) + assert.Equal(t, "/path/to/socket", c.StatusSocketPath) + assert.True(t, c.EnableDynamicConfig) +} + +func TestWithCommand_DefaultsAndOverrides(t *testing.T) { + // Define test cases for the single flag + testCases := []struct { + name string + flagValue string + expectedStorageType string + }{ + { + name: "single flag is not set", + flagValue: "", + expectedStorageType: v1beta1.EtcdStorageType, + }, + { + name: "single flag is set to false", + flagValue: "false", + expectedStorageType: v1beta1.EtcdStorageType, + }, + { + name: "single flag is set to true", + flagValue: "true", + expectedStorageType: v1beta1.KineStorageType, + }, + } + + // Create a fake command with a flag set that includes the test cases + fakeFlags := &FakeFlagSet{ + values: map[string]string{}, + } + for _, tc := range testCases { + fakeFlags.values["single"] = tc.flagValue + + c := &CfgVars{} + WithCommand(&FakeCommand{flagSet: fakeFlags})(c) + + assert.Equal(t, tc.expectedStorageType, c.DefaultStorageType, tc.name) + } +} + +func TestNewCfgVars_DataDir(t *testing.T) { + tests := []struct { + name string + fakeCmd command + dirs []string + expected *CfgVars + }{ + { + name: "default data dir", + fakeCmd: &FakeCommand{flagSet: &FakeFlagSet{}}, + expected: &CfgVars{DataDir: constant.DataDirDefault}, + }, + { + name: "custom data dir", + fakeCmd: &FakeCommand{ + flagSet: &FakeFlagSet{values: map[string]string{"data-dir": "/path/to/data"}}, + }, + expected: &CfgVars{DataDir: "/path/to/data"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := NewCfgVars(tt.fakeCmd, tt.dirs...) + assert.NoError(t, err) + assert.Equal(t, tt.expected.DataDir, c.DataDir) + }) + } +} + +func TestNodeConfig_Default(t *testing.T) { + oldDefaultPath := defaultConfigPath + defer func() { defaultConfigPath = oldDefaultPath }() + defaultConfigPath = "/tmp/k0s.yaml.nonexistent" + + c := &CfgVars{StartupConfigPath: defaultConfigPath} + + nodeConfig, err := c.NodeConfig() + + assert.NoError(t, err) + assert.NotNil(t, nodeConfig) + assert.True(t, reflect.DeepEqual(v1beta1.DefaultClusterConfig(c.defaultStorageSpec()), nodeConfig)) +} diff --git a/pkg/config/cli.go b/pkg/config/cli.go index 06103ec69d0b..2fb693c62b48 100644 --- a/pkg/config/cli.go +++ b/pkg/config/cli.go @@ -240,8 +240,8 @@ func FileInputFlag() *pflag.FlagSet { return flagset } -func GetCmdOpts(cmd *cobra.Command) (*CLIOptions, error) { - k0sVars, err := NewCfgVars(cmd) +func GetCmdOpts(cobraCmd command) (*CLIOptions, error) { + k0sVars, err := NewCfgVars(cobraCmd) if err != nil { return nil, err } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go deleted file mode 100644 index 445e9197db7e..000000000000 --- a/pkg/config/config_test.go +++ /dev/null @@ -1,385 +0,0 @@ -/* -Copyright 2021 k0s 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 "testing" - -func TestGetConfigFromFile(t *testing.T) { - t.Fatal("not implemented") -} - -/* -todo: reimplement/cleanup/whatever - -import ( - "context" - "fmt" - "net/url" - "os" - "path" - "path/filepath" - "testing" - - "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - "github.com/k0sproject/k0s/pkg/client/clientset/fake" - k0sv1beta1 "github.com/k0sproject/k0s/pkg/client/clientset/typed/k0s/v1beta1" - "github.com/k0sproject/k0s/pkg/constant" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - cOpts = v1.CreateOptions{TypeMeta: resourceType} - fileYaml = ` -apiVersion: k0s.k0sproject.io/v1beta1 -kind: ClusterConfig -spec: - api: - externalAddress: file-external-address - network: - serviceCIDR: 12.12.12.12/12 - podCIDR: 13.13.13.13/13 - kubeProxy: - mode: ipvs -` - apiYaml = ` -apiVersion: k0s.k0sproject.io/v1beta1 -kind: ClusterConfig -spec: - api: - externalAddress: api_external_address - network: - serviceCIDR: api_cidr -` -) - -// Test using config from a yaml file -func TestGetConfigFromFile(t *testing.T) { - CfgFile = writeConfigFile(t, fileYaml) - - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - } - err := loadingRules.InitRuntimeConfig(constant.GetConfig(t.TempDir())) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to load config: %s", err.Error()) - } - if cfg == nil { - t.Fatal("received an empty config! failing") - } - testCases := []struct { - name string - got string - expected string - }{ - {"API_external_address", cfg.Spec.API.ExternalAddress, "file-external-address"}, - {"Network_ServiceCIDR", cfg.Spec.Network.ServiceCIDR, "12.12.12.12/12"}, - {"Network_KubeProxy_Mode", cfg.Spec.Network.KubeProxy.Mode, "ipvs"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s eq %s", tc.name, tc.expected), func(t *testing.T) { - if tc.got != tc.expected { - t.Fatalf("expected to read '%s' for the %s test value. Got: %s", tc.expected, tc.name, tc.got) - } - }) - } -} - -func TestExternalEtcdConfig(t *testing.T) { - yamlData := ` -spec: - storage: - type: etcd - etcd: - externalCluster: - endpoints: - - http://etcd0:2379 - etcdPrefix: k0s-tenant` - - CfgFile = writeConfigFile(t, yamlData) - - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - } - err := loadingRules.InitRuntimeConfig(constant.GetConfig(t.TempDir())) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to load config: %s", err.Error()) - } - if cfg == nil { - t.Fatal("received an empty config! failing") - } - testCases := []struct { - name string - got string - expected string - }{ - {"Storage_Type", cfg.Spec.Storage.Type, "etcd"}, - {"External_Cluster_Endpoint", cfg.Spec.Storage.Etcd.ExternalCluster.Endpoints[0], "http://etcd0:2379"}, - {"External_Cluster_Prefix", cfg.Spec.Storage.Etcd.ExternalCluster.EtcdPrefix, "k0s-tenant"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s eq %s", tc.name, tc.expected), func(t *testing.T) { - if tc.got != tc.expected { - t.Fatalf("expected to read '%s' for the %s test value. Got: %s", tc.expected, tc.name, tc.got) - } - }) - } -} - -func TestConfigFromDefaults(t *testing.T) { - CfgFile = "" - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - } - err := loadingRules.InitRuntimeConfig(constant.GetConfig(t.TempDir())) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to load config: %s", err.Error()) - } - if cfg == nil { - t.Fatal("received an empty config! failing") - } - testCases := []struct { - name string - got string - expected string - }{ - {"API_external_address", cfg.Spec.API.ExternalAddress, ""}, - {"Network_ServiceCIDR", cfg.Spec.Network.ServiceCIDR, "10.96.0.0/12"}, - {"Network_KubeProxy_Mode", cfg.Spec.Network.KubeProxy.Mode, "iptables"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s eq %s", tc.name, tc.expected), func(t *testing.T) { - if tc.got != tc.expected { - t.Fatalf("expected to read '%s' for the %s test value. Got: %s", tc.expected, tc.name, tc.got) - } - }) - } -} - -// Test using node-config from a file when API config is enabled -func TestNodeConfigWithAPIConfig(t *testing.T) { - cfgFilePath := writeConfigFile(t, fileYaml) - CfgFile = cfgFilePath - - // if API config is enabled, Nodeconfig will be stripped of any cluster-wide-config settings - controllerOpts.EnableDynamicConfig = true - - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - Nodeconfig: true, - } - - err := loadingRules.InitRuntimeConfig(constant.GetConfig(t.TempDir())) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to fetch Node Config: %s", err.Error()) - } - testCases := []struct { - name string - got string - expected string - }{ - {"API_external_address", cfg.Spec.API.ExternalAddress, "file-external-address"}, - // PodCIDR is a cluster-wide setting. It shouldn't exist in Node config - {"Network_PodCIDR", cfg.Spec.Network.PodCIDR, ""}, - {"Network_ServiceCIDR", cfg.Spec.Network.ServiceCIDR, "12.12.12.12/12"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s eq %s", tc.name, tc.expected), func(t *testing.T) { - if tc.got != tc.expected { - t.Fatalf("expected to read '%s' for the %s test value. Got: %s", tc.expected, tc.name, tc.got) - } - }) - } -} - -func TestSingleNodeConfig(t *testing.T) { - - yamlData := ` -spec: - api: - address: 1.2.3.4` - - CfgFile = writeConfigFile(t, yamlData) - - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - Nodeconfig: true, - } - tempDir := t.TempDir() - k0sVars := constant.GetConfig(tempDir) - k0sVars.DefaultStorageType = "kine" - - err := loadingRules.InitRuntimeConfig(k0sVars) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to load config: %s", err.Error()) - } - - if cfg == nil { - t.Fatal("received an empty config! failing") - } - - assert.Equal(t, "kine", cfg.Spec.Storage.Type, "Storage type mismatch") - assert.Contains(t, - cfg.Spec.Storage.Kine.DataSource, - (&url.URL{ - Scheme: "sqlite", - OmitHost: true, - Path: filepath.ToSlash(filepath.Join(tempDir, "db", "state.db")), - }).String(), - "Data source mismatch", - ) -} - -func TestSingleNodeConfigWithEtcd(t *testing.T) { - yamlData := ` -spec: - storage: - type: etcd` - - CfgFile = writeConfigFile(t, yamlData) - - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - Nodeconfig: true, - } - k0sVars := constant.GetConfig(t.TempDir()) - k0sVars.DefaultStorageType = "kine" - - err := loadingRules.InitRuntimeConfig(k0sVars) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to load config: %s", err.Error()) - } - if cfg == nil { - t.Fatal("received an empty config! failing") - } - testCases := []struct { - name string - got string - expected string - }{{"Storage_Type", cfg.Spec.Storage.Type, "etcd"}} // config file storage type trumps k0sVars.DefaultStorageType - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s eq %s", tc.name, tc.expected), func(t *testing.T) { - if tc.got != tc.expected { - t.Fatalf("expected to read '%s' for the %s test value. Got: %s", tc.expected, tc.name, tc.got) - } - }) - } -} - -// when a component requests an API config, -// the merged node and cluster config should be returned -func TestAPIConfig(t *testing.T) { - CfgFile = writeConfigFile(t, fileYaml) - - controllerOpts.EnableDynamicConfig = true - // create the API config using a fake client - client := fake.NewSimpleClientset() - - createFakeAPIConfig(t, client.K0sV1beta1()) - - loadingRules := ClientConfigLoadingRules{ - RuntimeConfigPath: nonExistentPath(t), - APIClient: client.K0sV1beta1(), - } - err := loadingRules.InitRuntimeConfig(constant.GetConfig(t.TempDir())) - if err != nil { - t.Fatalf("failed to initialize k0s config: %s", err.Error()) - } - - cfg, err := loadingRules.Load() - if err != nil { - t.Fatalf("failed to fetch Node Config: %s", err.Error()) - } - - testCases := []struct { - name string - got string - expected string - }{ - {"API_external_address", cfg.Spec.API.ExternalAddress, "file-external-address"}, - {"Network_PodCIDR", cfg.Spec.Network.PodCIDR, "10.244.0.0/16"}, - {"Network_ServiceCIDR", cfg.Spec.Network.ServiceCIDR, "12.12.12.12/12"}, - {"Network_KubeProxy_Mode", cfg.Spec.Network.KubeProxy.Mode, "iptables"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s eq %s", tc.name, tc.expected), func(t *testing.T) { - if tc.got != tc.expected { - t.Fatalf("expected to read '%s' for the %s test value. Got: %s", tc.expected, tc.name, tc.got) - } - }) - } -} - -func writeConfigFile(t *testing.T, yamlData string) (filePath string) { - cfgFilePath := path.Join(t.TempDir(), "k0s-config.yaml") - require.NoError(t, os.WriteFile(cfgFilePath, []byte(yamlData), 0644)) - return cfgFilePath -} - -func nonExistentPath(t *testing.T) string { - return path.Join(t.TempDir(), "non-existent") -} - -func createFakeAPIConfig(t *testing.T, client k0sv1beta1.K0sV1beta1Interface) { - clusterConfigs := client.ClusterConfigs(constant.ClusterConfigNamespace) - - config, err := v1beta1.ConfigFromString(apiYaml, v1beta1.DefaultStorageSpec()) - require.NoError(t, err) - - _, err = clusterConfigs.Create(context.TODO(), config.GetClusterWideConfig().StripDefaults(), cOpts) - require.NoError(t, err) -} -*/ diff --git a/pkg/config/runtime.go b/pkg/config/runtime.go index 6652fda3f175..ba940220aeec 100644 --- a/pkg/config/runtime.go +++ b/pkg/config/runtime.go @@ -24,6 +24,7 @@ import ( "os" "path/filepath" "strings" + "syscall" "github.com/k0sproject/k0s/internal/pkg/dir" "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" @@ -39,7 +40,11 @@ const ( RuntimeConfigKind = "RuntimeConfig" ) -var ErrK0sNotRunning = errors.New("k0s is not running") +var ( + ErrK0sNotRunning = errors.New("k0s is not running") + ErrK0sAlreadyRunning = errors.New("an instance of k0s is already running") + ErrInvalidRuntimeConfig = errors.New("invalid runtime config") +) // Runtime config is a static copy of the start up config and CfgVars that is used by // commands that do not have a --config parameter of their own, such as `k0s token create`. @@ -47,7 +52,7 @@ var ErrK0sNotRunning = errors.New("k0s is not running") // `--data-dir` will be reused by the commands without the user having to specify them again. type RuntimeConfig struct { metav1.ObjectMeta `json:"metadata,omitempty"` - metav1.TypeMeta `json:"typeMeta,omitempty"` + metav1.TypeMeta `json:",omitempty,inline"` Spec *RuntimeConfigSpec `json:"spec"` } @@ -75,14 +80,31 @@ func LoadRuntimeConfig(k0sVars *CfgVars) (*RuntimeConfigSpec, error) { return nil, err } + if config.APIVersion != v1beta1.ClusterConfigAPIVersion { + return nil, fmt.Errorf("%w: invalid api version: %s", ErrInvalidRuntimeConfig, config.APIVersion) + } + + if config.Kind != RuntimeConfigKind { + return nil, fmt.Errorf("%w: invalid kind: %s", ErrInvalidRuntimeConfig, config.Kind) + } + spec := config.Spec + if spec == nil { + return nil, fmt.Errorf("%w: spec is nil", ErrInvalidRuntimeConfig) + } - if _, err := os.FindProcess(spec.Pid); err != nil { + proc, err := os.FindProcess(spec.Pid) + if err != nil { + return nil, fmt.Errorf("failed to find k0s process by pid: %w", err) + } + + // check if the process is still running + if err := proc.Signal(syscall.Signal(0)); err != nil { // apparently this is a leftover runtime config file for a pid that does not exist, so remove it here // and return an error defer func() { _ = spec.Cleanup() }() - return nil, ErrK0sNotRunning + return nil, errors.Join(ErrK0sNotRunning, err) } return spec, nil @@ -96,7 +118,7 @@ func migrateLegacyRuntimeConfig(k0sVars *CfgVars, content []byte) (*RuntimeConfi } // generate a new runtime config - return &RuntimeConfigSpec{K0sVars: k0sVars, NodeConfig: cfg}, nil + return &RuntimeConfigSpec{K0sVars: k0sVars, NodeConfig: cfg, Pid: os.Getpid()}, nil } func isLegacy(data []byte) bool { @@ -120,7 +142,7 @@ func isLegacy(data []byte) bool { func NewRuntimeConfig(k0sVars *CfgVars) (*RuntimeConfigSpec, error) { if _, err := LoadRuntimeConfig(k0sVars); err == nil { - return nil, fmt.Errorf("an instance of k0s is already running") + return nil, ErrK0sAlreadyRunning } nodeConfig, err := k0sVars.NodeConfig() @@ -134,6 +156,9 @@ func NewRuntimeConfig(k0sVars *CfgVars) (*RuntimeConfigSpec, error) { vars.StartupConfigPath = "" cfg := &RuntimeConfig{ + ObjectMeta: metav1.ObjectMeta{ + CreationTimestamp: metav1.Now(), + }, TypeMeta: metav1.TypeMeta{ APIVersion: v1beta1.ClusterConfigAPIVersion, Kind: RuntimeConfigKind, @@ -162,6 +187,10 @@ func NewRuntimeConfig(k0sVars *CfgVars) (*RuntimeConfigSpec, error) { } func (r *RuntimeConfigSpec) Cleanup() error { + if r == nil || r.K0sVars == nil { + return nil + } + if err := os.Remove(r.K0sVars.RuntimeConfigPath); err != nil { return fmt.Errorf("failed to clean up runtime config file: %w", err) } diff --git a/pkg/config/runtime_test.go b/pkg/config/runtime_test.go new file mode 100644 index 000000000000..0e57882e5870 --- /dev/null +++ b/pkg/config/runtime_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2023 k0s 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 ( + "os" + "testing" + + "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + "github.com/stretchr/testify/assert" +) + +func TestLoadRuntimeConfig_Legacy(t *testing.T) { + tmpfile, err := os.CreateTemp("", "runtime-config") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + // prepare k0sVars + k0sVars := &CfgVars{ + DataDir: "/var/lib/k0s-custom", + RuntimeConfigPath: tmpfile.Name(), + } + + content := []byte(`apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +metadata: + name: k0s +spec: + api: + address: 10.2.3.4 +`) + err = os.WriteFile(k0sVars.RuntimeConfigPath, content, 0644) + assert.NoError(t, err) + + spec, err := LoadRuntimeConfig(k0sVars) + assert.NoError(t, err) + assert.NotNil(t, spec) + assert.Equal(t, "/var/lib/k0s-custom", spec.K0sVars.DataDir) + assert.Equal(t, os.Getpid(), spec.Pid) + assert.NotNil(t, spec.NodeConfig) + assert.NotNil(t, spec.NodeConfig.Spec.API) + assert.Equal(t, "10.2.3.4", spec.NodeConfig.Spec.API.Address) +} + +func TestLoadRuntimeConfig_K0sNotRunning(t *testing.T) { + // create a temporary file for runtime config + tmpfile, err := os.CreateTemp("", "runtime-config") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + // prepare k0sVars + k0sVars := &CfgVars{ + RuntimeConfigPath: tmpfile.Name(), + } + + // write some content to the runtime config file + content := []byte(`--- +apiVersion: k0s.k0sproject.io/v1beta1 +kind: RuntimeConfig +spec: + nodeConfig: + metadata: + name: k0s + pid: 9999999 +`) + err = os.WriteFile(k0sVars.RuntimeConfigPath, content, 0644) + assert.NoError(t, err) + + // try to load runtime config and check if it returns an error + spec, err := LoadRuntimeConfig(k0sVars) + assert.Nil(t, spec) + assert.ErrorIs(t, err, ErrK0sNotRunning) +} + +func TestNewRuntimeConfig(t *testing.T) { + // create a temporary directory for k0s files + tempDir, err := os.MkdirTemp("", "k0s") + assert.NoError(t, err) + defer os.RemoveAll(tempDir) + + // create a temporary file for the runtime config + tmpfile, err := os.CreateTemp("", "runtime-config") + assert.NoError(t, err) + tmpfile.Close() + defer os.Remove(tmpfile.Name()) + + // prepare k0sVars + k0sVars := &CfgVars{ + RuntimeConfigPath: tmpfile.Name(), + DataDir: tempDir, + nodeConfig: &v1beta1.ClusterConfig{ + Spec: &v1beta1.ClusterSpec{ + API: &v1beta1.APISpec{Address: "10.0.0.1"}, + }, + }, + } + + // create a new runtime config and check if it's valid + spec, err := NewRuntimeConfig(k0sVars) + assert.NoError(t, err) + assert.NotNil(t, spec) + assert.Equal(t, tempDir, spec.K0sVars.DataDir) + assert.Equal(t, os.Getpid(), spec.Pid) + assert.NotNil(t, spec.NodeConfig) + cfg, err := spec.K0sVars.NodeConfig() + assert.NoError(t, err) + assert.Equal(t, "10.0.0.1", cfg.Spec.API.Address) + + // try to create a new runtime config when one is already active and check if it returns an error + _, err = NewRuntimeConfig(k0sVars) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrK0sAlreadyRunning) +}