Add support for reading MQTT password from file
Add `--mqtt.password-file` flag and `MQTT_PASSWORD_FILE` env var to allow reading the MQTT password from a file (e.g. Docker secrets). When set, the file contents overrides the --mqtt.password value. Addresses diebietse/invertergui#46
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jessevdk/go-flags"
|
"github.com/jessevdk/go-flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +25,7 @@ type config struct {
|
|||||||
Topic string `long:"mqtt.topic" env:"MQTT_TOPIC" default:"invertergui/updates" description:"Set the MQTT topic updates published to."`
|
Topic string `long:"mqtt.topic" env:"MQTT_TOPIC" default:"invertergui/updates" description:"Set the MQTT topic updates published to."`
|
||||||
Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"`
|
Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"`
|
||||||
Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"`
|
Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"`
|
||||||
|
PasswordFile string `long:"mqtt.password-file" env:"MQTT_PASSWORD_FILE" default:"" description:"Path to a file containing the MQTT password"`
|
||||||
}
|
}
|
||||||
Loglevel string `long:"loglevel" env:"LOGLEVEL" default:"info" description:"The log level to generate logs at. (\"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")"`
|
Loglevel string `long:"loglevel" env:"LOGLEVEL" default:"info" description:"The log level to generate logs at. (\"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")"`
|
||||||
}
|
}
|
||||||
@@ -31,5 +36,30 @@ func parseConfig() (*config, error) {
|
|||||||
if _, err := parser.Parse(); err != nil {
|
if _, err := parser.Parse(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := resolvePasswordFile(conf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return conf, nil
|
return conf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolvePasswordFile(conf *config) error {
|
||||||
|
if conf.MQTT.PasswordFile != "" && conf.MQTT.Password != "" {
|
||||||
|
return fmt.Errorf("mqtt.password and mqtt.password-file are mutually exclusive")
|
||||||
|
}
|
||||||
|
if conf.MQTT.PasswordFile != "" {
|
||||||
|
password, err := readPasswordFile(conf.MQTT.PasswordFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conf.MQTT.Password = password
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPasswordFile(path string) (string, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not read MQTT password file: %w", err)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(string(data), "\n\r"), nil
|
||||||
|
}
|
||||||
|
|||||||
107
cmd/invertergui/config_test.go
Normal file
107
cmd/invertergui/config_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testInlineSecret = "inline-secret"
|
||||||
|
|
||||||
|
func TestReadPasswordFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "plain password",
|
||||||
|
content: "secret",
|
||||||
|
expected: "secret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password with trailing newline",
|
||||||
|
content: "secret\n",
|
||||||
|
expected: "secret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password with trailing carriage return and newline",
|
||||||
|
content: "secret\r\n",
|
||||||
|
expected: "secret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty file",
|
||||||
|
content: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "password")
|
||||||
|
if err := os.WriteFile(path, []byte(tt.content), 0o600); err != nil {
|
||||||
|
t.Fatalf("failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := readPasswordFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("got %q, want %q", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadPasswordFile_NotFound(t *testing.T) {
|
||||||
|
_, err := readPasswordFile("/nonexistent/path/password")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing file, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePassword_MutuallyExclusive(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "password")
|
||||||
|
if err := os.WriteFile(path, []byte("secret"), 0o600); err != nil {
|
||||||
|
t.Fatalf("failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &config{}
|
||||||
|
conf.MQTT.Password = testInlineSecret
|
||||||
|
conf.MQTT.PasswordFile = path
|
||||||
|
|
||||||
|
err := resolvePasswordFile(conf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when both mqtt.password and mqtt.password-file are set, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePassword_FromFile(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "password")
|
||||||
|
if err := os.WriteFile(path, []byte("file-secret\n"), 0o600); err != nil {
|
||||||
|
t.Fatalf("failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &config{}
|
||||||
|
conf.MQTT.PasswordFile = path
|
||||||
|
|
||||||
|
if err := resolvePasswordFile(conf); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if conf.MQTT.Password != "file-secret" {
|
||||||
|
t.Errorf("got %q, want %q", conf.MQTT.Password, "file-secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePassword_NoFile(t *testing.T) {
|
||||||
|
conf := &config{}
|
||||||
|
conf.MQTT.Password = testInlineSecret
|
||||||
|
|
||||||
|
if err := resolvePasswordFile(conf); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if conf.MQTT.Password != testInlineSecret {
|
||||||
|
t.Errorf("got %q, want %q", conf.MQTT.Password, testInlineSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user