From 6fce553a693ef1aa0f5f0a649218c4f584533461 Mon Sep 17 00:00:00 2001 From: Nour Date: Sat, 27 Dec 2025 20:42:43 +0200 Subject: [PATCH 1/4] Implements a new generate-config subcommand that auto-generates example configuration files for k3s server and agent nodes Signed-off-by: Nour --- cmd/k3s/main.go | 2 + main.go | 2 + pkg/cli/cmds/agent.go | 150 +++++----- pkg/cli/cmds/generate_config.go | 48 ++++ pkg/cli/generateconfig/generate_config.go | 326 ++++++++++++++++++++++ 5 files changed, 454 insertions(+), 74 deletions(-) create mode 100644 pkg/cli/cmds/generate_config.go create mode 100644 pkg/cli/generateconfig/generate_config.go diff --git a/cmd/k3s/main.go b/cmd/k3s/main.go index 098b9a47dfc5..06278db1fa48 100644 --- a/cmd/k3s/main.go +++ b/cmd/k3s/main.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/k3s-io/k3s/pkg/cli/generateconfig" "github.com/k3s-io/k3s/pkg/configfilearg" "github.com/k3s-io/k3s/pkg/data" "github.com/k3s-io/k3s/pkg/datadir" @@ -91,6 +92,7 @@ func main() { internalCLIAction(version.Program+"-completion", dataDir, os.Args), internalCLIAction(version.Program+"-completion", dataDir, os.Args), ), + cmds.NewGenerateConfigCommand(generateconfig.Run), } if err := app.Run(os.Args); err != nil && !errors.Is(err, context.Canceled) { diff --git a/main.go b/main.go index 12123ec20280..827d80858e76 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/k3s-io/k3s/pkg/cli/completion" "github.com/k3s-io/k3s/pkg/cli/crictl" "github.com/k3s-io/k3s/pkg/cli/etcdsnapshot" + "github.com/k3s-io/k3s/pkg/cli/generateconfig" "github.com/k3s-io/k3s/pkg/cli/kubectl" "github.com/k3s-io/k3s/pkg/cli/secretsencrypt" "github.com/k3s-io/k3s/pkg/cli/server" @@ -53,6 +54,7 @@ func main() { completion.Bash, completion.Zsh, ), + cmds.NewGenerateConfigCommand(generateconfig.Run), } if err := app.Run(configfilearg.MustParse(os.Args)); err != nil && !errors.Is(err, context.Canceled) { diff --git a/pkg/cli/cmds/agent.go b/pkg/cli/cmds/agent.go index 916444945dbf..75404c86dcd5 100644 --- a/pkg/cli/cmds/agent.go +++ b/pkg/cli/cmds/agent.go @@ -259,85 +259,87 @@ var ( } ) +var AgentFlags = []cli.Flag{ + ConfigFlag, + DebugFlag, + VLevel, + VModule, + LogFile, + AlsoLogToStderr, + AgentTokenFlag, + &cli.StringFlag{ + Name: "token-file", + Usage: "(cluster) Token file to use for authentication", + EnvVars: []string{version.ProgramUpper + "_TOKEN_FILE"}, + Destination: &AgentConfig.TokenFile, + }, + &cli.StringFlag{ + Name: "server", + Aliases: []string{"s"}, + Usage: "(cluster) Server to connect to", + EnvVars: []string{version.ProgramUpper + "_URL"}, + Destination: &AgentConfig.ServerURL, + }, + // Note that this is different from DataDirFlag used elswhere in the CLI, + // as this is bound to AgentConfig instead of ServerConfig. + &cli.StringFlag{ + Name: "data-dir", + Aliases: []string{"d"}, + Usage: "(agent/data) Folder to hold state", + Destination: &AgentConfig.DataDir, + Value: "/var/lib/rancher/" + version.Program + "", + EnvVars: []string{version.ProgramUpper + "_DATA_DIR"}, + }, + NodeNameFlag, + WithNodeIDFlag, + NodeLabels, + NodeTaints, + ImageCredProvBinDirFlag, + ImageCredProvConfigFlag, + SELinuxFlag, + LBServerPortFlag, + ProtectKernelDefaultsFlag, + CRIEndpointFlag, + DefaultRuntimeFlag, + ImageServiceEndpointFlag, + PauseImageFlag, + SnapshotterFlag, + PrivateRegistryFlag, + DisableDefaultRegistryEndpointFlag, + NonrootDevicesFlag, + AirgapExtraRegistryFlag, + NodeIPFlag, + BindAddressFlag, + NodeExternalIPFlag, + NodeInternalDNSFlag, + NodeExternalDNSFlag, + ResolvConfFlag, + FlannelIfaceFlag, + FlannelConfFlag, + FlannelCniConfFileFlag, + ExtraKubeletArgs, + ExtraKubeProxyArgs, + // Experimental flags + EnablePProfFlag, + &cli.BoolFlag{ + Name: "rootless", + Usage: "(experimental) Run rootless", + Destination: &AgentConfig.Rootless, + }, + PreferBundledBin, + // Deprecated/hidden below + DockerFlag, + VPNAuth, + VPNAuthFile, + DisableAgentLBFlag, +} + func NewAgentCommand(action func(ctx *cli.Context) error) *cli.Command { return &cli.Command{ Name: "agent", Usage: "Run node agent", UsageText: appName + " agent [OPTIONS]", Action: action, - Flags: []cli.Flag{ - ConfigFlag, - DebugFlag, - VLevel, - VModule, - LogFile, - AlsoLogToStderr, - AgentTokenFlag, - &cli.StringFlag{ - Name: "token-file", - Usage: "(cluster) Token file to use for authentication", - EnvVars: []string{version.ProgramUpper + "_TOKEN_FILE"}, - Destination: &AgentConfig.TokenFile, - }, - &cli.StringFlag{ - Name: "server", - Aliases: []string{"s"}, - Usage: "(cluster) Server to connect to", - EnvVars: []string{version.ProgramUpper + "_URL"}, - Destination: &AgentConfig.ServerURL, - }, - // Note that this is different from DataDirFlag used elswhere in the CLI, - // as this is bound to AgentConfig instead of ServerConfig. - &cli.StringFlag{ - Name: "data-dir", - Aliases: []string{"d"}, - Usage: "(agent/data) Folder to hold state", - Destination: &AgentConfig.DataDir, - Value: "/var/lib/rancher/" + version.Program + "", - EnvVars: []string{version.ProgramUpper + "_DATA_DIR"}, - }, - NodeNameFlag, - WithNodeIDFlag, - NodeLabels, - NodeTaints, - ImageCredProvBinDirFlag, - ImageCredProvConfigFlag, - SELinuxFlag, - LBServerPortFlag, - ProtectKernelDefaultsFlag, - CRIEndpointFlag, - DefaultRuntimeFlag, - ImageServiceEndpointFlag, - PauseImageFlag, - SnapshotterFlag, - PrivateRegistryFlag, - DisableDefaultRegistryEndpointFlag, - NonrootDevicesFlag, - AirgapExtraRegistryFlag, - NodeIPFlag, - BindAddressFlag, - NodeExternalIPFlag, - NodeInternalDNSFlag, - NodeExternalDNSFlag, - ResolvConfFlag, - FlannelIfaceFlag, - FlannelConfFlag, - FlannelCniConfFileFlag, - ExtraKubeletArgs, - ExtraKubeProxyArgs, - // Experimental flags - EnablePProfFlag, - &cli.BoolFlag{ - Name: "rootless", - Usage: "(experimental) Run rootless", - Destination: &AgentConfig.Rootless, - }, - PreferBundledBin, - // Deprecated/hidden below - DockerFlag, - VPNAuth, - VPNAuthFile, - DisableAgentLBFlag, - }, + Flags: AgentFlags, } } diff --git a/pkg/cli/cmds/generate_config.go b/pkg/cli/cmds/generate_config.go new file mode 100644 index 000000000000..d9fd0016e8d9 --- /dev/null +++ b/pkg/cli/cmds/generate_config.go @@ -0,0 +1,48 @@ +package cmds + +import ( + "github.com/urfave/cli/v2" +) + +const GenerateConfigCommand = "generate-config" + +type GenerateConfig struct { + Output string + ConfigType string + FromConfig string +} + +var ( + GenerateConfigConfig = GenerateConfig{} + GenerateConfigFlags = []cli.Flag{ + DebugFlag, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Output file path (default: stdout)", + Destination: &GenerateConfigConfig.Output, + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "Config type to generate (server or agent)", + Value: "server", + Destination: &GenerateConfigConfig.ConfigType, + }, + &cli.StringFlag{ + Name: "from-config", + Usage: "Read existing config file to show current values", + Destination: &GenerateConfigConfig.FromConfig, + }, + } +) + +func NewGenerateConfigCommand(action func(*cli.Context) error) *cli.Command { + return &cli.Command{ + Name: GenerateConfigCommand, + Usage: "Generate an example k3s config file", + SkipFlagParsing: false, + Action: action, + Flags: GenerateConfigFlags, + } +} diff --git a/pkg/cli/generateconfig/generate_config.go b/pkg/cli/generateconfig/generate_config.go new file mode 100644 index 000000000000..340739cf83b7 --- /dev/null +++ b/pkg/cli/generateconfig/generate_config.go @@ -0,0 +1,326 @@ +package generateconfig + +import ( + "fmt" + "io" + "os" + "reflect" + "strings" + + "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" +) + +// Run generates an example k3s config file +func Run(ctx *cli.Context) error { + configType := cmds.GenerateConfigConfig.ConfigType + output := cmds.GenerateConfigConfig.Output + + if configType != "server" && configType != "agent" { + return fmt.Errorf("invalid config type %q, must be 'server' or 'agent'", configType) + } + + var flags []cli.Flag + var existingConfig interface{} + + if configType == "server" { + flags = cmds.ServerFlags + existingConfig = &cmds.ServerConfig + } else { + flags = cmds.AgentFlags + existingConfig = &cmds.AgentConfig + } + + var currentValues map[string]interface{} + if configFile := cmds.GenerateConfigConfig.FromConfig; configFile != "" { + if data, err := os.ReadFile(configFile); err == nil { + currentValues = make(map[string]interface{}) + _ = yaml.Unmarshal(data, ¤tValues) + } + } + + yamlContent := generateYAMLWithComments(flags, existingConfig, currentValues, configType) + + var writer io.Writer = os.Stdout + if output != "" { + file, err := os.Create(output) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer file.Close() + writer = file + } + + _, err := writer.Write([]byte(yamlContent)) + return err +} + +// generateYAMLWithComments creates a YAML string with comments +func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, currentValues map[string]interface{}, configType string) string { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("# Example k3s %s configuration file\n", configType)) + sb.WriteString("#\n") + sb.WriteString("# This file contains all available configuration options with their descriptions.\n") + sb.WriteString("# Uncomment and modify the options you want to use.\n") + sb.WriteString("#\n") + sb.WriteString(fmt.Sprintf("# Place this file at /etc/rancher/k3s/config.yaml or use --config to specify a different location.\n")) + sb.WriteString("#\n\n") + + categories := groupFlagsByCategory(flags) + + for _, category := range []string{"listener", "cluster", "client", "data", "networking", "agent/node", "agent/networking", "agent/runtime", "flags", "db", "secrets-encryption", "experimental", "components", "other"} { + categoryFlags := categories[category] + if len(categoryFlags) == 0 { + continue + } + + categoryName := strings.Title(strings.ReplaceAll(category, "/", " - ")) + if category == "other" { + categoryName = "Other Options" + } + sb.WriteString(fmt.Sprintf("# %s\n", strings.Repeat("=", 80))) + sb.WriteString(fmt.Sprintf("# %s\n", categoryName)) + sb.WriteString(fmt.Sprintf("# %s\n\n", strings.Repeat("=", 80))) + + for _, flag := range categoryFlags { + writeFlag(&sb, flag, existingConfig, currentValues) + } + + sb.WriteString("\n") + } + + return sb.String() +} + +// groupFlagsByCategory organizes flags into categories based on their usage text +func groupFlagsByCategory(flags []cli.Flag) map[string][]cli.Flag { + categories := make(map[string][]cli.Flag) + + for _, flag := range flags { + if isHiddenFlag(flag) { + continue + } + + name := getFlagName(flag) + if name == "config" || name == "debug" || name == "v" || name == "vmodule" || name == "log" || name == "alsologtostderr" { + continue + } + + usage := getFlagUsage(flag) + category := extractCategory(usage) + categories[category] = append(categories[category], flag) + } + + return categories +} + +// extractCategory extracts the category from usage text +func extractCategory(usage string) string { + if idx := strings.Index(usage, "("); idx != -1 { + if endIdx := strings.Index(usage[idx:], ")"); endIdx != -1 { + return usage[idx+1 : idx+endIdx] + } + } + return "other" +} + +// writeFlag writes a single flag to the string builder with comments +func writeFlag(sb *strings.Builder, flag cli.Flag, existingConfig interface{}, currentValues map[string]interface{}) { + name := getFlagName(flag) + usage := getFlagUsage(flag) + + if idx := strings.Index(usage, ") "); idx != -1 { + usage = usage[idx+2:] + } + + sb.WriteString(fmt.Sprintf("# %s\n", usage)) + + value := getFlagValue(flag, name, existingConfig, currentValues) + yamlValue := formatYAMLValue(value, flag) + sb.WriteString(fmt.Sprintf("# %s: %s\n\n", name, yamlValue)) +} + +// getFlagName extracts the name from a flag +func getFlagName(flag cli.Flag) string { + v := reflect.ValueOf(flag) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + nameField := v.FieldByName("Name") + if nameField.IsValid() { + return nameField.String() + } + return "" +} + +// getFlagUsage extracts the usage text from a flag +func getFlagUsage(flag cli.Flag) string { + v := reflect.ValueOf(flag) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + usageField := v.FieldByName("Usage") + if usageField.IsValid() { + return usageField.String() + } + return "" +} + +// isHiddenFlag checks if a flag is hidden +func isHiddenFlag(flag cli.Flag) bool { + v := reflect.ValueOf(flag) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + hiddenField := v.FieldByName("Hidden") + if hiddenField.IsValid() && hiddenField.Kind() == reflect.Bool { + return hiddenField.Bool() + } + return false +} + +// getFlagValue gets the current or default value for a flag +func getFlagValue(flag cli.Flag, name string, existingConfig interface{}, currentValues map[string]interface{}) interface{} { + if currentValues != nil { + if val, exists := currentValues[name]; exists { + return val + } + } + + if existingConfig != nil { + v := reflect.ValueOf(existingConfig) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + destField := getDestinationField(flag) + if destField.IsValid() && !destField.IsNil() { + destValue := destField.Elem() + if !isZeroValue(destValue) { + return destValue.Interface() + } + } + } + + return getDefaultValue(flag) +} + +// getDestinationField gets the Destination field value from a flag +func getDestinationField(flag cli.Flag) reflect.Value { + v := reflect.ValueOf(flag) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + destField := v.FieldByName("Destination") + if destField.IsValid() { + return destField + } + return reflect.Value{} +} + +// isZeroValue checks if a value is the zero value for its type +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice: + return v.Len() == 0 + case reflect.String: + return v.String() == "" + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +// getDefaultValue extracts the default value from a flag +func getDefaultValue(flag cli.Flag) interface{} { + v := reflect.ValueOf(flag) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + valueField := v.FieldByName("Value") + if valueField.IsValid() && !isZeroValue(valueField) { + return valueField.Interface() + } + + switch flag.(type) { + case *cli.StringSliceFlag: + return []string{} + case *cli.IntSliceFlag: + return []int{} + case *cli.BoolFlag: + return false + case *cli.IntFlag: + return 0 + case *cli.Int64Flag: + return int64(0) + case *cli.DurationFlag: + return "" + case *cli.StringFlag: + return "" + default: + return nil + } +} + +// formatYAMLValue formats a value for YAML output +func formatYAMLValue(value interface{}, flag cli.Flag) string { + if value == nil { + return "\"\"" + } + + switch v := value.(type) { + case string: + if v == "" { + return "\"\"" + } + if strings.Contains(v, " ") || strings.Contains(v, ":") || strings.Contains(v, "#") { + return fmt.Sprintf("%q", v) + } + return v + case bool: + return fmt.Sprintf("%t", v) + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return fmt.Sprintf("%v", v) + case []string: + if len(v) == 0 { + return "[]" + } + parts := make([]string, len(v)) + for i, s := range v { + if strings.Contains(s, " ") || strings.Contains(s, ":") { + parts[i] = fmt.Sprintf("%q", s) + } else { + parts[i] = s + } + } + return "[" + strings.Join(parts, ", ") + "]" + case cli.StringSlice: + return formatYAMLValue(v.Value(), flag) + case []int: + if len(v) == 0 { + return "[]" + } + parts := make([]string, len(v)) + for i, n := range v { + parts[i] = fmt.Sprintf("%d", n) + } + return "[" + strings.Join(parts, ", ") + "]" + default: + data, err := yaml.Marshal(value) + if err != nil { + return fmt.Sprintf("%v", value) + } + return strings.TrimSpace(string(data)) + } +} From 261c97de639c4466d7448da751a78d941ffc2ef6 Mon Sep 17 00:00:00 2001 From: Nour Date: Thu, 15 Jan 2026 01:40:25 +0200 Subject: [PATCH 2/4] use template for header, fmt.Fprintf for stringbuilder, and cli interfaces for flag helpers Signed-off-by: Nour --- pkg/cli/generateconfig/generate_config.go | 79 ++++++++++++----------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/pkg/cli/generateconfig/generate_config.go b/pkg/cli/generateconfig/generate_config.go index 340739cf83b7..0efb0ad7f142 100644 --- a/pkg/cli/generateconfig/generate_config.go +++ b/pkg/cli/generateconfig/generate_config.go @@ -6,12 +6,28 @@ import ( "os" "reflect" "strings" + "text/template" "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/k3s-io/k3s/pkg/version" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" ) +const yamlHeader = `{{- /* */ -}} +# Example {{ .Program }} {{ .Command }} configuration file +# This file contains all available configuration options with their descriptions. +# Uncomment and modify the options you want to use. +# Place this file at /etc/rancher/{{ .Program }}/config.yaml or use --config to specify a different location. +# + +` + +type cmdInfo struct { + Program string + Command string +} + // Run generates an example k3s config file func Run(ctx *cli.Context) error { configType := cmds.GenerateConfigConfig.ConfigType @@ -40,7 +56,10 @@ func Run(ctx *cli.Context) error { } } - yamlContent := generateYAMLWithComments(flags, existingConfig, currentValues, configType) + yamlContent, err := generateYAMLWithComments(flags, existingConfig, currentValues, configType) + if err != nil { + return err + } var writer io.Writer = os.Stdout if output != "" { @@ -52,21 +71,21 @@ func Run(ctx *cli.Context) error { writer = file } - _, err := writer.Write([]byte(yamlContent)) + _, err = writer.Write([]byte(yamlContent)) return err } // generateYAMLWithComments creates a YAML string with comments -func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, currentValues map[string]interface{}, configType string) string { +func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, currentValues map[string]interface{}, configType string) (string, error) { var sb strings.Builder - sb.WriteString(fmt.Sprintf("# Example k3s %s configuration file\n", configType)) - sb.WriteString("#\n") - sb.WriteString("# This file contains all available configuration options with their descriptions.\n") - sb.WriteString("# Uncomment and modify the options you want to use.\n") - sb.WriteString("#\n") - sb.WriteString(fmt.Sprintf("# Place this file at /etc/rancher/k3s/config.yaml or use --config to specify a different location.\n")) - sb.WriteString("#\n\n") + tmpl, err := template.New("config").Parse(yamlHeader) + if err != nil { + return "", err + } + if err = tmpl.Execute(&sb, cmdInfo{Program: version.Program, Command: configType}); err != nil { + return "", err + } categories := groupFlagsByCategory(flags) @@ -80,9 +99,9 @@ func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, curr if category == "other" { categoryName = "Other Options" } - sb.WriteString(fmt.Sprintf("# %s\n", strings.Repeat("=", 80))) - sb.WriteString(fmt.Sprintf("# %s\n", categoryName)) - sb.WriteString(fmt.Sprintf("# %s\n\n", strings.Repeat("=", 80))) + fmt.Fprintf(&sb, "# %s\n", strings.Repeat("=", 80)) + fmt.Fprintf(&sb, "# %s\n", categoryName) + fmt.Fprintf(&sb, "# %s\n\n", strings.Repeat("=", 80)) for _, flag := range categoryFlags { writeFlag(&sb, flag, existingConfig, currentValues) @@ -91,7 +110,7 @@ func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, curr sb.WriteString("\n") } - return sb.String() + return sb.String(), nil } // groupFlagsByCategory organizes flags into categories based on their usage text @@ -135,48 +154,34 @@ func writeFlag(sb *strings.Builder, flag cli.Flag, existingConfig interface{}, c usage = usage[idx+2:] } - sb.WriteString(fmt.Sprintf("# %s\n", usage)) + fmt.Fprintf(sb, "# %s\n", usage) value := getFlagValue(flag, name, existingConfig, currentValues) yamlValue := formatYAMLValue(value, flag) - sb.WriteString(fmt.Sprintf("# %s: %s\n\n", name, yamlValue)) + fmt.Fprintf(sb, "# %s: %s\n\n", name, yamlValue) } // getFlagName extracts the name from a flag func getFlagName(flag cli.Flag) string { - v := reflect.ValueOf(flag) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - nameField := v.FieldByName("Name") - if nameField.IsValid() { - return nameField.String() + names := flag.Names() + if len(names) > 0 { + return names[0] } return "" } // getFlagUsage extracts the usage text from a flag func getFlagUsage(flag cli.Flag) string { - v := reflect.ValueOf(flag) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - usageField := v.FieldByName("Usage") - if usageField.IsValid() { - return usageField.String() + if df, ok := flag.(cli.DocGenerationFlag); ok { + return df.GetUsage() } return "" } // isHiddenFlag checks if a flag is hidden func isHiddenFlag(flag cli.Flag) bool { - v := reflect.ValueOf(flag) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - hiddenField := v.FieldByName("Hidden") - if hiddenField.IsValid() && hiddenField.Kind() == reflect.Bool { - return hiddenField.Bool() + if vf, ok := flag.(cli.VisibleFlag); ok { + return !vf.IsVisible() } return false } From a264c81ff7effceffcedfa82e20664136c57cb03 Mon Sep 17 00:00:00 2001 From: Nour Date: Wed, 28 Jan 2026 19:39:26 +0200 Subject: [PATCH 3/4] Remove reflect usage, use cli interfaces and type assertions for flag helpers Signed-off-by: Nour --- pkg/cli/generateconfig/generate_config.go | 116 ++++++---------------- 1 file changed, 32 insertions(+), 84 deletions(-) diff --git a/pkg/cli/generateconfig/generate_config.go b/pkg/cli/generateconfig/generate_config.go index 0efb0ad7f142..4ec4e2c575b0 100644 --- a/pkg/cli/generateconfig/generate_config.go +++ b/pkg/cli/generateconfig/generate_config.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "os" - "reflect" "strings" "text/template" @@ -38,14 +37,10 @@ func Run(ctx *cli.Context) error { } var flags []cli.Flag - var existingConfig interface{} - if configType == "server" { flags = cmds.ServerFlags - existingConfig = &cmds.ServerConfig } else { flags = cmds.AgentFlags - existingConfig = &cmds.AgentConfig } var currentValues map[string]interface{} @@ -56,7 +51,7 @@ func Run(ctx *cli.Context) error { } } - yamlContent, err := generateYAMLWithComments(flags, existingConfig, currentValues, configType) + yamlContent, err := generateYAMLWithComments(flags, currentValues, configType) if err != nil { return err } @@ -76,7 +71,7 @@ func Run(ctx *cli.Context) error { } // generateYAMLWithComments creates a YAML string with comments -func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, currentValues map[string]interface{}, configType string) (string, error) { +func generateYAMLWithComments(flags []cli.Flag, currentValues map[string]interface{}, configType string) (string, error) { var sb strings.Builder tmpl, err := template.New("config").Parse(yamlHeader) @@ -104,7 +99,7 @@ func generateYAMLWithComments(flags []cli.Flag, existingConfig interface{}, curr fmt.Fprintf(&sb, "# %s\n\n", strings.Repeat("=", 80)) for _, flag := range categoryFlags { - writeFlag(&sb, flag, existingConfig, currentValues) + writeFlag(&sb, flag, currentValues) } sb.WriteString("\n") @@ -122,8 +117,8 @@ func groupFlagsByCategory(flags []cli.Flag) map[string][]cli.Flag { continue } - name := getFlagName(flag) - if name == "config" || name == "debug" || name == "v" || name == "vmodule" || name == "log" || name == "alsologtostderr" { + switch getFlagName(flag) { + case "config", "debug", "v", "vmodule", "log", "alsologtostderr": continue } @@ -146,7 +141,7 @@ func extractCategory(usage string) string { } // writeFlag writes a single flag to the string builder with comments -func writeFlag(sb *strings.Builder, flag cli.Flag, existingConfig interface{}, currentValues map[string]interface{}) { +func writeFlag(sb *strings.Builder, flag cli.Flag, currentValues map[string]interface{}) { name := getFlagName(flag) usage := getFlagUsage(flag) @@ -156,7 +151,7 @@ func writeFlag(sb *strings.Builder, flag cli.Flag, existingConfig interface{}, c fmt.Fprintf(sb, "# %s\n", usage) - value := getFlagValue(flag, name, existingConfig, currentValues) + value := getFlagValue(flag, name, currentValues) yamlValue := formatYAMLValue(value, flag) fmt.Fprintf(sb, "# %s: %s\n\n", name, yamlValue) } @@ -187,92 +182,45 @@ func isHiddenFlag(flag cli.Flag) bool { } // getFlagValue gets the current or default value for a flag -func getFlagValue(flag cli.Flag, name string, existingConfig interface{}, currentValues map[string]interface{}) interface{} { +func getFlagValue(flag cli.Flag, name string, currentValues map[string]interface{}) interface{} { if currentValues != nil { if val, exists := currentValues[name]; exists { return val } } - if existingConfig != nil { - v := reflect.ValueOf(existingConfig) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - destField := getDestinationField(flag) - if destField.IsValid() && !destField.IsNil() { - destValue := destField.Elem() - if !isZeroValue(destValue) { - return destValue.Interface() - } - } - } - return getDefaultValue(flag) } -// getDestinationField gets the Destination field value from a flag -func getDestinationField(flag cli.Flag) reflect.Value { - v := reflect.ValueOf(flag) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - destField := v.FieldByName("Destination") - if destField.IsValid() { - return destField - } - return reflect.Value{} -} - -// isZeroValue checks if a value is the zero value for its type -func isZeroValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice: - return v.Len() == 0 - case reflect.String: - return v.String() == "" - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Ptr: - return v.IsNil() - } - return false -} - -// getDefaultValue extracts the default value from a flag +// getDefaultValue extracts the default value from a flag using type assertions func getDefaultValue(flag cli.Flag) interface{} { - v := reflect.ValueOf(flag) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - valueField := v.FieldByName("Value") - if valueField.IsValid() && !isZeroValue(valueField) { - return valueField.Interface() - } - - switch flag.(type) { - case *cli.StringSliceFlag: - return []string{} - case *cli.IntSliceFlag: - return []int{} + switch f := flag.(type) { + case *cli.StringFlag: + return f.Value case *cli.BoolFlag: - return false + return f.Value case *cli.IntFlag: - return 0 + return f.Value case *cli.Int64Flag: - return int64(0) + return f.Value + case *cli.UintFlag: + return f.Value + case *cli.Uint64Flag: + return f.Value + case *cli.Float64Flag: + return f.Value case *cli.DurationFlag: - return "" - case *cli.StringFlag: - return "" + return f.Value.String() + case *cli.StringSliceFlag: + if f.Value != nil { + return f.Value.Value() + } + return []string{} + case *cli.IntSliceFlag: + if f.Value != nil { + return f.Value.Value() + } + return []int{} default: return nil } From edf8b27abaf61320cdedef63e743aaa8ca17749d Mon Sep 17 00:00:00 2001 From: Nour Date: Thu, 29 Jan 2026 13:45:05 +0200 Subject: [PATCH 4/4] Replace interface{} with any per linter recommendation Signed-off-by: Nour --- pkg/cli/generateconfig/generate_config.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cli/generateconfig/generate_config.go b/pkg/cli/generateconfig/generate_config.go index 4ec4e2c575b0..8c7dfd2504ef 100644 --- a/pkg/cli/generateconfig/generate_config.go +++ b/pkg/cli/generateconfig/generate_config.go @@ -43,10 +43,10 @@ func Run(ctx *cli.Context) error { flags = cmds.AgentFlags } - var currentValues map[string]interface{} + var currentValues map[string]any if configFile := cmds.GenerateConfigConfig.FromConfig; configFile != "" { if data, err := os.ReadFile(configFile); err == nil { - currentValues = make(map[string]interface{}) + currentValues = make(map[string]any) _ = yaml.Unmarshal(data, ¤tValues) } } @@ -71,7 +71,7 @@ func Run(ctx *cli.Context) error { } // generateYAMLWithComments creates a YAML string with comments -func generateYAMLWithComments(flags []cli.Flag, currentValues map[string]interface{}, configType string) (string, error) { +func generateYAMLWithComments(flags []cli.Flag, currentValues map[string]any, configType string) (string, error) { var sb strings.Builder tmpl, err := template.New("config").Parse(yamlHeader) @@ -141,7 +141,7 @@ func extractCategory(usage string) string { } // writeFlag writes a single flag to the string builder with comments -func writeFlag(sb *strings.Builder, flag cli.Flag, currentValues map[string]interface{}) { +func writeFlag(sb *strings.Builder, flag cli.Flag, currentValues map[string]any) { name := getFlagName(flag) usage := getFlagUsage(flag) @@ -182,7 +182,7 @@ func isHiddenFlag(flag cli.Flag) bool { } // getFlagValue gets the current or default value for a flag -func getFlagValue(flag cli.Flag, name string, currentValues map[string]interface{}) interface{} { +func getFlagValue(flag cli.Flag, name string, currentValues map[string]any) any { if currentValues != nil { if val, exists := currentValues[name]; exists { return val @@ -193,7 +193,7 @@ func getFlagValue(flag cli.Flag, name string, currentValues map[string]interface } // getDefaultValue extracts the default value from a flag using type assertions -func getDefaultValue(flag cli.Flag) interface{} { +func getDefaultValue(flag cli.Flag) any { switch f := flag.(type) { case *cli.StringFlag: return f.Value @@ -227,7 +227,7 @@ func getDefaultValue(flag cli.Flag) interface{} { } // formatYAMLValue formats a value for YAML output -func formatYAMLValue(value interface{}, flag cli.Flag) string { +func formatYAMLValue(value any, flag cli.Flag) string { if value == nil { return "\"\"" }