├── .gitignore ├── README.md ├── fixtures ├── executable.tmpl └── foo.tmpl ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | reefer 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reefer 2 | *Managing a stable environment in your container.* 3 | 4 | ![refrigerated container](http://upload.wikimedia.org/wikipedia/commons/6/60/Redundantreefer.JPG) 5 | 6 | Reefer is used to render templates based on environment variables 7 | before exec'ing a given process. 8 | 9 | This is useful to configure legacy applications by environment 10 | variables to support [12factor app like configs](http://12factor.net/config). 11 | 12 | # Example: nginx + ssl certificates on Docker 13 | First we create a image with nginx and reefer using a Dockerfile like this: 14 | 15 | FROM nginx 16 | RUN curl -L https://github.com/docker-infra/reefer/releases/download/v0.0.4/reefer.gz | \ 17 | gunzip > /usr/bin/reefer && chmod a+x /usr/bin/reefer 18 | COPY templates / 19 | ENTRYPOINT [ "/usr/bin/reefer", \ 20 | "-t", "/templates/nginx.conf.tmpl:/etc/nginx/nginx.conf", \ 21 | "-t", "/templates/cert.pem.tmpl:/etc/nginx/cert.pem", \ 22 | "-t", "/templates/key.pem.tmpl:/etc/nginx/key.pem", \ 23 | "-t", "/templates/htpasswd.tmpl:/etc/nginx/htpasswd", \ 24 | "/usr/bin/nginx", "-g", "daemon off;" 25 | ] 26 | 27 | The files in the templates/ directory would look something like this: 28 | 29 | cert.pem.tmpl: 30 | 31 | {{ .Env "TLS_CERT" }} 32 | 33 | 34 | key.pem.tmpl: 35 | 36 | {{ .Env "TLS_KEY" }} 37 | 38 | 39 | nginx.conf.tmpl: 40 | 41 | http { 42 | server { 43 | listen 443; 44 | 45 | ssl on; 46 | ssl_certificate /etc/nginx/cert.pem; 47 | ssl_certificate_key /etc/nginx/key.pem; 48 | 49 | server_name {{ .Env "DOMAIN" }}; 50 | location / { 51 | auth_basic "secret"; 52 | auth_basic_user_file /etc/nginx/htpasswd; 53 | 54 | root /srv/www/htdocs; 55 | index index.html; 56 | } 57 | } 58 | } 59 | 60 | 61 | htpasswd.tmpl: 62 | 63 | alice:{{ .Env "PASS_ALICE" }} 64 | bob:{{ .Env "PASS_BOB" }} 65 | 66 | 67 | Now you can start the image like this: 68 | 69 | $ docker run -e TLS_CERT=`cat your-cert.pem` -e TLS_KEY=`cat your-key.pem` \ 70 | -e DOMAIN=example.com -e PASS_ALICE=foobar23 -e PASS_BOB=blafasel -p 443:443 your-image 71 | 72 | Reefer will read the environment variables and render the templates. 73 | After that, it will exec() the remaining parameter (nginx -g daemon off; in this example). 74 | 75 | # Passing environment variable through to your application 76 | 77 | By default, reefer will not "pass through" environment variables you set to the application it executes; they will be available for the templates, but not the application. Notable exceptions: 78 | 79 | COLORS 80 | DISPLAY 81 | HOME 82 | HOSTNAME 83 | KRB5CCNAME 84 | LS_COLORS 85 | PATH 86 | PS1 87 | PS2 88 | TZ 89 | XAUTHORITY 90 | XAUTHORIZATION 91 | 92 | This is done for security reasons because one of the primary uses of reefer is to pass sensitive information (private keys, etc.) used in the generation of your templates. It is generally a good idea to not have these environment variables "floating around" in the container environment. If you would like to pass through environment variables to other applications in your container, you can specify individual environment variables to "keep" with `-e` like so: 93 | 94 | ENTRYPOINT [ "/usr/bin/reefer", \ 95 | "-t", "/templates/app.conf.tmpl:/app/etc/app.conf", \ 96 | "-t", "/templates/cert.pem.tmpl:/app/certs/cert.pem", \ 97 | "-t", "/templates/key.pem.tmpl:/app/certs/key.pem", \ 98 | "-e", "IMPORTANT_CONFIG_VAR", \ 99 | "/app/app" 100 | ] 101 | 102 | You can pass ALL environment variables through with `-E`: 103 | 104 | ENTRYPOINT [ "/usr/bin/reefer", \ 105 | "-t", "/templates/app.conf.tmpl:/app/etc/app.conf", \ 106 | "-E", \ 107 | "/app/app" 108 | ] 109 | 110 | -------------------------------------------------------------------------------- /fixtures/executable.tmpl: -------------------------------------------------------------------------------- 1 | Hello bar 2 | -------------------------------------------------------------------------------- /fixtures/foo.tmpl: -------------------------------------------------------------------------------- 1 | Hello {{ .Env "FOO" }} 2 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | "syscall" 13 | "text/template" 14 | ) 15 | 16 | const templateSuffix = ".tmpl" 17 | 18 | type list []string 19 | 20 | func (l *list) String() string { 21 | return "" 22 | } 23 | 24 | func (l *list) Set(str string) error { 25 | *l = append(*l, str) 26 | return nil 27 | } 28 | 29 | type templateData struct { 30 | } 31 | 32 | func (td templateData) Env(str string) string { 33 | return os.Getenv(str) 34 | } 35 | 36 | type templateList map[string]target 37 | 38 | type target struct { 39 | template *template.Template 40 | info os.FileInfo 41 | } 42 | 43 | func (tl templateList) String() string { 44 | return "" 45 | } 46 | 47 | func (tl templateList) Set(str string) error { 48 | parts := strings.SplitN(str, ":", 2) 49 | stat, err := os.Stat(parts[0]) 50 | if err != nil { 51 | return err 52 | } 53 | t, err := template.ParseFiles(parts[0]) 54 | if err != nil { 55 | return err 56 | } 57 | dest := "" 58 | if len(parts) == 2 { 59 | dest = parts[1] 60 | } else { 61 | dest = strings.TrimSuffix(parts[0], templateSuffix) 62 | } 63 | tl[dest] = target{ 64 | template: t, 65 | info: stat, 66 | } 67 | return nil 68 | } 69 | 70 | func (tl templateList) Render(root string) error { 71 | for d, t := range tl { 72 | dest := d 73 | if !path.IsAbs(d) { 74 | dest = path.Join(root, d) 75 | } 76 | data := templateData{} 77 | if err := os.MkdirAll(filepath.Dir(dest), 0700); err != nil { 78 | return fmt.Errorf("Couldn't mkdir %s: %s", filepath.Dir(dest), err) 79 | } 80 | fh, err := os.Create(dest) 81 | if err != nil { 82 | return fmt.Errorf("Couldn't create %s: %s", dest, err) 83 | } 84 | if err := fh.Chmod(t.info.Mode()); err != nil { 85 | return err 86 | } 87 | if err := t.template.Execute(fh, data); err != nil { 88 | return err 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | func getFilteredEnv(keep []string) (env []string) { 95 | for _, k := range keep { 96 | v := os.Getenv(k) 97 | if v == "" { 98 | continue 99 | } 100 | env = append(env, fmt.Sprintf("%s=%s", k, v)) 101 | } 102 | return env 103 | } 104 | 105 | var ( 106 | execEnv = []string{} 107 | keepAllEnvs = flag.Bool("E", false, "Keep all environment variables") 108 | keepEnvs = list{ 109 | "COLORS", 110 | "DISPLAY", 111 | "HOME", 112 | "HOSTNAME", 113 | "KRB5CCNAME", 114 | "LS_COLORS", 115 | "PATH", 116 | "PS1", 117 | "PS2", 118 | "TZ", 119 | "XAUTHORITY", 120 | "XAUTHORIZATION", 121 | } 122 | root = flag.String("r", "", "Root for relative paths") 123 | ) 124 | 125 | func main() { 126 | templates := templateList{} 127 | flag.Var(&templates, "t", "Specify template and append optional destination after collons. Format: foo.tmpl:/etc/foo.conf") 128 | flag.Var(&keepEnvs, "e", fmt.Sprintf("Keep specified environment variables beside %s", strings.Join(keepEnvs, ","))) 129 | flag.Parse() 130 | if *root == "" { 131 | r, err := os.Getwd() 132 | if err != nil { 133 | log.Fatal("Not root (-r) specified and couldn't get working directory") 134 | } 135 | *root = r 136 | } 137 | args := flag.Args() 138 | if len(args) == 0 { 139 | log.Fatal("No command provided, exiting") 140 | } 141 | 142 | if err := templates.Render(*root); err != nil { 143 | log.Fatal(err) 144 | } 145 | path, err := exec.LookPath(args[0]) 146 | if err != nil { 147 | log.Fatal(err) 148 | } 149 | if *keepAllEnvs { 150 | execEnv = os.Environ() 151 | } else { 152 | execEnv = getFilteredEnv(keepEnvs) 153 | } 154 | if err := syscall.Exec(path, args, execEnv); err != nil { 155 | log.Fatal(err) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | expectedContent = "Hello bar\n" 12 | ) 13 | 14 | func TestExplicitConf(t *testing.T) { 15 | templates := templateList{} 16 | os.Setenv("FOO", "bar") 17 | if err := templates.Set("fixtures/foo.tmpl:etc/foo.conf"); err != nil { 18 | t.Fatal(err) 19 | } 20 | if err := os.Remove(test(t, templates, "/etc/foo.conf")); err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestImplicitConf(t *testing.T) { 26 | templates := templateList{} 27 | os.Setenv("FOO", "bar") 28 | if err := templates.Set("fixtures/foo.tmpl"); err != nil { 29 | t.Fatal(err) 30 | } 31 | if err := os.Remove(test(t, templates, "/fixtures/foo")); err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func test(t *testing.T, templates templateList, dest string) string { 37 | testRoot, err := ioutil.TempDir("/tmp", "test-reefer") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | if err := templates.Render(testRoot); err != nil { 42 | t.Fatal(err) 43 | } 44 | file := filepath.Join(testRoot, dest) 45 | content, err := ioutil.ReadFile(file) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if string(content) != expectedContent { 50 | t.Fatal("Unexpected content: ", content) 51 | } 52 | return file 53 | } 54 | 55 | func TestFilterEnv(t *testing.T) { 56 | keep := []string{"FOO", "PATH"} 57 | os.Setenv("FOO", "bar") 58 | os.Setenv("PATH", "/bin:/usr/bin") 59 | os.Setenv("FILTERME", "gone") 60 | envs := getFilteredEnv(keep) 61 | 62 | if !isIn("FOO=bar", envs) || !isIn("PATH=/bin:/usr/bin", envs) || isIn("FILTERME=gone", envs) { 63 | t.Fatal("Unexpected env ", envs) 64 | } 65 | } 66 | 67 | func isIn(str string, list []string) bool { 68 | for _, i := range list { 69 | if i == str { 70 | return true 71 | } 72 | } 73 | return false 74 | } 75 | 76 | func TestKeepMode(t *testing.T) { 77 | templates := templateList{} 78 | ofi, err := os.Stat("fixtures/executable.tmpl") 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | if err := templates.Set("fixtures/executable.tmpl"); err != nil { 84 | t.Fatal(err) 85 | } 86 | file := test(t, templates, "/fixtures/executable") 87 | fi, err := os.Stat(file) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if fi.Mode() != ofi.Mode() { 93 | t.Fatal("Unexpected mode") 94 | } 95 | } 96 | --------------------------------------------------------------------------------