Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow reading secrets from files #20

Merged
merged 3 commits into from
Jan 18, 2025
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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ email:

You can use **environment variables** instead. For example:
```sh
export BEACON_SMTP_PASSWORD="your-password"
export BEACON_EMAIL_SMTP_PASSWORD="your-password"
```

For password, you can instead provide a file containing the password.
```sh
export BEACON_EMAIL_SMTP_PASSWORD_FILE="/path/to/password-file"
```

### Configuration sources
Expand Down Expand Up @@ -276,4 +281,4 @@ go tool pprof -http=:8080 ./scheduler.test block.out
# with trace, open "Goroutines" on the webpage
go test -trace=trace.out ./scheduler
go tool trace trace.out
```
```
43 changes: 24 additions & 19 deletions conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"log"
"os"
"path/filepath"
"reflect"
"strings"
"time"

"github.com/caarlos0/env/v11"
Expand All @@ -19,7 +19,8 @@ import (
const ENV_VAR_PREFIX = "BEACON_"

type Secret struct {
value string
Value string `env:""`
FromFile string `env:"_FILE,file"`
}
Comment on lines 21 to 24
Copy link
Contributor Author

@Gregofi Gregofi Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make this struct as reusable as possible. If another secret was added in the future, it can be reused without any hassle.

The reason for env:"" (and not env:"PASSWORD") is that it doesn't lock into naming (for example if authentication token was ever added, it can be named LOGIN_TOKEN, not something ending with PASSWORD).

The drawback of this solution is that the fields are now public. They were accessible before via the getter, but now one must be even more careful to not print them (for example by explicitly implementing serializers, as below).


func (s Secret) String() string {
Expand All @@ -31,23 +32,28 @@ func (s Secret) GoString() string {
}

func (s *Secret) Get() string {
return s.value
if s.FromFile != "" {
// File contents tend to end with newline
return strings.TrimSpace(s.FromFile)
} else {
return s.Value
}
}

func (s *Secret) IsSet() bool {
return s.value != ""
return s.Value != "" || s.FromFile != ""
}

type EmailConfig struct {
SmtpServer string `yaml:"smtp_server" env:"EMAIL_SMTP_SERVER"`
SmtpPort int `yaml:"smtp_port" env:"EMAIL_SMTP_PORT"`
SmtpUsername string `yaml:"smtp_username" env:"EMAIL_SMTP_USERNAME"`
SmtpPassword Secret `yaml:"smtp_password" env:"EMAIL_SMTP_PASSWORD"`
SendTo string `yaml:"send_to" env:"EMAIL_SEND_TO"`
Sender string `yaml:"sender" env:"EMAIL_SENDER"`
Prefix string `yaml:"prefix" env:"EMAIL_PREFIX"`
SmtpServer string `yaml:"smtp_server" env:"SMTP_SERVER"`
SmtpPort int `yaml:"smtp_port" env:"SMTP_PORT"`
SmtpUsername string `yaml:"smtp_username" env:"SMTP_USERNAME"`
SmtpPassword Secret `yaml:"smtp_password" envPrefix:"SMTP_PASSWORD"`
SendTo string `yaml:"send_to" env:"SEND_TO"`
Sender string `yaml:"sender" env:"SENDER"`
Prefix string `yaml:"prefix" env:"PREFIX"`
// not bool to allow more flexible usage
Enabled string `yaml:"enabled" env:"EMAIL_ENABLED"`
Enabled string `yaml:"enabled" env:"ENABLED"`
}

func (emailConf *EmailConfig) IsEnabled() bool {
Expand All @@ -66,10 +72,14 @@ func (s *Secret) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.ScalarNode {
return fmt.Errorf("expected scalar node got %s", node.Value)
}
s.value = node.Value
s.Value = node.Value
return nil
}

func (s Secret) MarshalYAML() (interface{}, error) {
return "*****", nil
}

// TzLocation wraps a *time.Location so we can provide custom YAML unmarshalling.
type TzLocation struct {
Location *time.Location
Expand All @@ -86,7 +96,7 @@ type Config struct {
Port int `yaml:"port" env:"PORT"`
SchedulerPeriod time.Duration `yaml:"scheduler_period" env:"SCHEDULER_PERIOD"`

EmailConf EmailConfig `yaml:"email"`
EmailConf EmailConfig `yaml:"email" envPrefix:"EMAIL_"`

Services ServicesList

Expand Down Expand Up @@ -216,11 +226,6 @@ func ConfigFromBytes(data []byte) (*Config, error) {
}
err = env.ParseWithOptions(config, env.Options{
Prefix: ENV_VAR_PREFIX,
FuncMap: map[reflect.Type]env.ParserFunc{
reflect.TypeOf(Secret{}): func(v string) (interface{}, error) {
return Secret{v}, nil
},
},
})
if err != nil {
return nil, err
Expand Down
28 changes: 26 additions & 2 deletions conf/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ services:
}

func TestSecretPrint(t *testing.T) {
secret := Secret{"Greg"}
secret := Secret{"Greg", ""}
assert.Equal(t, secret.Get(), "Greg")
assert.NotContains(t, fmt.Sprint(secret), "Greg")
assert.NotContains(t, fmt.Sprint(&secret), "Greg")
Expand Down Expand Up @@ -143,7 +143,7 @@ prefix: "[test]"
"mail.smtp2go.com",
587,
"beacon",
Secret{"h4xor"},
Secret{"h4xor", ""},
"you@example.fake",
"noreply@example.fake",
"[test]",
Expand All @@ -160,3 +160,27 @@ func TestSecretFromEnv(t *testing.T) {
require.Equal(t, "secr4t", conf.EmailConf.SmtpPassword.Get())

}

func TestSecretFromFile(t *testing.T) {
// Create the file
f, err := os.Create("secret-test.txt")
require.NoError(t, err)
_, err = f.WriteString("foo\n")
require.NoError(t, err)

// Set both, file should have prio
err = os.Setenv("BEACON_EMAIL_SMTP_PASSWORD", "bar")
require.NoError(t, err)
err = os.Setenv("BEACON_EMAIL_SMTP_PASSWORD_FILE", "secret-test.txt")
require.NoError(t, err)

conf, err := ConfigFromBytes([]byte(""))
require.NoError(t, err)
require.Equal(t, "foo", conf.EmailConf.SmtpPassword.Get())

// Cleanup
err = os.Unsetenv("BEACON_EMAIL_SMTP_PASSWORD_FILE")
require.NoError(t, err)
err = os.Remove("secret-test.txt")
require.NoError(t, err)
}
Loading