From 77ad55f1ebbe2c70e8f0deef5e306bc6a3b4f275 Mon Sep 17 00:00:00 2001 From: Marvin Preuss Date: Tue, 2 Apr 2024 07:02:59 +0000 Subject: [PATCH] feat(config): own PasswordFile type its possible to unmarshal a PasswordFile path to a read string. this is nice for docker secret setups. also adds a GetPassword method which checks if both Password and PasswordFile are declared. --- internal/config/config.go | 47 +++++++++++++++++-- internal/config/config_test.go | 84 ++++++++++++++++++++++++++++++++++ internal/config/errors.go | 8 ++++ internal/metrics/metrics.go | 46 ++++++++++++------- 4 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 internal/config/config_test.go create mode 100644 internal/config/errors.go diff --git a/internal/config/config.go b/internal/config/config.go index 375e4dd..34bdbfe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,11 +1,48 @@ package config +import ( + "encoding" + "fmt" + "os" +) + var Cfg Config //nolint:gochecknoglobals type Config struct { - Email string `env:"EMAIL,required"` - Password string `env:"PASSWORD,expand"` - PasswordFile string `env:"PASSWORD_FILE,expand"` - CacheDir string `env:"CACHE_DIR,expand" envDefault:"/var/cache/glucose_exporter"` - Debug bool `env:"DEBUG"` + Email string `env:"EMAIL,required"` + Password string `env:"PASSWORD,expand"` + PasswordFile PasswordFile `env:"PASSWORD_FILE,expand"` + CacheDir string `env:"CACHE_DIR,expand" envDefault:"/var/cache/glucose_exporter"` + Debug bool `env:"DEBUG"` +} + +func (c *Config) GetPassword() (string, error) { + if c.PasswordFile != "" && c.Password != "" { + return "", ErrTooManyPasswords + } + + if c.Password != "" { + return c.Password, nil + } + + if c.PasswordFile != "" { + return string(c.PasswordFile), nil + } + + return "", ErrMissingPassword +} + +type PasswordFile string + +var _ encoding.TextUnmarshaler = (*PasswordFile)(nil) + +func (pf *PasswordFile) UnmarshalText(text []byte) error { + b, err := os.ReadFile(string(text)) + if err != nil { + return fmt.Errorf("reading password file: %w", err) + } + + *pf = PasswordFile(string(b)) + + return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..2d4cd06 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,84 @@ +package config_test + +import ( + "errors" + "os" + "path" + "testing" + + "github.com/matryer/is" + "go.xsfx.dev/glucose_exporter/internal/config" +) + +func TestPasswordFile(t *testing.T) { + is := is.New(t) + + dir := t.TempDir() + + pfPath := path.Join(dir, "foo.txt") + + err := os.WriteFile(pfPath, []byte("f00b4r"), 0o600) + is.NoErr(err) + + var pf config.PasswordFile + + err = pf.UnmarshalText([]byte(pfPath)) + is.NoErr(err) + + is.Equal(string(pf), "f00b4r") +} + +func Test(t *testing.T) { + tables := []struct { + name string + cfg config.Config + expected string + err error + }{ + { + name: "00", + cfg: config.Config{}, + expected: "", + err: config.ErrMissingPassword, + }, + { + name: "01", + cfg: config.Config{ + Password: "foo", + PasswordFile: "bar", + }, + expected: "", + err: config.ErrTooManyPasswords, + }, + { + name: "02", + cfg: config.Config{ + Password: "foo", + }, + expected: "foo", + err: nil, + }, + { + name: "03", + cfg: config.Config{ + PasswordFile: "foo", + }, + expected: "foo", + err: nil, + }, + } + + is := is.New(t) + + for _, tt := range tables { + t.Run(tt.name, func(_ *testing.T) { + pass, err := tt.cfg.GetPassword() + if tt.err == nil { + is.NoErr(err) + is.Equal(pass, tt.expected) + } else { + is.True(errors.Is(err, tt.err)) + } + }) + } +} diff --git a/internal/config/errors.go b/internal/config/errors.go new file mode 100644 index 0000000..02028ea --- /dev/null +++ b/internal/config/errors.go @@ -0,0 +1,8 @@ +package config + +import "errors" + +var ( + ErrTooManyPasswords = errors.New("too many passwords") + ErrMissingPassword = errors.New("missing password") +) diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index adf64b8..95c540d 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -26,7 +26,6 @@ func Handler(w http.ResponseWriter, r *http.Request) { vm.WritePrometheus(w, false) } -//nolint:funlen func glucose(ctx context.Context, w io.Writer) error { c, err := cache.Load() if err != nil { @@ -37,22 +36,8 @@ func glucose(ctx context.Context, w io.Writer) error { if c.JWT == "" || time.Now().After(time.Time(c.Expires)) { slog.Debug("needs a fresh token") - token, err := api.Login( - ctx, - api.BaseURL, - config.Cfg.Email, - config.Cfg.Password, - ) - if err != nil { - return fmt.Errorf("login: %w", err) - } - - slog.Debug("got token", "token", token) - - if err := cache.Save( - cache.Cache{JWT: token.Data.AuthTicket.Token, Expires: token.Data.AuthTicket.Expires}, - ); err != nil { - return fmt.Errorf("saving cache: %w", err) + if err := token(ctx); err != nil { + return fmt.Errorf("refreshing token: %w", err) } } @@ -101,3 +86,30 @@ func glucose(ctx context.Context, w io.Writer) error { return nil } + +func token(ctx context.Context) error { + pass, err := config.Cfg.GetPassword() + if err != nil { + return fmt.Errorf("getting password from config: %w", err) + } + + token, err := api.Login( + ctx, + api.BaseURL, + config.Cfg.Email, + pass, + ) + if err != nil { + return fmt.Errorf("login: %w", err) + } + + slog.Debug("got token", "token", token) + + if err := cache.Save( + cache.Cache{JWT: token.Data.AuthTicket.Token, Expires: token.Data.AuthTicket.Expires}, + ); err != nil { + return fmt.Errorf("saving cache: %w", err) + } + + return nil +}