Skip to content

Commit

Permalink
Adding support for _FILE fallback to env.Getenv function
Browse files Browse the repository at this point in the history
Signed-off-by: Dave Henderson <[email protected]>
  • Loading branch information
hairyhenderson committed Aug 1, 2017
1 parent 20214f5 commit ee7a7cf
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 8 deletions.
23 changes: 20 additions & 3 deletions docs/content/functions/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ menu:

Exposes the [os.Getenv](https://golang.org/pkg/os/#Getenv) function.

Retrieves the value of the environment variable named by the key. If the
variable is unset, but the same variable ending in `_FILE` is set, the contents
of the file will be returned. Otherwise the provided default (or an empty
string) is returned.

This is a more forgiving alternative to using `.Env`, since missing keys will
return an empty string.
return an empty string, instead of panicking.

An optional default value can be given as well.
The `_FILE` fallback is especially useful for use with [12-factor][]-style
applications configurable only by environment variables, and especially in
conjunction with features like [Docker Secrets][].

#### Example

Expand All @@ -23,4 +30,14 @@ $ gomplate -i 'Hello, {{env.Getenv "USER"}}'
Hello, hairyhenderson
$ gomplate -i 'Hey, {{getenv "FIRSTNAME" "you"}}!'
Hey, you!
```
```

```console
$ echo "safe" > /tmp/mysecret
$ export SECRET_FILE=/tmp/mysecret
$ gomplate -i 'Your secret is {{getenv "SECRET"}}'
Your secret is safe
```

[12-factor]: https://12factor.net
[Docker Secrets]: https://docs.docker.com/engine/swarm/secrets/#build-support-for-docker-secrets-into-your-images
51 changes: 46 additions & 5 deletions env/env.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
package env

import "os"
import (
"io/ioutil"
"os"

// Getenv retrieves the value of the environment variable named by the key.
// It returns the value, or the default (or an emptry string) if the variable is
// not set.
"github.com/blang/vfs"
)

// Getenv - retrieves the value of the environment variable named by the key.
// If the variable is unset, but the same variable ending in `_FILE` is set, the
// referenced file will be read into the value.
// Otherwise the provided default (or an emptry string) is returned.
func Getenv(key string, def ...string) string {
val := os.Getenv(key)
return GetenvVFS(vfs.OS(), key, def...)
}

// GetenvVFS - a convenience function intended for internal use only!
func GetenvVFS(fs vfs.Filesystem, key string, def ...string) string {
val := getenvFile(fs, key)
if val == "" && len(def) > 0 {
return def[0]
}

return val
}

func getenvFile(fs vfs.Filesystem, key string) string {
val := os.Getenv(key)
if val != "" {
return val
}

p := os.Getenv(key + "_FILE")
if p != "" {
val, err := readFile(fs, p)
if err != nil {
return ""
}
return val
}

return ""
}

func readFile(fs vfs.Filesystem, p string) (string, error) {
f, err := fs.OpenFile(p, os.O_RDONLY, 0)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}
89 changes: 89 additions & 0 deletions env/env_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package env

import (
"errors"
"os"
"testing"

"github.com/blang/vfs"
"github.com/blang/vfs/memfs"
"github.com/stretchr/testify/assert"
)

Expand All @@ -12,3 +15,89 @@ func TestGetenv(t *testing.T) {
assert.Equal(t, os.Getenv("USER"), Getenv("USER"))
assert.Equal(t, "default value", Getenv("BLAHBLAHBLAH", "default value"))
}

func TestGetenvFile(t *testing.T) {
var fs vfs.Filesystem
fs = memfs.Create()
_ = fs.Mkdir("/tmp", 0777)
f, _ := vfs.Create(fs, "/tmp/foo")
_, _ = f.Write([]byte("foo"))

defer os.Unsetenv("FOO_FILE")
os.Setenv("FOO_FILE", "/tmp/foo")
assert.Equal(t, "foo", GetenvVFS(fs, "FOO", "bar"))

os.Setenv("FOO_FILE", "/tmp/missing")
assert.Equal(t, "bar", GetenvVFS(fs, "FOO", "bar"))

f, _ = vfs.Create(fs, "/tmp/unreadable")
fs = WriteOnly(fs)
os.Setenv("FOO_FILE", "/tmp/unreadable")
assert.Equal(t, "bar", GetenvVFS(fs, "FOO", "bar"))
}

// TODO: extract this into a separate package
// WriteOnly - represents a filesystem that's writeable, but read operations fail
func WriteOnly(fs vfs.Filesystem) vfs.Filesystem {
return &WoFS{fs}
}

type WoFS struct {
vfs.Filesystem
}

func (fs WoFS) Remove(name string) error {
return fs.Filesystem.Remove(name)
}

func (fs WoFS) Rename(oldpath, newpath string) error {
return fs.Filesystem.Rename(oldpath, newpath)
}

func (fs WoFS) Mkdir(name string, perm os.FileMode) error {
return fs.Filesystem.Mkdir(name, perm)
}

func (fs WoFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, error) {
f, err := fs.Filesystem.OpenFile(name, flag, perm)
if err != nil {
return WriteOnlyFile(f), err
}
return WriteOnlyFile(f), nil
}

func (fs WoFS) Lstat(name string) (os.FileInfo, error) {
return fs.Filesystem.Lstat(name)
}

func (fs WoFS) PathSeparator() uint8 {
return fs.Filesystem.PathSeparator()
}

func (fs WoFS) ReadDir(path string) ([]os.FileInfo, error) {
return nil, ErrWriteOnly
}

func (fs WoFS) Stat(name string) (os.FileInfo, error) {
return nil, ErrWriteOnly
}

func WriteOnlyFile(f vfs.File) vfs.File {
return &woFile{f}
}

type woFile struct {
vfs.File
}

// Write is disabled and returns ErrWriteOnly
func (f woFile) Write(p []byte) (n int, err error) {
return f.File.Write(p)
}

// Read is disabled and returns ErrWriteOnly
func (f woFile) Read([]byte) (n int, err error) {
return 0, ErrWriteOnly
}

var ErrWriteOnly = errors.New("Filesystem is write-only")

0 comments on commit ee7a7cf

Please sign in to comment.