diff --git a/secrets.go b/secrets.go new file mode 100644 index 0000000..79105a4 --- /dev/null +++ b/secrets.go @@ -0,0 +1,37 @@ +package traefikllmgateway + +import ( + "fmt" + "os" + "strings" +) + +// resolveSecret resolves a config string value to its underlying secret. An +// empty value returns empty with no error. A "env:NAME" value returns +// os.Getenv(NAME), erroring if the variable is unset or empty. A +// "file:/path" value returns the file's contents with surrounding +// whitespace trimmed, erroring if the file cannot be read. Any other value +// is returned unchanged as a literal. +// +// Error messages never include a resolved secret value — only the env var +// name or file path, which are config, not secrets. +func resolveSecret(v string) (string, error) { + if v == "" { + return "", nil + } + if name, ok := strings.CutPrefix(v, "env:"); ok { + val := os.Getenv(name) + if val == "" { + return "", fmt.Errorf("llmgateway: env var %q is unset or empty", name) + } + return val, nil + } + if path, ok := strings.CutPrefix(v, "file:"); ok { + b, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("llmgateway: cannot read secret file %q: %w", path, err) + } + return strings.TrimSpace(string(b)), nil + } + return v, nil +} diff --git a/secrets_test.go b/secrets_test.go new file mode 100644 index 0000000..df01069 --- /dev/null +++ b/secrets_test.go @@ -0,0 +1,37 @@ +package traefikllmgateway + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveSecret(t *testing.T) { + t.Setenv("LLMGW_T", "from-env") + dir := t.TempDir() + fp := filepath.Join(dir, "k") + if err := os.WriteFile(fp, []byte("from-file\n"), 0o600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + cases := []struct { + in string + want string + wantErr bool + }{ + {"plain", "plain", false}, + {"env:LLMGW_T", "from-env", false}, + {"env:LLMGW_MISSING", "", true}, + {"file:" + fp, "from-file", false}, + {"file:" + fp + ".nope", "", true}, + {"", "", false}, + } + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + got, err := resolveSecret(c.in) + if (err != nil) != c.wantErr || got != c.want { + t.Errorf("resolveSecret(%q) = %q, %v; wantErr %v", c.in, got, err, c.wantErr) + } + }) + } +}