diff --git a/cmd/controller/controller.go b/cmd/controller/controller.go index 6c8deda1b805..5660848bb1ab 100644 --- a/cmd/controller/controller.go +++ b/cmd/controller/controller.go @@ -34,7 +34,6 @@ import ( "github.com/k0sproject/k0s/internal/pkg/dir" "github.com/k0sproject/k0s/internal/pkg/file" k0slog "github.com/k0sproject/k0s/internal/pkg/log" - "github.com/k0sproject/k0s/internal/pkg/stringmap" "github.com/k0sproject/k0s/internal/pkg/sysinfo" "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/k0sproject/k0s/pkg/applier" @@ -99,8 +98,6 @@ func NewControllerCmd() *cobra.Command { return err } - c.Logging = stringmap.Merge(c.CmdLogLevels, c.DefaultLogLevels) - if err := (&sysinfo.K0sSysinfoSpec{ ControllerRoleEnabled: true, WorkerRoleEnabled: c.SingleNode || c.EnableWorker, @@ -210,7 +207,7 @@ func (c *command) start(ctx context.Context) error { Config: nodeConfig.Spec.Storage.Etcd, JoinClient: joinClient, K0sVars: c.K0sVars, - LogLevel: c.Logging["etcd"], + LogLevel: c.LogLevels.Etcd, } default: return fmt.Errorf("invalid storage type: %s", nodeConfig.Spec.Storage.Type) @@ -247,7 +244,7 @@ func (c *command) start(ctx context.Context) error { if enableKonnectivity { nodeComponents.Add(ctx, &controller.Konnectivity{ SingleNode: c.SingleNode, - LogLevel: c.Logging[constant.KonnectivityServerComponentName], + LogLevel: c.LogLevels.Konnectivity, K0sVars: c.K0sVars, KubeClientFactory: adminClientFactory, NodeConfig: nodeConfig, @@ -259,7 +256,7 @@ func (c *command) start(ctx context.Context) error { nodeComponents.Add(ctx, &controller.APIServer{ ClusterConfig: nodeConfig, K0sVars: c.K0sVars, - LogLevel: c.Logging["kube-apiserver"], + LogLevel: c.LogLevels.KubeAPIServer, Storage: storageBackend, EnableKonnectivity: enableKonnectivity, DisableEndpointReconciler: disableEndpointReconciler, @@ -499,7 +496,7 @@ func (c *command) start(ctx context.Context) error { if enableKonnectivity { clusterComponents.Add(ctx, &controller.KonnectivityAgent{ SingleNode: c.SingleNode, - LogLevel: c.Logging[constant.KonnectivityServerComponentName], + LogLevel: c.LogLevels.Konnectivity, K0sVars: c.K0sVars, KubeClientFactory: adminClientFactory, NodeConfig: nodeConfig, @@ -510,7 +507,7 @@ func (c *command) start(ctx context.Context) error { if !slices.Contains(c.DisableComponents, constant.KubeSchedulerComponentName) { clusterComponents.Add(ctx, &controller.Scheduler{ - LogLevel: c.Logging[constant.KubeSchedulerComponentName], + LogLevel: c.LogLevels.KubeScheduler, K0sVars: c.K0sVars, SingleNode: c.SingleNode, }) @@ -518,7 +515,7 @@ func (c *command) start(ctx context.Context) error { if !slices.Contains(c.DisableComponents, constant.KubeControllerManagerComponentName) { clusterComponents.Add(ctx, &controller.Manager{ - LogLevel: c.Logging[constant.KubeControllerManagerComponentName], + LogLevel: c.LogLevels.KubeControllerManager, K0sVars: c.K0sVars, SingleNode: c.SingleNode, ServiceClusterIPRange: nodeConfig.Spec.Network.BuildServiceCIDR(nodeConfig.Spec.API.Address), diff --git a/cmd/controller/controller_test.go b/cmd/controller/controller_test.go index 41b0775e115a..31b761479088 100644 --- a/cmd/controller/controller_test.go +++ b/cmd/controller/controller_test.go @@ -17,65 +17,71 @@ limitations under the License. package controller_test import ( + "strconv" "strings" "testing" "github.com/k0sproject/k0s/cmd" + "github.com/k0sproject/k0s/pkg/constant" + "github.com/stretchr/testify/assert" ) func TestControllerCmd_Help(t *testing.T) { + defaultConfigPath := strconv.Quote(constant.K0sConfigPathDefault) + defaultDataDir := strconv.Quote(constant.DataDirDefault) + var out strings.Builder underTest := cmd.NewRootCmd() underTest.SetArgs([]string{"controller", "--help"}) underTest.SetOut(&out) assert.NoError(t, underTest.Execute()) - assert.Regexp(t, `^Run controller + assert.Equal(t, `Run controller Usage: - k0s controller \[join-token\] \[flags\] + k0s controller [join-token] [flags] Aliases: controller, server Examples: -\tCommand to associate master nodes: -\tCLI argument: -\t\$ k0s controller \[join-token\] + Command to associate master nodes: + CLI argument: + $ k0s controller [join-token] -\tor CLI flag: -\t\$ k0s controller --token-file \[path_to_file\] -\tNote: Token can be passed either as a CLI argument or as a flag + or CLI flag: + $ k0s controller --token-file [path_to_file] + Note: Token can be passed either as a CLI argument or as a flag Flags: - --cidr-range string HACK: cidr range for the windows worker node \(default "10\.96\.0\.0/12"\) - -c, --config string config file, use '-' to read the config from stdin \(default ".+k0s\.yaml"\) - --cri-socket string container runtime socket to use, default to internal containerd\. Format: \[remote\|docker\]:\[path-to-socket\] - --data-dir string Data Directory for k0s\. DO NOT CHANGE for an existing setup, things will break! \(default ".+k0s"\) - -d, --debug Debug logging \(default: false\) - --debugListenOn string Http listenOn for Debug pprof handler \(default ":6060"\) - --disable-components strings disable components \(valid items: applier-manager,autopilot,control-api,coredns,csr-approver,endpoint-reconciler,helm,konnectivity-server,kube-controller-manager,kube-proxy,kube-scheduler,metrics-server,network-provider,node-role,system-rbac,windows-node,worker-config\) + --cidr-range string HACK: cidr range for the windows worker node (default "10.96.0.0/12") + -c, --config string config file, use '-' to read the config from stdin (default `+defaultConfigPath+`) + --cri-socket string container runtime socket to use, default to internal containerd. Format: [remote|docker]:[path-to-socket] + --data-dir string Data Directory for k0s. DO NOT CHANGE for an existing setup, things will break! (default `+defaultDataDir+`) + -d, --debug Debug logging (default: false) + --debugListenOn string Http listenOn for Debug pprof handler (default ":6060") + --disable-components strings disable components (valid items: applier-manager,autopilot,control-api,coredns,csr-approver,endpoint-reconciler,helm,konnectivity-server,kube-controller-manager,kube-proxy,kube-scheduler,metrics-server,network-provider,node-role,system-rbac,windows-node,worker-config) --enable-cloud-provider Whether or not to enable cloud provider support in kubelet --enable-dynamic-config enable cluster-wide dynamic config based on custom resource - --enable-k0s-cloud-provider enables the k0s-cloud-provider \(default false\) - --enable-metrics-scraper enable scraping metrics from the controller components \(kube-scheduler, kube-controller-manager\) - --enable-worker enable worker \(default false\) + --enable-k0s-cloud-provider enables the k0s-cloud-provider (default false) + --enable-metrics-scraper enable scraping metrics from the controller components (kube-scheduler, kube-controller-manager) + --enable-worker enable worker (default false) -h, --help help for controller --ignore-pre-flight-checks continue even if pre-flight checks fail - --iptables-mode string iptables mode \(valid values: nft, legacy, auto\)\. default: auto - --k0s-cloud-provider-port int the port that k0s-cloud-provider binds on \(default 10258\) - --k0s-cloud-provider-update-frequency duration the frequency of k0s-cloud-provider node updates \(default 2m0s\) + --iptables-mode string iptables mode (valid values: nft, legacy, auto). default: auto + --k0s-cloud-provider-port int the port that k0s-cloud-provider binds on (default 10258) + --k0s-cloud-provider-update-frequency duration the frequency of k0s-cloud-provider node updates (default 2m0s) --kube-controller-manager-extra-args string extra args for kube-controller-manager --kubelet-extra-args string extra args for kubelet --labels strings Node labels, list of key=value pairs - -l, --logging stringToString Logging Levels for the different components \(default \[.+]\) + -l, --logging stringToString Logging Levels for the different components (default [containerd=info,etcd=info,konnectivity-server=1,kube-apiserver=1,kube-controller-manager=1,kube-scheduler=1,kubelet=1]) --no-taints disable default taints for controller node - --profile string worker profile to use on the node \(default "default"\) - --single enable single node \(implies --enable-worker, default false\) - --status-socket string Full file path to the socket file\. \(default: /status\.sock\) + --profile string worker profile to use on the node (default "default") + --single enable single node (implies --enable-worker, default false) + --status-socket string Full file path to the socket file. (default: /status.sock) --taints strings Node taints, list of key=value:effect strings - --token-file string Path to the file containing join-token\. - -v, --verbose Verbose logging \(default: false\) -$`, out.String()) + --token-file string Path to the file containing join-token. + -v, --verbose Verbose logging (default: false) +`, out.String()) } diff --git a/cmd/install/controller_test.go b/cmd/install/controller_test.go index 60f0875d9ebc..cf8d1a68c3f2 100644 --- a/cmd/install/controller_test.go +++ b/cmd/install/controller_test.go @@ -17,24 +17,30 @@ limitations under the License. package install_test import ( + "strconv" "strings" "testing" "github.com/k0sproject/k0s/cmd" + "github.com/k0sproject/k0s/pkg/constant" + "github.com/stretchr/testify/assert" ) -func TestInstallCmd_Controller_Help(t *testing.T) { +func TestInstallController_Help(t *testing.T) { + defaultConfigPath := strconv.Quote(constant.K0sConfigPathDefault) + defaultDataDir := strconv.Quote(constant.DataDirDefault) + var out strings.Builder underTest := cmd.NewRootCmd() underTest.SetArgs([]string{"install", "controller", "--help"}) underTest.SetOut(&out) assert.NoError(t, underTest.Execute()) - assert.Regexp(t, `^Install k0s controller on a brand-new system\. Must be run as root \(or with sudo\) + assert.Equal(t, `Install k0s controller on a brand-new system. Must be run as root (or with sudo) Usage: - k0s install controller \[flags\] + k0s install controller [flags] Aliases: controller, server @@ -48,36 +54,36 @@ With the controller subcommand you can setup a single node cluster by running: Flags: - --cidr-range string HACK: cidr range for the windows worker node \(default "10\.96\.0\.0/12"\) - -c, --config string config file, use '-' to read the config from stdin \(default ".+k0s\.yaml"\) - --cri-socket string container runtime socket to use, default to internal containerd\. Format: \[remote\|docker\]:\[path-to-socket\] - --data-dir string Data Directory for k0s\. DO NOT CHANGE for an existing setup, things will break! \(default ".+k0s"\) - -d, --debug Debug logging \(default: false\) - --debugListenOn string Http listenOn for Debug pprof handler \(default ":6060"\) - --disable-components strings disable components \(valid items: applier-manager,autopilot,control-api,coredns,csr-approver,endpoint-reconciler,helm,konnectivity-server,kube-controller-manager,kube-proxy,kube-scheduler,metrics-server,network-provider,node-role,system-rbac,windows-node,worker-config\) + --cidr-range string HACK: cidr range for the windows worker node (default "10.96.0.0/12") + -c, --config string config file, use '-' to read the config from stdin (default `+defaultConfigPath+`) + --cri-socket string container runtime socket to use, default to internal containerd. Format: [remote|docker]:[path-to-socket] + --data-dir string Data Directory for k0s. DO NOT CHANGE for an existing setup, things will break! (default `+defaultDataDir+`) + -d, --debug Debug logging (default: false) + --debugListenOn string Http listenOn for Debug pprof handler (default ":6060") + --disable-components strings disable components (valid items: applier-manager,autopilot,control-api,coredns,csr-approver,endpoint-reconciler,helm,konnectivity-server,kube-controller-manager,kube-proxy,kube-scheduler,metrics-server,network-provider,node-role,system-rbac,windows-node,worker-config) --enable-cloud-provider Whether or not to enable cloud provider support in kubelet --enable-dynamic-config enable cluster-wide dynamic config based on custom resource - --enable-k0s-cloud-provider enables the k0s-cloud-provider \(default false\) - --enable-metrics-scraper enable scraping metrics from the controller components \(kube-scheduler, kube-controller-manager\) - --enable-worker enable worker \(default false\) + --enable-k0s-cloud-provider enables the k0s-cloud-provider (default false) + --enable-metrics-scraper enable scraping metrics from the controller components (kube-scheduler, kube-controller-manager) + --enable-worker enable worker (default false) -h, --help help for controller - --iptables-mode string iptables mode \(valid values: nft, legacy, auto\)\. default: auto - --k0s-cloud-provider-port int the port that k0s-cloud-provider binds on \(default 10258\) - --k0s-cloud-provider-update-frequency duration the frequency of k0s-cloud-provider node updates \(default 2m0s\) + --iptables-mode string iptables mode (valid values: nft, legacy, auto). default: auto + --k0s-cloud-provider-port int the port that k0s-cloud-provider binds on (default 10258) + --k0s-cloud-provider-update-frequency duration the frequency of k0s-cloud-provider node updates (default 2m0s) --kube-controller-manager-extra-args string extra args for kube-controller-manager --kubelet-extra-args string extra args for kubelet --labels strings Node labels, list of key=value pairs - -l, --logging stringToString Logging Levels for the different components \(default \[.+]\) + -l, --logging stringToString Logging Levels for the different components (default [containerd=info,etcd=info,konnectivity-server=1,kube-apiserver=1,kube-controller-manager=1,kube-scheduler=1,kubelet=1]) --no-taints disable default taints for controller node - --profile string worker profile to use on the node \(default "default"\) - --single enable single node \(implies --enable-worker, default false\) - --status-socket string Full file path to the socket file\. \(default: /status\.sock\) + --profile string worker profile to use on the node (default "default") + --single enable single node (implies --enable-worker, default false) + --status-socket string Full file path to the socket file. (default: /status.sock) --taints strings Node taints, list of key=value:effect strings - --token-file string Path to the file containing join-token\. - -v, --verbose Verbose logging \(default: false\) + --token-file string Path to the file containing join-token. + -v, --verbose Verbose logging (default: false) Global Flags: -e, --env stringArray set environment variable --force force init script creation -$`, out.String()) +`, out.String()) } diff --git a/cmd/worker/worker.go b/cmd/worker/worker.go index f5fc9774b96d..1b7f7e47f016 100644 --- a/cmd/worker/worker.go +++ b/cmd/worker/worker.go @@ -26,7 +26,6 @@ import ( "syscall" k0slog "github.com/k0sproject/k0s/internal/pkg/log" - "github.com/k0sproject/k0s/internal/pkg/stringmap" "github.com/k0sproject/k0s/internal/pkg/sysinfo" "github.com/k0sproject/k0s/pkg/build" "github.com/k0sproject/k0s/pkg/component/manager" @@ -74,7 +73,6 @@ func NewWorkerCmd() *cobra.Command { c.TokenArg = args[0] } - c.Logging = stringmap.Merge(c.CmdLogLevels, c.DefaultLogLevels) if c.TokenArg != "" && c.TokenFile != "" { return fmt.Errorf("you can only pass one token argument either as a CLI argument 'k0s worker [token]' or as a flag 'k0s worker --token-file [path]'") } @@ -141,7 +139,7 @@ func (c *Command) Start(ctx context.Context) error { } if c.CriSocket == "" { - componentManager.Add(ctx, containerd.NewComponent(c.Logging["containerd"], c.K0sVars, workerConfig)) + componentManager.Add(ctx, containerd.NewComponent(c.LogLevels.Containerd, c.K0sVars, workerConfig)) } componentManager.Add(ctx, worker.NewOCIBundleReconciler(c.K0sVars)) @@ -156,7 +154,7 @@ func (c *Command) Start(ctx context.Context) error { StaticPods: staticPods, Kubeconfig: kubeletKubeconfigPath, Configuration: *workerConfig.KubeletConfiguration.DeepCopy(), - LogLevel: c.Logging["kubelet"], + LogLevel: c.LogLevels.Kubelet, Labels: c.Labels, Taints: c.Taints, ExtraArgs: c.KubeletExtraArgs, diff --git a/pkg/config/cli.go b/pkg/config/cli.go index 12d0081db8fb..a04d3ec1f2b9 100644 --- a/pkg/config/cli.go +++ b/pkg/config/cli.go @@ -48,13 +48,11 @@ var ( type CLIOptions struct { WorkerOptions ControllerOptions - CfgFile string - Debug bool - DebugListenOn string - DefaultLogLevels map[string]string - K0sVars *CfgVars - Logging map[string]string // merged outcome of default log levels and cmdLoglevels - Verbose bool + CfgFile string + Debug bool + DebugListenOn string + K0sVars *CfgVars + Verbose bool } // Shared controller cli flags @@ -78,7 +76,7 @@ type ControllerOptions struct { type WorkerOptions struct { CIDRRange string CloudProvider bool - CmdLogLevels map[string]string + LogLevels LogLevels CriSocket string KubeletExtraArgs string Labels []string @@ -106,19 +104,96 @@ func (o *ControllerOptions) Normalize() error { return nil } -func DefaultLogLevels() map[string]string { - return map[string]string{ - "etcd": "info", - "containerd": "info", - "konnectivity-server": "1", - "kube-apiserver": "1", - "kube-controller-manager": "1", - "kube-scheduler": "1", - "kubelet": "1", - "kube-proxy": "1", +type LogLevels = struct { + Containerd string + Etcd string + Konnectivity string + KubeAPIServer string + KubeControllerManager string + KubeScheduler string + Kubelet string +} + +func DefaultLogLevels() LogLevels { + return LogLevels{ + Containerd: "info", + Etcd: "info", + Konnectivity: "1", + KubeAPIServer: "1", + KubeControllerManager: "1", + KubeScheduler: "1", + Kubelet: "1", } } +type logLevelsFlag LogLevels + +func (f *logLevelsFlag) Type() string { + return "stringToString" +} + +func (f *logLevelsFlag) Set(val string) error { + val = strings.TrimPrefix(val, "[") + val = strings.TrimSuffix(val, "]") + + parsed := DefaultLogLevels() + + for val != "" { + pair, rest, _ := strings.Cut(val, ",") + val = rest + k, v, ok := strings.Cut(pair, "=") + + if k == "" { + return fmt.Errorf("component name cannot be empty: %q", pair) + } + if !ok { + return fmt.Errorf("must be of format component=level: %q", pair) + } + + switch k { + case "containerd": + parsed.Containerd = v + case "etcd": + parsed.Etcd = v + case "konnectivity-server": + parsed.Konnectivity = v + case "kube-apiserver": + parsed.KubeAPIServer = v + case "kube-controller-manager": + parsed.KubeControllerManager = v + case "kube-scheduler": + parsed.KubeScheduler = v + case "kubelet": + parsed.Kubelet = v + default: + return fmt.Errorf("unknown component name: %q", k) + } + } + + *f = parsed + return nil +} + +func (f *logLevelsFlag) String() string { + var buf strings.Builder + buf.WriteString("[containerd=") + buf.WriteString(f.Containerd) + buf.WriteString(",etcd=") + buf.WriteString(f.Etcd) + buf.WriteString(",konnectivity-server=") + buf.WriteString(f.Konnectivity) + buf.WriteString(",kube-apiserver=") + buf.WriteString(f.KubeAPIServer) + buf.WriteString(",kube-controller-manager=") + buf.WriteString(f.KubeControllerManager) + buf.WriteString(",kube-scheduler=") + buf.WriteString(f.KubeScheduler) + buf.WriteString(",kubelet=") + buf.WriteString(f.Kubelet) + buf.WriteString("]") + return buf.String() +} + func GetPersistentFlagSet() *pflag.FlagSet { flagset := &pflag.FlagSet{} flagset.BoolVarP(&Debug, "debug", "d", false, "Debug logging (default: false)") @@ -152,11 +227,16 @@ func GetCriSocketFlag() *pflag.FlagSet { func GetWorkerFlags() *pflag.FlagSet { flagset := &pflag.FlagSet{} + if workerOpts.LogLevels == (LogLevels{}) { + // initialize zero value with defaults + workerOpts.LogLevels = DefaultLogLevels() + } + flagset.StringVar(&workerOpts.WorkerProfile, "profile", "default", "worker profile to use on the node") flagset.StringVar(&workerOpts.CIDRRange, "cidr-range", "10.96.0.0/12", "HACK: cidr range for the windows worker node") flagset.BoolVar(&workerOpts.CloudProvider, "enable-cloud-provider", false, "Whether or not to enable cloud provider support in kubelet") flagset.StringVar(&workerOpts.TokenFile, "token-file", "", "Path to the file containing join-token.") - flagset.StringToStringVarP(&workerOpts.CmdLogLevels, "logging", "l", DefaultLogLevels(), "Logging Levels for the different components") + flagset.VarP((*logLevelsFlag)(&workerOpts.LogLevels), "logging", "l", "Logging Levels for the different components") flagset.StringSliceVarP(&workerOpts.Labels, "labels", "", []string{}, "Node labels, list of key=value pairs") flagset.StringSliceVarP(&workerOpts.Taints, "taints", "", []string{}, "Node taints, list of key=value:effect strings") flagset.StringVar(&workerOpts.KubeletExtraArgs, "kubelet-extra-args", "", "extra args for kubelet") @@ -233,12 +313,11 @@ func GetCmdOpts(cobraCmd command) (*CLIOptions, error) { ControllerOptions: controllerOpts, WorkerOptions: workerOpts, - CfgFile: CfgFile, - Debug: Debug, - Verbose: Verbose, - DefaultLogLevels: DefaultLogLevels(), - K0sVars: k0sVars, - DebugListenOn: DebugListenOn, + CfgFile: CfgFile, + Debug: Debug, + Verbose: Verbose, + K0sVars: k0sVars, + DebugListenOn: DebugListenOn, }, nil } diff --git a/pkg/config/cli_test.go b/pkg/config/cli_test.go index 8a3ffacc499d..313f5887b960 100644 --- a/pkg/config/cli_test.go +++ b/pkg/config/cli_test.go @@ -55,3 +55,49 @@ func TestControllerOptions_Normalize(t *testing.T) { assert.Equal(t, expected, underTest.DisableComponents) }) } + +func TestLogLevelsFlagSet(t *testing.T) { + t.Run("full_input", func(t *testing.T) { + var underTest logLevelsFlag + assert.NoError(t, underTest.Set("kubelet=a,kube-scheduler=b,kube-controller-manager=c,kube-apiserver=d,konnectivity-server=e,etcd=f,containerd=g")) + assert.Equal(t, logLevelsFlag{ + Containerd: "g", + Etcd: "f", + Konnectivity: "e", + KubeAPIServer: "d", + KubeControllerManager: "c", + KubeScheduler: "b", + Kubelet: "a", + }, underTest) + assert.Equal(t, "[containerd=g,etcd=f,konnectivity-server=e,kube-apiserver=d,kube-controller-manager=c,kube-scheduler=b,kubelet=a]", underTest.String()) + }) + + t.Run("partial_input", func(t *testing.T) { + var underTest logLevelsFlag + assert.NoError(t, underTest.Set("[kubelet=a,etcd=b]")) + assert.Equal(t, logLevelsFlag{ + Containerd: "info", + Etcd: "b", + Konnectivity: "1", + KubeAPIServer: "1", + KubeControllerManager: "1", + KubeScheduler: "1", + Kubelet: "a", + }, underTest) + }) + + for _, test := range []struct { + name, input, msg string + }{ + {"unknown_component", "random=debug", `unknown component name: "random"`}, + {"empty_component_name", "=info", "component name cannot be empty"}, + {"unknown_component_only", "random", `must be of format component=level: "random"`}, + {"no_equals", "kube-apiserver", `must be of format component=level: "kube-apiserver"`}, + {"mixed_valid_and_invalid", "containerd=info,random=debug", `unknown component name: "random"`}, + } { + t.Run(test.name, func(t *testing.T) { + var underTest logLevelsFlag + assert.ErrorContains(t, underTest.Set(test.input), test.msg) + }) + } +}