From 482f216c924848f418d4d51e30b3123038e86bb5 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Fri, 10 Oct 2014 11:15:50 -0600 Subject: [PATCH] Initial commit --- GLOCKFILE | 3 + Makefile | 21 +++++++ README.md | 95 +++++++++++++++++++++++++++++++ dockerize.go | 94 +++++++++++++++++++++++++++++++ examples/nginx/Dockerfile | 20 +++++++ examples/nginx/default.tmpl | 18 ++++++ exec.go | 20 +++++++ tail.go | 25 +++++++++ template.go | 108 ++++++++++++++++++++++++++++++++++++ 9 files changed, 404 insertions(+) create mode 100644 GLOCKFILE create mode 100644 Makefile create mode 100644 README.md create mode 100644 dockerize.go create mode 100644 examples/nginx/Dockerfile create mode 100644 examples/nginx/default.tmpl create mode 100644 exec.go create mode 100644 tail.go create mode 100644 template.go diff --git a/GLOCKFILE b/GLOCKFILE new file mode 100644 index 0000000..d2e9c76 --- /dev/null +++ b/GLOCKFILE @@ -0,0 +1,3 @@ +github.com/ActiveState/tail 068b72961a6bc5b4a82cf4fc14ccc724c0cfa73a +github.com/howeyc/fsnotify 6b1ef893dc11e0447abda6da20a5203481878dda +gopkg.in/tomb.v1 c131134a1947e9afd9cecfe11f4c6dff0732ae58 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7dd3637 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.SILENT : +.PHONY : dockerize clean fmt + +TAG:=`git describe --abbrev=0 --tags` +LDFLAGS:=-X main.buildVersion $(TAG) + +all: dockerize + +dockerize: + echo "Building dockerize" + go install -ldflags "$(LDFLAGS)" + +dist-clean: + rm -rf dist + rm -f dockerize-linux-*.tar.gz + +dist: dist-clean + mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/linux/amd64/dockerize + +release: dist + tar -cvzf dockerize-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 dockerize diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6a6378 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +dockerize +============= + +Utility to simplify running applications in docker containers. + +dockerize is a utility to simplify running applications in docker containers. It allows you +to generate application configuration files at container startup time from templates and +container environment variables. It also allows log files to be tailed to stdout and/or +stderr. + +The typical use case for dockerize is when you have an application that has one or more +configuration files and you would like to control some of the values using environment variables. + +For example, a Python application using Sqlalchemy may be able to use environment variables directly. +It may require that the database URL be read from a python settings file with a variable named +`SQLALCHEMY_DATABASE_URI`. dockerize allows you to set an environment variable such as +`DATABASE_URL` and update the python file when the container starts. + +Another use case is when the application logs to specific files on the filesystem and not stdout +or stderr. This makes it difficult to troubleshoot the container using the `docker logs` command. +For example, nginx will log to `/var/log/nginx/access.log' and +'/var/log/nginx/error.log' by default. While you can sometimes work around this, it's tedious to find +the a solution for every application. dockerize allows you to specify which logs files should +be tailed and where they should be sent. + + +## Installation + +Download the latest version in your container: + +* [linux/amd64](https://github.com/jwilder/dockerize/releases/download/v0.0.1/dockerize-linux-amd64-v0.0.1.tar.gz) + +For Ubuntu Images: + +``` +RUN apt-get update && apt-get install -y wget +RUN wget https://github.com/jwilder/dockerize/releases/download/v0.0.1/dockerize-linux-amd64-v0.0.1.tar.gz +RUN tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.0.1.tar.gz +``` + +## Usage + +dockerize works by wrapping the call to your application using the `ENTRYPOINT` or `CMD` directives. + +This would generate `/etc/nginx/nginx.conf` from the template located at `/etc/nginx/nginx.tmpl` and +send `/var/log/nginx/access.log' to `STDOUT` and `/var/log/nginx/error.log` to `STDERR` after running +`nginx`. + +``` +CMD dockerize -template /etc/nginx/nginx.tmpl:/etc/nginx/nginx.conf -stdout /var/log/nginx/access.log -stderr /var/log/nginx/error.log nginx +``` + +### Command-line Options + +You can specify multiple template by passing using `-template` multiple times: + +``` +$ dockerize -template template1.tmpl:file1.cfg -template template2.tmpl:file3 + +``` + +You can tail multiple files to `STDOUT` and `STDERR` by passing the options multiple times. + +``` +$ dockerize -stdout info.log -stdout perf.log + +``` + +If your file uses `{{` and `}}` as part of it's syntax, you can change the template escape characters using the `-delims`. + +``` +$ dockerize -delims "<%:%>" +``` + +## Using Templates + +Templates use Golang [text/template](http://golang.org/pkg/text/template/). You can access environment +variables within a template with `.Env`. + +``` +{{ .Env.PATH }} is my path +``` + +There are a few built in functions as well: + + * `default` - Returns a default value for one that does not exist + * `contains` - Returns true if a string is within another string + * `exists` - Determines if a file path exists or not + * `split` - Splits a string into an array using a separator string + * `replace` - Replaces all occurences of a string within another string + * `parseUrl`- Parses a URL into it's protocol, scheme, host, etc. parts. + +## License + +MIT diff --git a/dockerize.go b/dockerize.go new file mode 100644 index 0000000..ab1366f --- /dev/null +++ b/dockerize.go @@ -0,0 +1,94 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strings" + "sync" +) + +type sliceVar []string + +type Context struct { +} + +func (c *Context) Env() map[string]string { + env := make(map[string]string) + for _, i := range os.Environ() { + sep := strings.Index(i, "=") + env[i[0:sep]] = i[sep+1:] + } + return env +} + +var ( + buildVersion string + version bool + wg sync.WaitGroup + + templatesFlag sliceVar + stdoutTailFlag sliceVar + stderrTailFlag sliceVar + delimsFlag string + delims []string +) + +func (s *sliceVar) Set(value string) error { + *s = append(*s, value) + return nil +} + +func (s *sliceVar) String() string { + return strings.Join(*s, ",") +} + +func main() { + + flag.BoolVar(&version, "version", false, "show version") + flag.Var(&templatesFlag, "template", "Template (/template:/dest). Can be passed multiple times") + flag.Var(&stdoutTailFlag, "stdout", "Tails a file to stdout. Can be passed multiple times") + flag.Var(&stderrTailFlag, "stderr", "Tails a file to stderr. Can be passed multiple times") + flag.StringVar(&delimsFlag, "delims", "", `template tag delimiters. default "{{":"}}" `) + + flag.Parse() + + if version { + fmt.Println(buildVersion) + return + } + + if flag.NArg() == 0 { + log.Fatalln("no command specified") + } + + if delimsFlag != "" { + delims = strings.Split(delimsFlag, ":") + if len(delims) != 2 { + log.Fatalf("bad delimiters argument: %s. expected \"left:right\"", delimsFlag) + } + } + for _, t := range templatesFlag { + parts := strings.Split(t, ":") + if len(parts) != 2 { + log.Fatalf("bad template argument: %s. expected \"/template:/dest\"", t) + } + generateFile(parts[0], parts[1]) + } + + wg.Add(1) + go runCmd(flag.Arg(0), flag.Args()[1:]...) + + for _, out := range stdoutTailFlag { + wg.Add(1) + go tailFile(out, os.Stdout) + } + + for _, err := range stderrTailFlag { + wg.Add(1) + go tailFile(err, os.Stderr) + } + + wg.Wait() +} diff --git a/examples/nginx/Dockerfile b/examples/nginx/Dockerfile new file mode 100644 index 0000000..496218b --- /dev/null +++ b/examples/nginx/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu:14.04 +MAINTAINER Jason Wilder jwilder@litl.com + +# Install Nginx. +RUN echo "deb http://ppa.launchpad.net/nginx/stable/ubuntu trusty main" > /etc/apt/sources.list.d/nginx-stable-trusty.list +RUN echo "deb-src http://ppa.launchpad.net/nginx/stable/ubuntu trusty main" >> /etc/apt/sources.list.d/nginx-stable-trusty.list +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C +RUN apt-get update +RUN apt-get install -y wget nginx + +RUN wget https://github.com/jwilder/dockerize/releases/download/v0.0.1/dockerize-linux-amd64-v0.0.1.tar.gz +RUN tar -C /usr/local/bin -xvzf dockerize-linux-amd64-v0.0.1.tar.gz + +RUN echo "daemon off;" >> /etc/nginx/nginx.conf + +ADD default.tmpl /etc/nginx/sites-available/default.tmpl + +EXPOSE 80 + +CMD dockerize -template /etc/nginx/sites-available/default.tmpl:/etc/nginx/sites-available/default -stdout /var/log/nginx/access.log -stderr /var/log/nginx/error.log nginx diff --git a/examples/nginx/default.tmpl b/examples/nginx/default.tmpl new file mode 100644 index 0000000..26cef2f --- /dev/null +++ b/examples/nginx/default.tmpl @@ -0,0 +1,18 @@ +server { + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + + root /usr/share/nginx/html; + index index.html index.htm; + + # Make site accessible from http://localhost/ + server_name localhost; + + location / { + access_log off; + proxy_pass {{ .Env.PROXY_URL }}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/exec.go b/exec.go new file mode 100644 index 0000000..75ca82f --- /dev/null +++ b/exec.go @@ -0,0 +1,20 @@ +package main + +import ( + "log" + "os" + "os/exec" +) + +func runCmd(cmd string, args ...string) { + + //FIXME: forward signals + process := exec.Command(cmd, args...) + process.Stdin = os.Stdin + process.Stdout = os.Stdout + process.Stderr = os.Stderr + err := process.Run() + if err != nil { + log.Fatalf("error running command: %s, %s\n", cmd, err) + } +} diff --git a/tail.go b/tail.go new file mode 100644 index 0000000..3940163 --- /dev/null +++ b/tail.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/ActiveState/tail" +) + +func tailFile(file string, dest *os.File) { + defer wg.Done() + t, err := tail.TailFile(file, tail.Config{ + Follow: true, + ReOpen: true, + //Poll: true, + Logger: tail.DiscardingLogger, + }) + if err != nil { + log.Fatalf("unable to tail %s: %s", "foo", err) + } + for line := range t.Lines { + fmt.Fprintln(dest, line.Text) + } +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..65423cd --- /dev/null +++ b/template.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "log" + "net/url" + "os" + "path/filepath" + "strings" + "syscall" + "text/template" +) + +func exists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func contains(item map[string]string, key string) bool { + if _, ok := item[key]; ok { + return true + } + return false +} + +func defaultValue(args ...interface{}) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("default called with no values!") + } + + if len(args) > 0 { + if args[0] != nil { + return args[0].(string), nil + } + } + + if len(args) > 1 { + if args[1] == nil { + return "", fmt.Errorf("default called with nil default value!") + } + + if _, ok := args[1].(string); !ok { + return "", fmt.Errorf("default is not a string value. hint: surround it w/ double quotes.") + } + + return args[1].(string), nil + } + + return "", fmt.Errorf("default called with no default value") +} + +func parseUrl(rawurl string) *url.URL { + u, err := url.Parse(rawurl) + if err != nil { + log.Fatalf("unable to parse url %s: %s", rawurl, err) + } + return u +} + +func generateFile(templatePath, destPath string) bool { + tmpl := template.New(filepath.Base(templatePath)).Funcs(template.FuncMap{ + "contains": contains, + "exists": exists, + "split": strings.Split, + "replace": strings.Replace, + "default": defaultValue, + "parseUrl": parseUrl, + }) + + if len(delims) > 0 { + tmpl = tmpl.Delims(delims[0], delims[1]) + } + tmpl, err := tmpl.ParseFiles(templatePath) + if err != nil { + log.Fatalf("unable to parse template: %s", err) + } + + dest := os.Stdout + if destPath != "" { + dest, err = os.Create(destPath) + if err != nil { + log.Fatalf("unable to create %s", err) + } + defer dest.Close() + } + + err = tmpl.ExecuteTemplate(dest, filepath.Base(templatePath), &Context{}) + if err != nil { + log.Fatalf("template error: %s\n", err) + } + + if fi, err := os.Stat(destPath); err == nil { + if err := dest.Chmod(fi.Mode()); err != nil { + log.Fatalf("unable to chmod temp file: %s\n", err) + } + if err := dest.Chown(int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)); err != nil { + log.Fatalf("unable to chown temp file: %s\n", err) + } + } + + return true +}