From 395be85cc21df783552fea127cf14afc29216c06 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Thu, 30 Oct 2025 17:13:09 +0000 Subject: [PATCH 01/13] feat: initial implementation of config file support --- cli/cli.go | 57 +++++++++++++++++-------- cli/config_loader.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 cli/config_loader.go diff --git a/cli/cli.go b/cli/cli.go index 1b8ef32..caf5074 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -27,6 +27,7 @@ type Config struct { AllowStrings []string LogLevel string LogDir string + ConfigPath string ProxyPort int64 PprofEnabled bool PprofPort int64 @@ -63,6 +64,12 @@ func BaseCommand() *serpent.Command { Short: "Network isolation tool for monitoring and restricting HTTP/HTTPS requests", Long: `boundary creates an isolated network environment for target processes, intercepting HTTP/HTTPS traffic through a transparent proxy that enforces user-defined allow rules.`, Options: []serpent.Option{ + { + Flag: "config", + Env: "BOUNDARY_CONFIG", + Description: "Path to YAML config file.", + Value: serpent.StringOf(&config.ConfigPath), + }, { Flag: "allow", Env: "BOUNDARY_ALLOW", @@ -104,8 +111,8 @@ func BaseCommand() *serpent.Command { }, }, Handler: func(inv *serpent.Invocation) error { - args := inv.Args - return Run(inv.Context(), config, args) + args := inv.Args + return Run(inv.Context(), config, args) }, } } @@ -158,13 +165,27 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("no command specified") } - // Parse allow list; default to deny-all if none provided - if len(config.AllowStrings) == 0 { - logger.Warn("No allow rules specified; all network traffic will be denied by default") - } - - // Parse allow rules - allowRules, err := rulesengine.ParseAllowSpecs(config.AllowStrings) + // Load file config and merge (CLI overrides file), enforce allow exclusivity + fileCfg, filePath, err := loadConfigFile(config.ConfigPath) + if err != nil { + logger.Error("Failed to load config file", "error", err) + return err + } + if filePath != "" { + logger.Debug("Loaded config file", "path", filePath) + } + mergedCfg, allowList, err := mergeConfig(fileCfg, config) + if err != nil { + logger.Error("Configuration error", "error", err) + return err + } + // Parse allow list; default to deny-all if none provided + if len(allowList) == 0 { + logger.Warn("No allow rules specified; all network traffic will be denied by default") + } + + // Parse allow rules + allowRules, err := rulesengine.ParseAllowSpecs(allowList) if err != nil { logger.Error("Failed to parse allow rules", "error", err) return fmt.Errorf("failed to parse allow rules: %v", err) @@ -176,7 +197,7 @@ func Run(ctx context.Context, config Config, args []string) error { // Create auditor auditor := audit.NewLogAuditor(logger) - // Create TLS certificate manager + // Create TLS certificate manager certManager, err := tls.NewCertificateManager(tls.Config{ Logger: logger, ConfigDir: configDir, @@ -194,10 +215,10 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("failed to setup TLS and CA certificate: %v", err) } - // Create jailer with cert path from TLS setup - jailer, err := createJailer(jail.Config{ + // Create jailer with cert path from TLS setup + jailer, err := createJailer(jail.Config{ Logger: logger, - HttpProxyPort: int(config.ProxyPort), + HttpProxyPort: int(mergedCfg.ProxyPort), Username: username, Uid: uid, Gid: gid, @@ -209,16 +230,16 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("failed to create jailer: %v", err) } - // Create boundary instance - boundaryInstance, err := boundary.New(ctx, boundary.Config{ + // Create boundary instance + boundaryInstance, err := boundary.New(ctx, boundary.Config{ RuleEngine: ruleEngine, Auditor: auditor, TLSConfig: tlsConfig, Logger: logger, Jailer: jailer, - ProxyPort: int(config.ProxyPort), - PprofEnabled: config.PprofEnabled, - PprofPort: int(config.PprofPort), + ProxyPort: int(mergedCfg.ProxyPort), + PprofEnabled: mergedCfg.PprofEnabled, + PprofPort: int(mergedCfg.PprofPort), }) if err != nil { return fmt.Errorf("failed to create boundary instance: %v", err) diff --git a/cli/config_loader.go b/cli/config_loader.go new file mode 100644 index 0000000..f7231c0 --- /dev/null +++ b/cli/config_loader.go @@ -0,0 +1,99 @@ +package cli + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +type fileConfig struct { + Allow []string `yaml:"allow"` + LogLevel string `yaml:"log_level"` + LogDir string `yaml:"log_dir"` + ProxyPort int64 `yaml:"proxy_port"` + Pprof struct { + Enabled bool `yaml:"enabled"` + Port int64 `yaml:"port"` + } `yaml:"pprof"` +} + +func loadConfigFile(configPath string) (fileConfig, string, error) { + var cfg fileConfig + path, err := resolveConfigPath(configPath) + if err != nil { + return cfg, "", err + } + if path == "" { + return cfg, "", nil + } + b, err := os.ReadFile(path) + if err != nil { + return cfg, "", fmt.Errorf("failed to read config file %s: %v", path, err) + } + if err := yaml.Unmarshal(b, &cfg); err != nil { + return cfg, "", fmt.Errorf("failed to parse YAML in %s: %v", path, err) + } + return cfg, path, nil +} + +func resolveConfigPath(configPath string) (string, error) { + if configPath != "" { + return configPath, nil + } + // XDG default: $XDG_CONFIG_HOME/boundary/config.yaml or ~/.config/boundary/config.yaml + base := os.Getenv("XDG_CONFIG_HOME") + if base == "" { + h, err := os.UserHomeDir() + if err != nil { + return "", nil + } + base = filepath.Join(h, ".config") + } + path := filepath.Join(base, "boundary", "config.yaml") + if _, err := os.Stat(path); err == nil { + return path, nil + } + return "", nil +} + +// mergeConfig applies CLI over file config (CLI wins), with allow exclusivity enforced. +// Returns final Config and the allow list to use. +func mergeConfig(file fileConfig, cliCfg Config) (Config, []string, error) { + // Enforce allow exclusivity + if len(file.Allow) > 0 && len(cliCfg.AllowStrings) > 0 { + return Config{}, nil, errors.New("allow rules specified in both config file and CLI; specify in only one source") + } + + final := cliCfg + + // Fill from file where CLI left zero values + if final.LogLevel == "" && file.LogLevel != "" { + final.LogLevel = file.LogLevel + } + if final.LogDir == "" && file.LogDir != "" { + final.LogDir = file.LogDir + } + if final.ProxyPort == 0 && file.ProxyPort != 0 { + final.ProxyPort = file.ProxyPort + } + // pprof + if !final.PprofEnabled && file.Pprof.Enabled { + final.PprofEnabled = true + } + if final.PprofPort == 0 && file.Pprof.Port != 0 { + final.PprofPort = file.Pprof.Port + } + + // Choose allow from the only specified source + allow := cliCfg.AllowStrings + if len(allow) == 0 && len(file.Allow) > 0 { + allow = file.Allow + } + + return final, allow, nil +} + + From 9893ebb8674587d9bff3f410bacc9f8cc930fa97 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Thu, 30 Oct 2025 19:06:26 +0000 Subject: [PATCH 02/13] refactor: minor improvements --- cli/cli.go | 6 +++--- cli/config_loader.go | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index caf5074..b42be9b 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -174,18 +174,18 @@ func Run(ctx context.Context, config Config, args []string) error { if filePath != "" { logger.Debug("Loaded config file", "path", filePath) } - mergedCfg, allowList, err := mergeConfig(fileCfg, config) + mergedCfg, err := mergeConfig(fileCfg, config) if err != nil { logger.Error("Configuration error", "error", err) return err } // Parse allow list; default to deny-all if none provided - if len(allowList) == 0 { + if len(mergedCfg.AllowStrings) == 0 { logger.Warn("No allow rules specified; all network traffic will be denied by default") } // Parse allow rules - allowRules, err := rulesengine.ParseAllowSpecs(allowList) + allowRules, err := rulesengine.ParseAllowSpecs(mergedCfg.AllowStrings) if err != nil { logger.Error("Failed to parse allow rules", "error", err) return fmt.Errorf("failed to parse allow rules: %v", err) diff --git a/cli/config_loader.go b/cli/config_loader.go index f7231c0..c52489b 100644 --- a/cli/config_loader.go +++ b/cli/config_loader.go @@ -60,12 +60,12 @@ func resolveConfigPath(configPath string) (string, error) { } // mergeConfig applies CLI over file config (CLI wins), with allow exclusivity enforced. -// Returns final Config and the allow list to use. -func mergeConfig(file fileConfig, cliCfg Config) (Config, []string, error) { +// Returns final merged Config with AllowStrings set from the single allowed source. +func mergeConfig(file fileConfig, cliCfg Config) (Config, error) { // Enforce allow exclusivity - if len(file.Allow) > 0 && len(cliCfg.AllowStrings) > 0 { - return Config{}, nil, errors.New("allow rules specified in both config file and CLI; specify in only one source") - } + if len(file.Allow) > 0 && len(cliCfg.AllowStrings) > 0 { + return Config{}, errors.New("allow rules specified in both config file and CLI; specify in only one source") + } final := cliCfg @@ -87,13 +87,12 @@ func mergeConfig(file fileConfig, cliCfg Config) (Config, []string, error) { final.PprofPort = file.Pprof.Port } - // Choose allow from the only specified source - allow := cliCfg.AllowStrings - if len(allow) == 0 && len(file.Allow) > 0 { - allow = file.Allow - } + // Choose allow from the only specified source and set on final config + if len(final.AllowStrings) == 0 && len(file.Allow) > 0 { + final.AllowStrings = file.Allow + } - return final, allow, nil + return final, nil } From 4124017512029c204c9b66bbfbad33b5a78da16d Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Thu, 30 Oct 2025 19:23:49 +0000 Subject: [PATCH 03/13] refactor: minor improvements --- cli/config_loader.go | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/cli/config_loader.go b/cli/config_loader.go index c52489b..5c3d160 100644 --- a/cli/config_loader.go +++ b/cli/config_loader.go @@ -40,23 +40,19 @@ func loadConfigFile(configPath string) (fileConfig, string, error) { } func resolveConfigPath(configPath string) (string, error) { - if configPath != "" { - return configPath, nil - } - // XDG default: $XDG_CONFIG_HOME/boundary/config.yaml or ~/.config/boundary/config.yaml - base := os.Getenv("XDG_CONFIG_HOME") - if base == "" { - h, err := os.UserHomeDir() - if err != nil { - return "", nil - } - base = filepath.Join(h, ".config") - } - path := filepath.Join(base, "boundary", "config.yaml") - if _, err := os.Stat(path); err == nil { - return path, nil - } - return "", nil + if configPath != "" { + return configPath, nil + } + // Default: ~/.config/coder_boundary/config.yaml (same base as cert storage) + h, err := os.UserHomeDir() + if err != nil { + return "", nil + } + path := filepath.Join(h, ".config", "coder_boundary", "config.yaml") + if _, err := os.Stat(path); err == nil { + return path, nil + } + return "", nil } // mergeConfig applies CLI over file config (CLI wins), with allow exclusivity enforced. From 8630477ace19f6e1253e049c752dc0d9ca2b85ca Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Thu, 30 Oct 2025 19:52:45 +0000 Subject: [PATCH 04/13] refactor: minor improvements --- cli/cli.go | 11 +++-------- cli/config_loader.go | 13 +++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index b42be9b..d805f08 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -165,20 +165,15 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("no command specified") } - // Load file config and merge (CLI overrides file), enforce allow exclusivity - fileCfg, filePath, err := loadConfigFile(config.ConfigPath) + // Load and merge config (CLI overrides file), enforce allow exclusivity + mergedCfg, filePath, err := loadAndMergeConfig(config) if err != nil { - logger.Error("Failed to load config file", "error", err) + logger.Error("Configuration error", "error", err) return err } if filePath != "" { logger.Debug("Loaded config file", "path", filePath) } - mergedCfg, err := mergeConfig(fileCfg, config) - if err != nil { - logger.Error("Configuration error", "error", err) - return err - } // Parse allow list; default to deny-all if none provided if len(mergedCfg.AllowStrings) == 0 { logger.Warn("No allow rules specified; all network traffic will be denied by default") diff --git a/cli/config_loader.go b/cli/config_loader.go index 5c3d160..577252e 100644 --- a/cli/config_loader.go +++ b/cli/config_loader.go @@ -39,6 +39,19 @@ func loadConfigFile(configPath string) (fileConfig, string, error) { return cfg, path, nil } +// loadAndMergeConfig loads YAML config (if any) and merges with CLI config, returning the merged Config and the file path used. +func loadAndMergeConfig(cliCfg Config) (Config, string, error) { + fileCfg, filePath, err := loadConfigFile(cliCfg.ConfigPath) + if err != nil { + return Config{}, "", err + } + merged, err := mergeConfig(fileCfg, cliCfg) + if err != nil { + return Config{}, filePath, err + } + return merged, filePath, nil +} + func resolveConfigPath(configPath string) (string, error) { if configPath != "" { return configPath, nil From 568bfe721acc5ed32b52cbaf31949a605c88760e Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Thu, 30 Oct 2025 21:17:20 +0000 Subject: [PATCH 05/13] refactor: run gofmt --- cli/cli.go | 68 ++++++++++++++++++++--------------------- cli/config_loader.go | 72 +++++++++++++++++++++----------------------- rulesengine/rules.go | 4 +-- 3 files changed, 71 insertions(+), 73 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index d805f08..f904e40 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -27,7 +27,7 @@ type Config struct { AllowStrings []string LogLevel string LogDir string - ConfigPath string + ConfigPath string ProxyPort int64 PprofEnabled bool PprofPort int64 @@ -64,12 +64,12 @@ func BaseCommand() *serpent.Command { Short: "Network isolation tool for monitoring and restricting HTTP/HTTPS requests", Long: `boundary creates an isolated network environment for target processes, intercepting HTTP/HTTPS traffic through a transparent proxy that enforces user-defined allow rules.`, Options: []serpent.Option{ - { - Flag: "config", - Env: "BOUNDARY_CONFIG", - Description: "Path to YAML config file.", - Value: serpent.StringOf(&config.ConfigPath), - }, + { + Flag: "config", + Env: "BOUNDARY_CONFIG", + Description: "Path to YAML config file.", + Value: serpent.StringOf(&config.ConfigPath), + }, { Flag: "allow", Env: "BOUNDARY_ALLOW", @@ -111,8 +111,8 @@ func BaseCommand() *serpent.Command { }, }, Handler: func(inv *serpent.Invocation) error { - args := inv.Args - return Run(inv.Context(), config, args) + args := inv.Args + return Run(inv.Context(), config, args) }, } } @@ -165,22 +165,22 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("no command specified") } - // Load and merge config (CLI overrides file), enforce allow exclusivity - mergedCfg, filePath, err := loadAndMergeConfig(config) - if err != nil { - logger.Error("Configuration error", "error", err) - return err - } - if filePath != "" { - logger.Debug("Loaded config file", "path", filePath) - } - // Parse allow list; default to deny-all if none provided - if len(mergedCfg.AllowStrings) == 0 { - logger.Warn("No allow rules specified; all network traffic will be denied by default") - } - - // Parse allow rules - allowRules, err := rulesengine.ParseAllowSpecs(mergedCfg.AllowStrings) + // Load and merge config (CLI overrides file), enforce allow exclusivity + mergedCfg, filePath, err := loadAndMergeConfig(config) + if err != nil { + logger.Error("Configuration error", "error", err) + return err + } + if filePath != "" { + logger.Debug("Loaded config file", "path", filePath) + } + // Parse allow list; default to deny-all if none provided + if len(mergedCfg.AllowStrings) == 0 { + logger.Warn("No allow rules specified; all network traffic will be denied by default") + } + + // Parse allow rules + allowRules, err := rulesengine.ParseAllowSpecs(mergedCfg.AllowStrings) if err != nil { logger.Error("Failed to parse allow rules", "error", err) return fmt.Errorf("failed to parse allow rules: %v", err) @@ -192,7 +192,7 @@ func Run(ctx context.Context, config Config, args []string) error { // Create auditor auditor := audit.NewLogAuditor(logger) - // Create TLS certificate manager + // Create TLS certificate manager certManager, err := tls.NewCertificateManager(tls.Config{ Logger: logger, ConfigDir: configDir, @@ -210,10 +210,10 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("failed to setup TLS and CA certificate: %v", err) } - // Create jailer with cert path from TLS setup - jailer, err := createJailer(jail.Config{ + // Create jailer with cert path from TLS setup + jailer, err := createJailer(jail.Config{ Logger: logger, - HttpProxyPort: int(mergedCfg.ProxyPort), + HttpProxyPort: int(mergedCfg.ProxyPort), Username: username, Uid: uid, Gid: gid, @@ -225,16 +225,16 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("failed to create jailer: %v", err) } - // Create boundary instance - boundaryInstance, err := boundary.New(ctx, boundary.Config{ + // Create boundary instance + boundaryInstance, err := boundary.New(ctx, boundary.Config{ RuleEngine: ruleEngine, Auditor: auditor, TLSConfig: tlsConfig, Logger: logger, Jailer: jailer, - ProxyPort: int(mergedCfg.ProxyPort), - PprofEnabled: mergedCfg.PprofEnabled, - PprofPort: int(mergedCfg.PprofPort), + ProxyPort: int(mergedCfg.ProxyPort), + PprofEnabled: mergedCfg.PprofEnabled, + PprofPort: int(mergedCfg.PprofPort), }) if err != nil { return fmt.Errorf("failed to create boundary instance: %v", err) diff --git a/cli/config_loader.go b/cli/config_loader.go index 577252e..293b3dc 100644 --- a/cli/config_loader.go +++ b/cli/config_loader.go @@ -10,11 +10,11 @@ import ( ) type fileConfig struct { - Allow []string `yaml:"allow"` - LogLevel string `yaml:"log_level"` - LogDir string `yaml:"log_dir"` - ProxyPort int64 `yaml:"proxy_port"` - Pprof struct { + Allow []string `yaml:"allow"` + LogLevel string `yaml:"log_level"` + LogDir string `yaml:"log_dir"` + ProxyPort int64 `yaml:"proxy_port"` + Pprof struct { Enabled bool `yaml:"enabled"` Port int64 `yaml:"port"` } `yaml:"pprof"` @@ -41,40 +41,40 @@ func loadConfigFile(configPath string) (fileConfig, string, error) { // loadAndMergeConfig loads YAML config (if any) and merges with CLI config, returning the merged Config and the file path used. func loadAndMergeConfig(cliCfg Config) (Config, string, error) { - fileCfg, filePath, err := loadConfigFile(cliCfg.ConfigPath) - if err != nil { - return Config{}, "", err - } - merged, err := mergeConfig(fileCfg, cliCfg) - if err != nil { - return Config{}, filePath, err - } - return merged, filePath, nil + fileCfg, filePath, err := loadConfigFile(cliCfg.ConfigPath) + if err != nil { + return Config{}, "", err + } + merged, err := mergeConfig(fileCfg, cliCfg) + if err != nil { + return Config{}, filePath, err + } + return merged, filePath, nil } func resolveConfigPath(configPath string) (string, error) { - if configPath != "" { - return configPath, nil - } - // Default: ~/.config/coder_boundary/config.yaml (same base as cert storage) - h, err := os.UserHomeDir() - if err != nil { - return "", nil - } - path := filepath.Join(h, ".config", "coder_boundary", "config.yaml") - if _, err := os.Stat(path); err == nil { - return path, nil - } - return "", nil + if configPath != "" { + return configPath, nil + } + // Default: ~/.config/coder_boundary/config.yaml (same base as cert storage) + h, err := os.UserHomeDir() + if err != nil { + return "", nil + } + path := filepath.Join(h, ".config", "coder_boundary", "config.yaml") + if _, err := os.Stat(path); err == nil { + return path, nil + } + return "", nil } // mergeConfig applies CLI over file config (CLI wins), with allow exclusivity enforced. // Returns final merged Config with AllowStrings set from the single allowed source. func mergeConfig(file fileConfig, cliCfg Config) (Config, error) { // Enforce allow exclusivity - if len(file.Allow) > 0 && len(cliCfg.AllowStrings) > 0 { - return Config{}, errors.New("allow rules specified in both config file and CLI; specify in only one source") - } + if len(file.Allow) > 0 && len(cliCfg.AllowStrings) > 0 { + return Config{}, errors.New("allow rules specified in both config file and CLI; specify in only one source") + } final := cliCfg @@ -96,12 +96,10 @@ func mergeConfig(file fileConfig, cliCfg Config) (Config, error) { final.PprofPort = file.Pprof.Port } - // Choose allow from the only specified source and set on final config - if len(final.AllowStrings) == 0 && len(file.Allow) > 0 { - final.AllowStrings = file.Allow - } + // Choose allow from the only specified source and set on final config + if len(final.AllowStrings) == 0 && len(file.Allow) > 0 { + final.AllowStrings = file.Allow + } - return final, nil + return final, nil } - - diff --git a/rulesengine/rules.go b/rulesengine/rules.go index 4365734..a8c9f5d 100644 --- a/rulesengine/rules.go +++ b/rulesengine/rules.go @@ -142,14 +142,14 @@ func parseMethodPattern(token string) (string, string, error) { if token == "" { return "", "", errors.New("expected http token, got empty string") } - + // Find the first invalid HTTP token character for i := 0; i < len(token); i++ { if !isHTTPTokenChar(token[i]) { return token[:i], token[i:], nil } } - + // Entire string is a valid HTTP token return token, "", nil } From 4df146d299fc635a2056e9b312521d902e5d7baa Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 31 Oct 2025 15:54:55 +0000 Subject: [PATCH 06/13] fix: reimplement with built-in support --- cli/cli.go | 71 +++++++++++++++-------------- cli/config_loader.go | 105 ------------------------------------------- 2 files changed, 37 insertions(+), 139 deletions(-) delete mode 100644 cli/config_loader.go diff --git a/cli/cli.go b/cli/cli.go index f904e40..51b6e2d 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -24,13 +24,15 @@ import ( // Config holds all configuration for the CLI type Config struct { - AllowStrings []string - LogLevel string - LogDir string - ConfigPath string - ProxyPort int64 - PprofEnabled bool - PprofPort int64 + Config serpent.YAMLConfigPath `yaml:"-"` + AllowStrings serpent.StringArray `yaml:"allow"` + LogLevel serpent.String `yaml:"log_level"` + LogDir serpent.String `yaml:"log_dir"` + ProxyPort serpent.Int64 `yaml:"proxy_port"` + Pprof struct { + Enabled serpent.Bool `yaml:"enabled"` + Port serpent.Int64 `yaml:"port"` + } `yaml:"pprof"` } // NewCommand creates and returns the root serpent command @@ -68,46 +70,54 @@ func BaseCommand() *serpent.Command { Flag: "config", Env: "BOUNDARY_CONFIG", Description: "Path to YAML config file.", - Value: serpent.StringOf(&config.ConfigPath), + Default: "~/.config/coder_boundary/config.yaml", + Value: &config.Config, + YAML: "", }, { Flag: "allow", Env: "BOUNDARY_ALLOW", Description: "Allow rule (repeatable). Format: \"pattern\" or \"METHOD[,METHOD] pattern\".", - Value: serpent.StringArrayOf(&config.AllowStrings), + Value: &config.AllowStrings, + YAML: "allow", }, { Flag: "log-level", Env: "BOUNDARY_LOG_LEVEL", Description: "Set log level (error, warn, info, debug).", Default: "warn", - Value: serpent.StringOf(&config.LogLevel), + Value: &config.LogLevel, + YAML: "log_level", }, { Flag: "log-dir", Env: "BOUNDARY_LOG_DIR", Description: "Set a directory to write logs to rather than stderr.", - Value: serpent.StringOf(&config.LogDir), + Value: &config.LogDir, + YAML: "log_dir", }, { Flag: "proxy-port", Env: "PROXY_PORT", Description: "Set a port for HTTP proxy.", Default: "8080", - Value: serpent.Int64Of(&config.ProxyPort), + Value: &config.ProxyPort, + YAML: "proxy_port", }, { Flag: "pprof", Env: "BOUNDARY_PPROF", Description: "Enable pprof profiling server.", - Value: serpent.BoolOf(&config.PprofEnabled), + Value: &config.Pprof.Enabled, + YAML: "pprof.enabled", }, { Flag: "pprof-port", Env: "BOUNDARY_PPROF_PORT", Description: "Set port for pprof profiling server.", Default: "6060", - Value: serpent.Int64Of(&config.PprofPort), + Value: &config.Pprof.Port, + YAML: "pprof.port", }, }, Handler: func(inv *serpent.Invocation) error { @@ -165,22 +175,14 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("no command specified") } - // Load and merge config (CLI overrides file), enforce allow exclusivity - mergedCfg, filePath, err := loadAndMergeConfig(config) - if err != nil { - logger.Error("Configuration error", "error", err) - return err - } - if filePath != "" { - logger.Debug("Loaded config file", "path", filePath) - } // Parse allow list; default to deny-all if none provided - if len(mergedCfg.AllowStrings) == 0 { + allowStrings := config.AllowStrings.Value() + if len(allowStrings) == 0 { logger.Warn("No allow rules specified; all network traffic will be denied by default") } // Parse allow rules - allowRules, err := rulesengine.ParseAllowSpecs(mergedCfg.AllowStrings) + allowRules, err := rulesengine.ParseAllowSpecs(allowStrings) if err != nil { logger.Error("Failed to parse allow rules", "error", err) return fmt.Errorf("failed to parse allow rules: %v", err) @@ -213,7 +215,7 @@ func Run(ctx context.Context, config Config, args []string) error { // Create jailer with cert path from TLS setup jailer, err := createJailer(jail.Config{ Logger: logger, - HttpProxyPort: int(mergedCfg.ProxyPort), + HttpProxyPort: int(config.ProxyPort.Value()), Username: username, Uid: uid, Gid: gid, @@ -232,9 +234,9 @@ func Run(ctx context.Context, config Config, args []string) error { TLSConfig: tlsConfig, Logger: logger, Jailer: jailer, - ProxyPort: int(mergedCfg.ProxyPort), - PprofEnabled: mergedCfg.PprofEnabled, - PprofPort: int(mergedCfg.PprofPort), + ProxyPort: int(config.ProxyPort.Value()), + PprofEnabled: config.Pprof.Enabled.Value(), + PprofPort: int(config.Pprof.Port.Value()), }) if err != nil { return fmt.Errorf("failed to create boundary instance: %v", err) @@ -299,7 +301,7 @@ func Run(ctx context.Context, config Config, args []string) error { // setupLogging creates a slog logger with the specified level func setupLogging(config Config) (*slog.Logger, error) { var level slog.Level - switch strings.ToLower(config.LogLevel) { + switch strings.ToLower(config.LogLevel.Value()) { case "error": level = slog.LevelError case "warn": @@ -314,10 +316,11 @@ func setupLogging(config Config) (*slog.Logger, error) { logTarget := os.Stderr - if config.LogDir != "" { + logDir := config.LogDir.Value() + if logDir != "" { // Set up the logging directory if it doesn't exist yet - if err := os.MkdirAll(config.LogDir, 0755); err != nil { - return nil, fmt.Errorf("could not set up log dir %s: %v", config.LogDir, err) + if err := os.MkdirAll(logDir, 0755); err != nil { + return nil, fmt.Errorf("could not set up log dir %s: %v", logDir, err) } // Create a logfile (timestamp and pid to avoid race conditions with multiple boundary calls running) @@ -325,7 +328,7 @@ func setupLogging(config Config) (*slog.Logger, error) { time.Now().Format("2006-01-02_15-04-05"), os.Getpid()) - logFile, err := os.Create(filepath.Join(config.LogDir, logFilePath)) + logFile, err := os.Create(filepath.Join(logDir, logFilePath)) if err != nil { return nil, fmt.Errorf("could not create log file %s: %v", logFilePath, err) } diff --git a/cli/config_loader.go b/cli/config_loader.go deleted file mode 100644 index 293b3dc..0000000 --- a/cli/config_loader.go +++ /dev/null @@ -1,105 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -type fileConfig struct { - Allow []string `yaml:"allow"` - LogLevel string `yaml:"log_level"` - LogDir string `yaml:"log_dir"` - ProxyPort int64 `yaml:"proxy_port"` - Pprof struct { - Enabled bool `yaml:"enabled"` - Port int64 `yaml:"port"` - } `yaml:"pprof"` -} - -func loadConfigFile(configPath string) (fileConfig, string, error) { - var cfg fileConfig - path, err := resolveConfigPath(configPath) - if err != nil { - return cfg, "", err - } - if path == "" { - return cfg, "", nil - } - b, err := os.ReadFile(path) - if err != nil { - return cfg, "", fmt.Errorf("failed to read config file %s: %v", path, err) - } - if err := yaml.Unmarshal(b, &cfg); err != nil { - return cfg, "", fmt.Errorf("failed to parse YAML in %s: %v", path, err) - } - return cfg, path, nil -} - -// loadAndMergeConfig loads YAML config (if any) and merges with CLI config, returning the merged Config and the file path used. -func loadAndMergeConfig(cliCfg Config) (Config, string, error) { - fileCfg, filePath, err := loadConfigFile(cliCfg.ConfigPath) - if err != nil { - return Config{}, "", err - } - merged, err := mergeConfig(fileCfg, cliCfg) - if err != nil { - return Config{}, filePath, err - } - return merged, filePath, nil -} - -func resolveConfigPath(configPath string) (string, error) { - if configPath != "" { - return configPath, nil - } - // Default: ~/.config/coder_boundary/config.yaml (same base as cert storage) - h, err := os.UserHomeDir() - if err != nil { - return "", nil - } - path := filepath.Join(h, ".config", "coder_boundary", "config.yaml") - if _, err := os.Stat(path); err == nil { - return path, nil - } - return "", nil -} - -// mergeConfig applies CLI over file config (CLI wins), with allow exclusivity enforced. -// Returns final merged Config with AllowStrings set from the single allowed source. -func mergeConfig(file fileConfig, cliCfg Config) (Config, error) { - // Enforce allow exclusivity - if len(file.Allow) > 0 && len(cliCfg.AllowStrings) > 0 { - return Config{}, errors.New("allow rules specified in both config file and CLI; specify in only one source") - } - - final := cliCfg - - // Fill from file where CLI left zero values - if final.LogLevel == "" && file.LogLevel != "" { - final.LogLevel = file.LogLevel - } - if final.LogDir == "" && file.LogDir != "" { - final.LogDir = file.LogDir - } - if final.ProxyPort == 0 && file.ProxyPort != 0 { - final.ProxyPort = file.ProxyPort - } - // pprof - if !final.PprofEnabled && file.Pprof.Enabled { - final.PprofEnabled = true - } - if final.PprofPort == 0 && file.Pprof.Port != 0 { - final.PprofPort = file.Pprof.Port - } - - // Choose allow from the only specified source and set on final config - if len(final.AllowStrings) == 0 && len(file.Allow) > 0 { - final.AllowStrings = file.Allow - } - - return final, nil -} From 6c9beaf69a2d6118ad3634173f3b21af7da35e73 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 31 Oct 2025 17:55:16 +0000 Subject: [PATCH 07/13] fix: almost works --- cli/cli.go | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 51b6e2d..1779f5a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -2,6 +2,7 @@ package cli import ( "context" + "encoding/json" "fmt" "log" "log/slog" @@ -30,7 +31,7 @@ type Config struct { LogDir serpent.String `yaml:"log_dir"` ProxyPort serpent.Int64 `yaml:"proxy_port"` Pprof struct { - Enabled serpent.Bool `yaml:"enabled"` + Enabled serpent.Bool `yaml:"enabled"` Port serpent.Int64 `yaml:"port"` } `yaml:"pprof"` } @@ -61,6 +62,18 @@ func NewCommand() *serpent.Command { func BaseCommand() *serpent.Command { config := Config{} + // Resolve default config path - use same logic as util.GetUserInfo() (handles sudo scenarios) + _, _, _, _, configDir := util.GetUserInfo() + defaultConfigPath := "" + if configDir != "" { + defaultConfigPath = filepath.Join(configDir, "config.yaml") + } else { + // Fallback if we can't determine config dir + if home, err := os.UserHomeDir(); err == nil { + defaultConfigPath = filepath.Join(home, ".config", "coder_boundary", "config.yaml") + } + } + return &serpent.Command{ Use: "boundary", Short: "Network isolation tool for monitoring and restricting HTTP/HTTPS requests", @@ -70,7 +83,7 @@ func BaseCommand() *serpent.Command { Flag: "config", Env: "BOUNDARY_CONFIG", Description: "Path to YAML config file.", - Default: "~/.config/coder_boundary/config.yaml", + Default: defaultConfigPath, Value: &config.Config, YAML: "", }, @@ -109,7 +122,7 @@ func BaseCommand() *serpent.Command { Env: "BOUNDARY_PPROF", Description: "Enable pprof profiling server.", Value: &config.Pprof.Enabled, - YAML: "pprof.enabled", + YAML: "pprof_enabled", }, { Flag: "pprof-port", @@ -117,7 +130,7 @@ func BaseCommand() *serpent.Command { Description: "Set port for pprof profiling server.", Default: "6060", Value: &config.Pprof.Port, - YAML: "pprof.port", + YAML: "pprof_port", }, }, Handler: func(inv *serpent.Invocation) error { @@ -133,6 +146,23 @@ func isChild() bool { // Run executes the boundary command with the given configuration and arguments func Run(ctx context.Context, config Config, args []string) error { + // Debug: show config path and if file exists + configPath := config.Config.String() + fmt.Fprintf(os.Stderr, "Config path: %s\n", configPath) + if configPath != "" { + if _, err := os.Stat(configPath); err == nil { + fmt.Fprintf(os.Stderr, "Config file exists and will be loaded by serpent\n") + } else { + fmt.Fprintf(os.Stderr, "Config file does not exist: %v\n", err) + } + } + + configInJSON, err := json.Marshal(config) + if err != nil { + panic(err) + } + fmt.Printf("%s\n", configInJSON) + logger, err := setupLogging(config) if err != nil { return fmt.Errorf("could not set up logging: %v", err) From a9518d6f12209e285248aa31103871155f0f0e34 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 31 Oct 2025 18:03:58 +0000 Subject: [PATCH 08/13] fix: seems eventually works --- cli/cli.go | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 1779f5a..f12e482 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -63,14 +63,21 @@ func BaseCommand() *serpent.Command { config := Config{} // Resolve default config path - use same logic as util.GetUserInfo() (handles sudo scenarios) + // Set it directly on the config so serpent sees it as the default value _, _, _, _, configDir := util.GetUserInfo() - defaultConfigPath := "" if configDir != "" { - defaultConfigPath = filepath.Join(configDir, "config.yaml") + defaultPath := filepath.Join(configDir, "config.yaml") + // Only set if file exists - serpent will load it automatically + if _, err := os.Stat(defaultPath); err == nil { + config.Config = serpent.YAMLConfigPath(defaultPath) + } } else { // Fallback if we can't determine config dir if home, err := os.UserHomeDir(); err == nil { - defaultConfigPath = filepath.Join(home, ".config", "coder_boundary", "config.yaml") + defaultPath := filepath.Join(home, ".config", "coder_boundary", "config.yaml") + if _, err := os.Stat(defaultPath); err == nil { + config.Config = serpent.YAMLConfigPath(defaultPath) + } } } @@ -83,7 +90,6 @@ func BaseCommand() *serpent.Command { Flag: "config", Env: "BOUNDARY_CONFIG", Description: "Path to YAML config file.", - Default: defaultConfigPath, Value: &config.Config, YAML: "", }, @@ -146,17 +152,6 @@ func isChild() bool { // Run executes the boundary command with the given configuration and arguments func Run(ctx context.Context, config Config, args []string) error { - // Debug: show config path and if file exists - configPath := config.Config.String() - fmt.Fprintf(os.Stderr, "Config path: %s\n", configPath) - if configPath != "" { - if _, err := os.Stat(configPath); err == nil { - fmt.Fprintf(os.Stderr, "Config file exists and will be loaded by serpent\n") - } else { - fmt.Fprintf(os.Stderr, "Config file does not exist: %v\n", err) - } - } - configInJSON, err := json.Marshal(config) if err != nil { panic(err) From e598002969d5dd522de9f652ebbbfbdc48e05615 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 31 Oct 2025 18:29:04 +0000 Subject: [PATCH 09/13] fix: minor fixes --- cli/cli.go | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index f12e482..2592a2c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -30,10 +30,8 @@ type Config struct { LogLevel serpent.String `yaml:"log_level"` LogDir serpent.String `yaml:"log_dir"` ProxyPort serpent.Int64 `yaml:"proxy_port"` - Pprof struct { - Enabled serpent.Bool `yaml:"enabled"` - Port serpent.Int64 `yaml:"port"` - } `yaml:"pprof"` + PprofEnabled serpent.Bool `yaml:"pprof_enabled"` + PprofPort serpent.Int64 `yaml:"pprof_port"` } // NewCommand creates and returns the root serpent command @@ -62,23 +60,12 @@ func NewCommand() *serpent.Command { func BaseCommand() *serpent.Command { config := Config{} - // Resolve default config path - use same logic as util.GetUserInfo() (handles sudo scenarios) - // Set it directly on the config so serpent sees it as the default value - _, _, _, _, configDir := util.GetUserInfo() - if configDir != "" { - defaultPath := filepath.Join(configDir, "config.yaml") - // Only set if file exists - serpent will load it automatically + // Set default config path if file exists - serpent will load it automatically + if home, err := os.UserHomeDir(); err == nil { + defaultPath := filepath.Join(home, ".config", "coder_boundary", "config.yaml") if _, err := os.Stat(defaultPath); err == nil { config.Config = serpent.YAMLConfigPath(defaultPath) } - } else { - // Fallback if we can't determine config dir - if home, err := os.UserHomeDir(); err == nil { - defaultPath := filepath.Join(home, ".config", "coder_boundary", "config.yaml") - if _, err := os.Stat(defaultPath); err == nil { - config.Config = serpent.YAMLConfigPath(defaultPath) - } - } } return &serpent.Command{ @@ -127,7 +114,7 @@ func BaseCommand() *serpent.Command { Flag: "pprof", Env: "BOUNDARY_PPROF", Description: "Enable pprof profiling server.", - Value: &config.Pprof.Enabled, + Value: &config.PprofEnabled, YAML: "pprof_enabled", }, { @@ -135,7 +122,7 @@ func BaseCommand() *serpent.Command { Env: "BOUNDARY_PPROF_PORT", Description: "Set port for pprof profiling server.", Default: "6060", - Value: &config.Pprof.Port, + Value: &config.PprofPort, YAML: "pprof_port", }, }, @@ -260,8 +247,8 @@ func Run(ctx context.Context, config Config, args []string) error { Logger: logger, Jailer: jailer, ProxyPort: int(config.ProxyPort.Value()), - PprofEnabled: config.Pprof.Enabled.Value(), - PprofPort: int(config.Pprof.Port.Value()), + PprofEnabled: config.PprofEnabled.Value(), + PprofPort: int(config.PprofPort.Value()), }) if err != nil { return fmt.Errorf("failed to create boundary instance: %v", err) From 14b843112c07568d9cbde98411a54ac57b781e41 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 31 Oct 2025 18:35:31 +0000 Subject: [PATCH 10/13] fix: minor fixes --- cli/cli.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 2592a2c..fc81860 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -139,16 +139,16 @@ func isChild() bool { // Run executes the boundary command with the given configuration and arguments func Run(ctx context.Context, config Config, args []string) error { - configInJSON, err := json.Marshal(config) + logger, err := setupLogging(config) if err != nil { - panic(err) + return fmt.Errorf("could not set up logging: %v", err) } - fmt.Printf("%s\n", configInJSON) - logger, err := setupLogging(config) + configInJSON, err := json.Marshal(config) if err != nil { - return fmt.Errorf("could not set up logging: %v", err) + return err } + logger.Debug("config", "json_config", configInJSON) if isChild() { logger.Info("boundary CHILD process is started") From cace22321ee1b56a0b56b852fec889de8f214375 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Wed, 12 Nov 2025 15:04:00 +0000 Subject: [PATCH 11/13] temporary commit --- cli/cli.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cli/cli.go b/cli/cli.go index fc81860..d41e5d3 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -61,11 +61,23 @@ func BaseCommand() *serpent.Command { config := Config{} // Set default config path if file exists - serpent will load it automatically + logToFile := func(message string) { + if logFile, err := os.OpenFile("/tmp/yev_boundary_log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { + fmt.Fprintf(logFile, "[%s] %s\n", time.Now().Format(time.RFC3339), message) + logFile.Close() + } + } + if home, err := os.UserHomeDir(); err == nil { defaultPath := filepath.Join(home, ".config", "coder_boundary", "config.yaml") if _, err := os.Stat(defaultPath); err == nil { config.Config = serpent.YAMLConfigPath(defaultPath) + logToFile(fmt.Sprintf("Config file loaded: %s", defaultPath)) + } else { + logToFile(fmt.Sprintf("Config file not found: %s", defaultPath)) } + } else { + logToFile("Failed to determine home directory, cannot check for default config") } return &serpent.Command{ From e03df6bfd7599d32b7f7c6c3952d8102db9d867d Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Wed, 12 Nov 2025 16:33:49 +0000 Subject: [PATCH 12/13] tmp commit: merge config & cli --- cli/cli.go | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index d41e5d3..576ea7a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -25,13 +25,14 @@ import ( // Config holds all configuration for the CLI type Config struct { - Config serpent.YAMLConfigPath `yaml:"-"` - AllowStrings serpent.StringArray `yaml:"allow"` - LogLevel serpent.String `yaml:"log_level"` - LogDir serpent.String `yaml:"log_dir"` - ProxyPort serpent.Int64 `yaml:"proxy_port"` - PprofEnabled serpent.Bool `yaml:"pprof_enabled"` - PprofPort serpent.Int64 `yaml:"pprof_port"` + Config serpent.YAMLConfigPath `yaml:"-"` + AllowListStrings serpent.StringArray `yaml:"allowlist"` // From config file + AllowStrings serpent.StringArray `yaml:"-"` // From CLI flags only + LogLevel serpent.String `yaml:"log_level"` + LogDir serpent.String `yaml:"log_dir"` + ProxyPort serpent.Int64 `yaml:"proxy_port"` + PprofEnabled serpent.Bool `yaml:"pprof_enabled"` + PprofPort serpent.Int64 `yaml:"pprof_port"` } // NewCommand creates and returns the root serpent command @@ -49,6 +50,9 @@ func NewCommand() *serpent.Command { # Monitor all requests to specific domains (allow only those) boundary --allow "domain=github.com path=/api/issues/*" --allow "method=GET,HEAD domain=github.com" -- npm install + # Use allowlist from config file with additional CLI allow rules + boundary --allow "domain=example.com" -- curl https://example.com + # Block everything by default (implicit)` return cmd @@ -95,9 +99,15 @@ func BaseCommand() *serpent.Command { { Flag: "allow", Env: "BOUNDARY_ALLOW", - Description: "Allow rule (repeatable). Format: \"pattern\" or \"METHOD[,METHOD] pattern\".", + Description: "Allow rule (repeatable). These are merged with allowlist from config file. Format: \"pattern\" or \"METHOD[,METHOD] pattern\".", Value: &config.AllowStrings, - YAML: "allow", + YAML: "", // CLI only, not loaded from YAML + }, + { + Flag: "", // No CLI flag, YAML only + Description: "Allowlist rules from config file (YAML only).", + Value: &config.AllowListStrings, + YAML: "allowlist", }, { Flag: "log-level", @@ -199,14 +209,19 @@ func Run(ctx context.Context, config Config, args []string) error { return fmt.Errorf("no command specified") } - // Parse allow list; default to deny-all if none provided + // Merge allowlist from config file with allow from CLI flags + allowListStrings := config.AllowListStrings.Value() allowStrings := config.AllowStrings.Value() - if len(allowStrings) == 0 { + + // Combine allowlist (config file) with allow (CLI flags) + allAllowStrings := append(allowListStrings, allowStrings...) + + if len(allAllowStrings) == 0 { logger.Warn("No allow rules specified; all network traffic will be denied by default") } // Parse allow rules - allowRules, err := rulesengine.ParseAllowSpecs(allowStrings) + allowRules, err := rulesengine.ParseAllowSpecs(allAllowStrings) if err != nil { logger.Error("Failed to parse allow rules", "error", err) return fmt.Errorf("failed to parse allow rules: %v", err) From 64e40c8f5b85313ed57fcbc8518acf4be4bd9edc Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Wed, 12 Nov 2025 19:49:13 +0000 Subject: [PATCH 13/13] remove debug logs --- cli/cli.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 576ea7a..b2edaba 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -65,23 +65,11 @@ func BaseCommand() *serpent.Command { config := Config{} // Set default config path if file exists - serpent will load it automatically - logToFile := func(message string) { - if logFile, err := os.OpenFile("/tmp/yev_boundary_log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { - fmt.Fprintf(logFile, "[%s] %s\n", time.Now().Format(time.RFC3339), message) - logFile.Close() - } - } - if home, err := os.UserHomeDir(); err == nil { defaultPath := filepath.Join(home, ".config", "coder_boundary", "config.yaml") if _, err := os.Stat(defaultPath); err == nil { config.Config = serpent.YAMLConfigPath(defaultPath) - logToFile(fmt.Sprintf("Config file loaded: %s", defaultPath)) - } else { - logToFile(fmt.Sprintf("Config file not found: %s", defaultPath)) } - } else { - logToFile("Failed to determine home directory, cannot check for default config") } return &serpent.Command{