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.
This commit is contained in:
Marvin Preuss 2024-04-02 07:02:59 +00:00
parent 242ee3c0c5
commit 77ad55f1eb
4 changed files with 163 additions and 22 deletions

View File

@ -1,11 +1,48 @@
package config package config
import (
"encoding"
"fmt"
"os"
)
var Cfg Config //nolint:gochecknoglobals var Cfg Config //nolint:gochecknoglobals
type Config struct { type Config struct {
Email string `env:"EMAIL,required"` Email string `env:"EMAIL,required"`
Password string `env:"PASSWORD,expand"` Password string `env:"PASSWORD,expand"`
PasswordFile string `env:"PASSWORD_FILE,expand"` PasswordFile PasswordFile `env:"PASSWORD_FILE,expand"`
CacheDir string `env:"CACHE_DIR,expand" envDefault:"/var/cache/glucose_exporter"` CacheDir string `env:"CACHE_DIR,expand" envDefault:"/var/cache/glucose_exporter"`
Debug bool `env:"DEBUG"` 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
}

View File

@ -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))
}
})
}
}

View File

@ -0,0 +1,8 @@
package config
import "errors"
var (
ErrTooManyPasswords = errors.New("too many passwords")
ErrMissingPassword = errors.New("missing password")
)

View File

@ -26,7 +26,6 @@ func Handler(w http.ResponseWriter, r *http.Request) {
vm.WritePrometheus(w, false) vm.WritePrometheus(w, false)
} }
//nolint:funlen
func glucose(ctx context.Context, w io.Writer) error { func glucose(ctx context.Context, w io.Writer) error {
c, err := cache.Load() c, err := cache.Load()
if err != nil { 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)) { if c.JWT == "" || time.Now().After(time.Time(c.Expires)) {
slog.Debug("needs a fresh token") slog.Debug("needs a fresh token")
token, err := api.Login( if err := token(ctx); err != nil {
ctx, return fmt.Errorf("refreshing token: %w", err)
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)
} }
} }
@ -101,3 +86,30 @@ func glucose(ctx context.Context, w io.Writer) error {
return nil 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
}