Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions cmd/imds-broker/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package main

import (
"context"
"fmt"
"io"
"os"
"strings"

"github.com/urfave/cli/v3"

brokerconfig "github.com/jamestelfer/imds-broker/pkg/config"
)

// configCommand defines the host-side configuration command. It reads and
// writes the broker configuration file directly; it makes no AWS calls and
// starts no servers. The configuration file is host-controlled, not an
// agent-reachable interface: see the project README sandbox model.
func configCommand() *cli.Command {
return &cli.Command{
Name: "config",
Usage: "Inspect and modify the host-side broker configuration file",
Commands: []*cli.Command{
configPathCommand(),
configListCommand(),
configSetCommand(),
},
}
}

// commandWriter returns the root command writer, defaulting to stdout.
func commandWriter(cmd *cli.Command) io.Writer {
if w := cmd.Root().Writer; w != nil {
return w
}
return os.Stdout
}

func configPathCommand() *cli.Command {
return &cli.Command{
Name: "path",
Usage: "Print the configuration file location",
Action: func(ctx context.Context, cmd *cli.Command) error {
path, err := brokerconfig.ResolvePath(ctx)
if err != nil {
return fmt.Errorf("config path: %w", err)
}
_, err = io.WriteString(commandWriter(cmd), path+"\n")
return err
},
}
}

func configListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Aliases: []string{"show"},
Usage: "List the current configuration values",
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg, err := brokerconfig.Load(ctx)
if err != nil {
return fmt.Errorf("config list: %w", err)
}

fileState := "not found (using built-in defaults)"
if cfg.Found {
fileState = "found"
}
var b strings.Builder
fmt.Fprintf(&b, "path: %s\n", cfg.Path)
fmt.Fprintf(&b, "file: %s\n", fileState)
fmt.Fprintf(&b, "%s: %s\n", brokerconfig.KeyProfileFilter, valueOrUnset(cfg.ProfileFilter))
fmt.Fprintf(&b, "%s: %s\n", brokerconfig.KeyRegion, valueOrUnset(cfg.Region))
fmt.Fprintf(&b, "%s: %s\n", brokerconfig.KeyLogLevel, valueOrUnset(cfg.LogLevel))
_, err = io.WriteString(commandWriter(cmd), b.String())
return err
},
}
}

func configSetCommand() *cli.Command {
return &cli.Command{
Name: "set",
Usage: "Set a configuration value (creates the file if absent)",
ArgsUsage: "<key> <value>",
Description: "Valid keys: " + strings.Join(brokerconfig.Keys, ", ") +
". An empty value clears the key.",
Action: func(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args()
if args.Len() != 2 {
return fmt.Errorf("config set: expected <key> <value>, got %d argument(s)", args.Len())
}
key, value := args.Get(0), args.Get(1)

cfg, err := brokerconfig.Set(ctx, key, value)
if err != nil {
return fmt.Errorf("config set: %w", err)
}

msg := fmt.Sprintf("set %s = %s in %s\n", key, value, cfg.Path)
if value == "" {
msg = fmt.Sprintf("cleared %s in %s\n", key, cfg.Path)
}
_, err = io.WriteString(commandWriter(cmd), msg)
return err
},
}
}

// valueOrUnset renders an absent configuration value distinctly from an empty
// string set on disk.
func valueOrUnset(v string) string {
if v == "" {
return "(unset; built-in default applies)"
}
return v
}
5 changes: 1 addition & 4 deletions cmd/imds-broker/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ func doctorCommand() *cli.Command {
profileFilterFlag(),
},
Action: func(ctx context.Context, cmd *cli.Command) error {
w := cmd.Root().Writer
if w == nil {
w = os.Stdout
}
w := commandWriter(cmd)

cfg, err := brokerconfig.Load(ctx)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/imds-broker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func main() {
serveCommand(),
profilesCommand(),
mcpCommand(),
configCommand(),
doctorCommand(),
versionCommand(),
},
Expand Down
55 changes: 55 additions & 0 deletions cmd/imds-broker/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,61 @@ func TestEffectiveRegion_ConfigDefaultAndFlagOverride(t *testing.T) {
func(c *cli.Command) { assert.Equal(t, "us-east-1", effectiveRegion(c, cfg)) })
}

// runConfigCmd runs the top-level config command with args, capturing stdout.
func runConfigCmd(t *testing.T, args ...string) (string, error) {
t.Helper()
var buf bytes.Buffer
cmd := &cli.Command{
Name: "imds-broker",
Writer: &buf,
Commands: []*cli.Command{configCommand()},
}
err := cmd.Run(context.Background(), append([]string{"imds-broker"}, args...))
return buf.String(), err
}

func TestConfigPath_PrintsResolvedLocation(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)

out, err := runConfigCmd(t, "config", "path")
require.NoError(t, err)
assert.Contains(t, out, filepath.Join(dir, brokerconfig.RelPath))
}

func TestConfigList_ReportsUnsetWhenAbsent(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)

out, err := runConfigCmd(t, "config", "list")
require.NoError(t, err)
assert.Contains(t, out, "file: not found")
assert.Contains(t, out, "profile-filter: (unset")
}

func TestConfigSet_WritesThenListShows(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)

out, err := runConfigCmd(t, "config", "set", "region", "ap-southeast-2")
require.NoError(t, err)
assert.Contains(t, out, "set region = ap-southeast-2")

out, err = runConfigCmd(t, "config", "list")
require.NoError(t, err)
assert.Contains(t, out, "file: found")
assert.Contains(t, out, "region: ap-southeast-2")
}

func TestConfigSet_RejectsWrongArgCount(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)

_, err := runConfigCmd(t, "config", "set", "region")
require.Error(t, err)
assert.Contains(t, err.Error(), "expected <key> <value>")
}

func TestEffectiveLogLevel_Precedence(t *testing.T) {
withConfig := &brokerconfig.Config{LogLevel: "debug"}
noConfig := &brokerconfig.Config{}
Expand Down
Loading