├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── examples ├── json │ └── json-example └── nginx │ ├── Dockerfile │ └── default.tmpl ├── exec.go ├── go.mod ├── go.sum ├── main.go ├── tail.go └── template.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.23 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Run tests 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go template 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | .idea 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | # Created by .ignore support plugin (hsz.mobi) 29 | 30 | dockerize 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 AS binary 2 | 3 | WORKDIR /go/src/github.com/jwilder/dockerize 4 | COPY *.go go.* /go/src/github.com/jwilder/dockerize/ 5 | 6 | ENV GO111MODULE=on 7 | RUN go mod tidy 8 | 9 | RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -o /go/bin/dockerize . 10 | 11 | FROM gcr.io/distroless/static:nonroot 12 | LABEL MAINTAINER="Jason Wilder " 13 | 14 | USER nonroot:nonroot 15 | COPY --from=binary /go/bin/dockerize /bin/dockerize 16 | 17 | ENTRYPOINT ["/bin/dockerize"] 18 | CMD ["--help"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2018 Jason Wilder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | .PHONY : dockerize clean fmt 3 | 4 | TAG:=`git describe --abbrev=0 --tags` 5 | LDFLAGS:=-X main.buildVersion=$(TAG) 6 | GO111MODULE:=on 7 | 8 | all: dockerize 9 | 10 | deps: 11 | go mod tidy 12 | 13 | dockerize: 14 | echo "Building dockerize" 15 | go install -ldflags "$(LDFLAGS)" 16 | 17 | dist-clean: 18 | rm -rf dist 19 | rm -f dockerize-*.tar.gz 20 | 21 | dist: deps dist-clean 22 | mkdir -p dist/alpine-linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -a -tags netgo -installsuffix netgo -o dist/alpine-linux/amd64/dockerize 23 | mkdir -p dist/alpine-linux/ppc64le && GOOS=linux GOARCH=ppc64le go build -ldflags "$(LDFLAGS)" -a -tags netgo -installsuffix netgo -o dist/alpine-linux/ppc64le/dockerize 24 | mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/linux/amd64/dockerize 25 | mkdir -p dist/linux/386 && GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o dist/linux/386/dockerize 26 | mkdir -p dist/linux/armel && GOOS=linux GOARCH=arm GOARM=5 go build -ldflags "$(LDFLAGS)" -o dist/linux/armel/dockerize 27 | mkdir -p dist/linux/armhf && GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -o dist/linux/armhf/dockerize 28 | mkdir -p dist/linux/arm64 && GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o dist/linux/arm64/dockerize 29 | mkdir -p dist/linux/ppc64le && GOOS=linux GOARCH=ppc64le go build -ldflags "$(LDFLAGS)" -o dist/linux/ppc64le/dockerize 30 | mkdir -p dist/darwin/amd64 && GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/darwin/amd64/dockerize 31 | mkdir -p dist/darwin/amd64 && GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o dist/darwin/arm64/dockerize 32 | mkdir -p dist/linux/s390x && GOOS=linux GOARCH=s390x go build -ldflags "$(LDFLAGS)" -o dist/linux/s390x/dockerize 33 | 34 | release: dist 35 | tar -cvzf dockerize-alpine-linux-amd64-$(TAG).tar.gz -C dist/alpine-linux/amd64 dockerize 36 | tar -cvzf dockerize-alpine-linux-ppc64le-$(TAG).tar.gz -C dist/alpine-linux/ppc64le dockerize 37 | tar -cvzf dockerize-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 dockerize 38 | tar -cvzf dockerize-linux-386-$(TAG).tar.gz -C dist/linux/386 dockerize 39 | tar -cvzf dockerize-linux-armel-$(TAG).tar.gz -C dist/linux/armel dockerize 40 | tar -cvzf dockerize-linux-armhf-$(TAG).tar.gz -C dist/linux/armhf dockerize 41 | tar -cvzf dockerize-linux-arm64-$(TAG).tar.gz -C dist/linux/arm64 dockerize 42 | tar -cvzf dockerize-linux-ppc64le-$(TAG).tar.gz -C dist/linux/ppc64le dockerize 43 | tar -cvzf dockerize-darwin-amd64-$(TAG).tar.gz -C dist/darwin/amd64 dockerize 44 | tar -cvzf dockerize-darwin-arm64-$(TAG).tar.gz -C dist/darwin/arm64 dockerize 45 | tar -cvzf dockerize-linux-s390x-$(TAG).tar.gz -C dist/linux/s390x dockerize 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dockerize ![version v0.9.3](https://img.shields.io/badge/version-v0.9.3-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) 2 | ============= 3 | 4 | Utility to simplify running applications in docker containers. 5 | 6 | dockerize is a utility to simplify running applications in docker containers. It allows you to: 7 | * generate application configuration files at container startup time from templates and container environment variables 8 | * Tail multiple log files to stdout and/or stderr 9 | * Wait for other services to be available using TCP, HTTP(S), unix before starting the main process. 10 | 11 | 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. 12 | 13 | For example, a Python application using Sqlalchemy might not be able to use environment variables directly. 14 | It may require that the database URL be read from a python settings file with a variable named 15 | `SQLALCHEMY_DATABASE_URI`. dockerize allows you to set an environment variable such as 16 | `DATABASE_URL` and update the python file when the container starts. 17 | In addition, it can also delay the starting of the python application until the database container is running and listening on the TCP port. 18 | 19 | Another use case is when the application logs to specific files on the filesystem and not stdout 20 | or stderr. This makes it difficult to troubleshoot the container using the `docker logs` command. 21 | For example, nginx will log to `/var/log/nginx/access.log` and 22 | `/var/log/nginx/error.log` by default. While you can sometimes work around this, it's tedious to find a solution for every application. dockerize allows you to specify which logs files should be tailed and where they should be sent. 23 | 24 | See [A Simple Way To Dockerize Applications](http://jasonwilder.com/blog/2014/10/13/a-simple-way-to-dockerize-applications/) 25 | 26 | 27 | ## Installation 28 | 29 | Download the latest version in your container: 30 | 31 | * [linux/amd64](https://github.com/jwilder/dockerize/releases/download/v0.9.3/dockerize-linux-amd64-v0.9.3.tar.gz) 32 | * [alpine/amd64](https://github.com/jwilder/dockerize/releases/download/v0.9.3/dockerize-alpine-linux-amd64-v0.9.3.tar.gz) 33 | * [darwin/amd64](https://github.com/jwilder/dockerize/releases/download/v0.9.3/dockerize-darwin-amd64-v0.9.3.tar.gz) 34 | 35 | 36 | ### Docker Base Image 37 | 38 | The `jwilder/dockerize` image is a base image based on `gcr.io/distroless/static`. `dockerize` is installed in the `$PATH` and can be used directly. 39 | 40 | ``` 41 | FROM jwilder/dockerize 42 | ... 43 | ENTRYPOINT dockerize ... 44 | ``` 45 | 46 | ### Ubuntu Images 47 | 48 | ``` Dockerfile 49 | ENV DOCKERIZE_VERSION v0.9.3 50 | 51 | RUN apt-get update \ 52 | && apt-get install -y wget \ 53 | && wget -O - https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz | tar xzf - -C /usr/local/bin \ 54 | && apt-get autoremove -yqq --purge wget && rm -rf /var/lib/apt/lists/* 55 | ``` 56 | 57 | 58 | ### For Alpine Images: 59 | 60 | ``` Dockerfile 61 | ENV DOCKERIZE_VERSION v0.9.3 62 | 63 | RUN apk update --no-cache \ 64 | && apk add --no-cache wget openssl \ 65 | && wget -O - https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz | tar xzf - -C /usr/local/bin \ 66 | && apk del wget 67 | ``` 68 | 69 | ## Usage 70 | 71 | dockerize works by wrapping the call to your application using the `ENTRYPOINT` or `CMD` directives. 72 | 73 | This would generate `/etc/nginx/nginx.conf` from the template located at `/etc/nginx/nginx.tmpl` and 74 | send `/var/log/nginx/access.log` to `STDOUT` and `/var/log/nginx/error.log` to `STDERR` after running 75 | `nginx`, only after waiting for the `web` host to respond on `tcp 8000`: 76 | 77 | ``` Dockerfile 78 | CMD dockerize -template /etc/nginx/nginx.tmpl:/etc/nginx/nginx.conf -stdout /var/log/nginx/access.log -stderr /var/log/nginx/error.log -wait tcp://web:8000 nginx 79 | ``` 80 | 81 | ### Command-line Options 82 | 83 | You can specify multiple templates by passing using `-template` multiple times: 84 | 85 | ``` 86 | $ dockerize -template template1.tmpl:file1.cfg -template template2.tmpl:file3 87 | 88 | ``` 89 | 90 | Templates can be generated to `STDOUT` by not specifying a dest: 91 | 92 | ``` 93 | $ dockerize -template template1.tmpl 94 | 95 | ``` 96 | 97 | Template may also be a directory. In this case all files within this directory are processed as template and stored with the same name in the destination directory. 98 | If the destination directory is omitted, the output is sent to `STDOUT`. The files in the source directory are processed in sorted order (as returned by `ioutil.ReadDir`). 99 | 100 | ``` 101 | $ dockerize -template src_dir:dest_dir 102 | 103 | ``` 104 | 105 | If the destination file already exists, dockerize will overwrite it. The -no-overwrite flag overrides this behaviour. 106 | 107 | ``` 108 | $ dockerize -no-overwrite -template template1.tmpl:file 109 | ``` 110 | 111 | You can tail multiple files to `STDOUT` and `STDERR` by passing the options multiple times. 112 | 113 | ``` 114 | $ dockerize -stdout info.log -stdout perf.log 115 | 116 | ``` 117 | 118 | If `inotify` does not work in your container, you can use `-poll` to poll for file changes instead. 119 | 120 | ``` 121 | $ dockerize -stdout info.log -stdout perf.log -poll 122 | 123 | ``` 124 | 125 | If your file uses `{{` and `}}` as part of it's syntax, you can change the template escape characters using the `-delims`. 126 | 127 | ``` 128 | $ dockerize -delims "<%:%>" 129 | ``` 130 | 131 | Http headers can be specified for http/https protocols. 132 | 133 | ``` 134 | $ dockerize -wait http://web:80 -wait-http-header "Authorization:Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 135 | ``` 136 | 137 | ## Waiting for other dependencies 138 | 139 | It is common when using tools like [Docker Compose](https://docs.docker.com/compose/) to depend on services in other linked containers, however oftentimes relying on [links](https://docs.docker.com/compose/compose-file/#links) is not enough - whilst the container itself may have _started_, the _service(s)_ within it may not yet be ready - resulting in shell script hacks to work around race conditions. 140 | 141 | Dockerize gives you the ability to wait for services on a specified protocol (`file`, `tcp`, `tcp4`, `tcp6`, `http`, `https` and `unix`) before starting your application: 142 | 143 | ``` 144 | $ dockerize -wait tcp://db:5432 -wait http://web:80 -wait file:///tmp/generated-file 145 | ``` 146 | 147 | ### Timeout 148 | 149 | You can optionally specify how long to wait for the services to become available by using the `-timeout #` argument (Default: 10 seconds). If the timeout is reached and the service is still not available, the process exits with status code 1. 150 | 151 | ``` 152 | $ dockerize -wait tcp://db:5432 -wait http://web:80 -timeout 10s 153 | ``` 154 | 155 | See [this issue](https://github.com/docker/compose/issues/374#issuecomment-126312313) for a deeper discussion, and why support isn't and won't be available in the Docker ecosystem itself. 156 | 157 | ## Using Templates 158 | 159 | Templates use Golang [text/template](http://golang.org/pkg/text/template/). You can access environment 160 | variables within a template with `.Env`. 161 | 162 | ``` 163 | {{ .Env.PATH }} is my path 164 | ``` 165 | 166 | There are a few built-in functions as well: 167 | 168 | * `default $var $default` - Returns a default value for one that does not exist. `{{ default .Env.VERSION "0.1.2" }}` 169 | * `contains $map $key` - Returns true if a string is within another string 170 | * `exists $path` - Determines if a file path exists or not. `{{ exists "/etc/default/myapp" }}` 171 | * `split $string $sep` - Splits a string into an array using a separator string. Alias for [`strings.Split`][go.string.Split]. `{{ split .Env.PATH ":" }}` 172 | * `replace $string $old $new $count` - Replaces all occurrences of a string within another string. Alias for [`strings.Replace`][go.string.Replace]. `{{ replace .Env.PATH ":" }}` 173 | * `parseUrl $url` - Parses a URL into it's [protocol, scheme, host, etc. parts][go.url.URL]. Alias for [`url.Parse`][go.url.Parse] 174 | * `atoi $value` - Parses a string $value into an int. `{{ if (gt (atoi .Env.NUM_THREADS) 1) }}` 175 | * `add $arg1 $arg` - Performs integer addition. `{{ add (atoi .Env.SHARD_NUM) -1 }}` 176 | * `isTrue $value` - Parses a string $value to a boolean value. `{{ if isTrue .Env.ENABLED }}` 177 | * `lower $value` - Lowercase a string. 178 | * `upper $value` - Uppercase a string. 179 | * `jsonQuery $json $query` - Returns the result of a selection query against a json document. 180 | * `loop` - Create for loops. 181 | 182 | ### jsonQuery 183 | 184 | Objects and fields are accessed by name. Array elements are accessed by index in square brackets (e.g. `[1]`). Nested elements are separated by dots (`.`). 185 | 186 | **Examples:** 187 | 188 | With the following JSON in `.Env.SERVICES` 189 | 190 | ``` 191 | { 192 | "services": [ 193 | { 194 | "name": "service1", 195 | "port": 8000, 196 | },{ 197 | "name": "service2", 198 | "port": 9000, 199 | } 200 | ] 201 | } 202 | ``` 203 | 204 | the template expression `jsonQuery .Env.SERVICES "services.[1].port"` returns `9000`. 205 | 206 | ### loop 207 | 208 | `loop` allows for creating for loop within a template. It takes 1 to 3 arguments. 209 | 210 | ``` 211 | # Loop from 0...10 212 | {{ range loop 10 }} 213 | i = {{ . }} 214 | {{ end }} 215 | 216 | # Loop from 5...10 217 | {{ range $i := loop 5 10 }} 218 | i = {{ $i }} 219 | {{ end }} 220 | 221 | # Loop from 5...10 by 2 222 | {{ range $i := loop 5 10 2 }} 223 | i = {{ $i }} 224 | {{ end }} 225 | ``` 226 | 227 | ## License 228 | 229 | MIT 230 | 231 | 232 | [go.string.Split]: https://golang.org/pkg/strings/#Split 233 | [go.string.Replace]: https://golang.org/pkg/strings/#Replace 234 | [go.url.Parse]: https://golang.org/pkg/net/url/#Parse 235 | [go.url.URL]: https://golang.org/pkg/net/url/#URL 236 | -------------------------------------------------------------------------------- /examples/json/json-example: -------------------------------------------------------------------------------- 1 | {{ with $jsonDoc := `{ 2 | "services": [ 3 | { 4 | "name": "service1", 5 | "port": 8000 6 | }, { 7 | "name": "service2", 8 | "port": 9000 9 | } 10 | ] 11 | }` }} 12 | 13 | NAME0={{ jsonQuery $jsonDoc "services.[0].name" }} 14 | PORT0={{ jsonQuery $jsonDoc "services.[0].port" }} 15 | NAME1={{ jsonQuery $jsonDoc "services.[1].name" }} 16 | PORT1={{ jsonQuery $jsonDoc "services.[1].port" }} 17 | 18 | {{ range $index, $value := jsonQuery $jsonDoc "services" }} 19 | Index: {{ printf "%v" $index }} 20 | Name: {{ printf "%v" $value.name }} 21 | Port: {{ printf "%v" $value.port }} 22 | --- 23 | {{end}} 24 | 25 | {{end}} -------------------------------------------------------------------------------- /examples/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Jason Wilder mail@jasonwilder.com 3 | 4 | # Install Nginx. 5 | RUN echo "deb http://ppa.launchpad.net/nginx/stable/ubuntu trusty main" > /etc/apt/sources.list.d/nginx-stable-trusty.list \ 6 | && echo "deb-src http://ppa.launchpad.net/nginx/stable/ubuntu trusty main" >> /etc/apt/sources.list.d/nginx-stable-trusty.list 7 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C \ 8 | && apt-get update && apt-get install -y wget nginx 9 | 10 | RUN wget https://github.com/jwilder/dockerize/releases/download/v0.0.4/dockerize-linux-amd64-v0.0.4.tar.gz \ 11 | && tar -C /usr/local/bin -xvzf dockerize-linux-amd64-v0.0.4.tar.gz 12 | 13 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf 14 | 15 | ADD default.tmpl /etc/nginx/sites-available/default.tmpl 16 | 17 | EXPOSE 80 18 | 19 | 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"] 20 | -------------------------------------------------------------------------------- /examples/nginx/default.tmpl: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server ipv6only=on; 4 | 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | 8 | # Make site accessible from http://localhost/ 9 | server_name localhost; 10 | 11 | location / { 12 | access_log off; 13 | proxy_pass {{ .Env.PROXY_URL }}; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | func runCmd(ctx context.Context, cancel context.CancelFunc, cmd string, args ...string) { 15 | defer wg.Done() 16 | 17 | process := exec.Command(cmd, args...) 18 | process.Stdin = os.Stdin 19 | process.Stdout = os.Stdout 20 | process.Stderr = os.Stderr 21 | 22 | // start the process 23 | err := process.Start() 24 | if err != nil { 25 | log.Fatalf("Error starting command: `%s` - %s\n", cmd, err) 26 | } 27 | 28 | // Setup signaling 29 | sigs := make(chan os.Signal, 1) 30 | signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) 31 | 32 | wg.Add(1) 33 | go func() { 34 | defer wg.Done() 35 | 36 | select { 37 | case sig := <-sigs: 38 | log.Printf("Received signal: %s\n", sig) 39 | signalProcessWithTimeout(process, sig) 40 | cancel() 41 | case <-ctx.Done(): 42 | // exit when context is done 43 | } 44 | }() 45 | 46 | err = process.Wait() 47 | cancel() 48 | 49 | if err == nil { 50 | log.Println("Command finished successfully.") 51 | } else { 52 | log.Printf("Command exited with error: %s\n", err) 53 | // OPTIMIZE: This could be cleaner 54 | os.Exit(err.(*exec.ExitError).Sys().(syscall.WaitStatus).ExitStatus()) 55 | } 56 | 57 | } 58 | 59 | func signalProcessWithTimeout(process *exec.Cmd, sig os.Signal) { 60 | done := make(chan struct{}) 61 | 62 | go func() { 63 | process.Process.Signal(sig) // pretty sure this doesn't do anything. It seems like the signal is automatically sent to the command? 64 | process.Wait() 65 | close(done) 66 | }() 67 | select { 68 | case <-done: 69 | return 70 | case <-time.After(10 * time.Second): 71 | log.Println("Killing command due to timeout.") 72 | process.Process.Kill() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jwilder/dockerize 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/Masterminds/sprig/v3 v3.3.0 7 | github.com/hpcloud/tail v1.0.0 8 | github.com/jwilder/gojq v0.0.0-20161018055142-c550732d4a52 9 | golang.org/x/net v0.39.0 10 | ) 11 | 12 | require ( 13 | dario.cat/mergo v1.0.1 // indirect 14 | github.com/Masterminds/goutils v1.1.1 // indirect 15 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 16 | github.com/elgs/gosplitargs v0.0.0-20241205072753-cbd889c0f906 // indirect 17 | github.com/fsnotify/fsnotify v1.8.0 // indirect 18 | github.com/google/uuid v1.6.0 // indirect 19 | github.com/huandu/xstrings v1.5.0 // indirect 20 | github.com/mitchellh/copystructure v1.2.0 // indirect 21 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 22 | github.com/shopspring/decimal v1.4.0 // indirect 23 | github.com/spf13/cast v1.7.1 // indirect 24 | golang.org/x/crypto v0.37.0 // indirect 25 | golang.org/x/sys v0.32.0 // indirect 26 | gopkg.in/fsnotify.v1 v1.4.7 // indirect 27 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 6 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 8 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/elgs/gosplitargs v0.0.0-20241205072753-cbd889c0f906 h1:Gfn+NcN3eAVFLXd9hN9sTd0vtsYXSGwVUKk6EHFVn3s= 12 | github.com/elgs/gosplitargs v0.0.0-20241205072753-cbd889c0f906/go.mod h1:w1WVg5EhY8yy+53iAGOaUp4JxlmV24K3D21BpFgxqcY= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 16 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 17 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 18 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 19 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 20 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 21 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 24 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 25 | github.com/jwilder/gojq v0.0.0-20161018055142-c550732d4a52 h1:ZSTiJFRPQr2XRqfgvm2xpEsrsudezdk8ykBXXiJDfiQ= 26 | github.com/jwilder/gojq v0.0.0-20161018055142-c550732d4a52/go.mod h1:pD7F1lLmlib/2Vy3xild2aXjNnnSudq54IJGftfO4O0= 27 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 28 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 32 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 33 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 34 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 38 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 39 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 40 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 41 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 42 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 43 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 44 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 45 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 46 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 47 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 48 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 49 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 50 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 53 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 54 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 55 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 56 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 57 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "golang.org/x/net/context" 17 | ) 18 | 19 | const defaultWaitRetryInterval = time.Second 20 | 21 | type sliceVar []string 22 | type hostFlagsVar []string 23 | 24 | type Context struct { 25 | } 26 | 27 | type HttpHeader struct { 28 | name string 29 | value string 30 | } 31 | 32 | func (c *Context) Env() map[string]string { 33 | env := make(map[string]string) 34 | for _, i := range os.Environ() { 35 | sep := strings.Index(i, "=") 36 | env[i[0:sep]] = i[sep+1:] 37 | } 38 | return env 39 | } 40 | 41 | var ( 42 | buildVersion string 43 | version bool 44 | poll bool 45 | wg sync.WaitGroup 46 | 47 | templatesFlag sliceVar 48 | templateDirsFlag sliceVar 49 | stdoutTailFlag sliceVar 50 | stderrTailFlag sliceVar 51 | headersFlag sliceVar 52 | delimsFlag string 53 | delims []string 54 | headers []HttpHeader 55 | urls []url.URL 56 | waitFlag hostFlagsVar 57 | waitRetryInterval time.Duration 58 | waitTimeoutFlag time.Duration 59 | dependencyChan chan struct{} 60 | noOverwriteFlag bool 61 | 62 | ctx context.Context 63 | cancel context.CancelFunc 64 | ) 65 | 66 | func (i *hostFlagsVar) String() string { 67 | return fmt.Sprint(*i) 68 | } 69 | 70 | func (i *hostFlagsVar) Set(value string) error { 71 | *i = append(*i, value) 72 | return nil 73 | } 74 | 75 | func (s *sliceVar) Set(value string) error { 76 | *s = append(*s, value) 77 | return nil 78 | } 79 | 80 | func (s *sliceVar) String() string { 81 | return strings.Join(*s, ",") 82 | } 83 | 84 | func waitForDependencies() { 85 | dependencyChan := make(chan struct{}) 86 | 87 | go func() { 88 | for _, u := range urls { 89 | log.Println("Waiting for:", u.String()) 90 | 91 | switch u.Scheme { 92 | case "file": 93 | wg.Add(1) 94 | go func(u url.URL) { 95 | defer wg.Done() 96 | ticker := time.NewTicker(waitRetryInterval) 97 | defer ticker.Stop() 98 | var err error 99 | if _, err = os.Stat(u.Path); err == nil { 100 | log.Printf("File %s had been generated\n", u.String()) 101 | return 102 | } 103 | for range ticker.C { 104 | if _, err = os.Stat(u.Path); err == nil { 105 | log.Printf("File %s had been generated\n", u.String()) 106 | return 107 | } else if os.IsNotExist(err) { 108 | continue 109 | } else { 110 | log.Printf("Problem with check file %s exist: %v. Sleeping %s\n", u.String(), err.Error(), waitRetryInterval) 111 | 112 | } 113 | } 114 | }(u) 115 | case "tcp", "tcp4", "tcp6": 116 | waitForSocket(u.Scheme, u.Host, waitTimeoutFlag) 117 | case "unix": 118 | waitForSocket(u.Scheme, u.Path, waitTimeoutFlag) 119 | case "http", "https": 120 | wg.Add(1) 121 | go func(u url.URL) { 122 | client := &http.Client{ 123 | Timeout: waitTimeoutFlag, 124 | } 125 | 126 | defer wg.Done() 127 | for { 128 | req, err := http.NewRequest("GET", u.String(), nil) 129 | if err != nil { 130 | log.Printf("Problem with dial: %v. Sleeping %s\n", err.Error(), waitRetryInterval) 131 | time.Sleep(waitRetryInterval) 132 | } 133 | if len(headers) > 0 { 134 | for _, header := range headers { 135 | req.Header.Add(header.name, header.value) 136 | } 137 | } 138 | 139 | resp, err := client.Do(req) 140 | if err != nil { 141 | log.Printf("Problem with request: %s. Sleeping %s\n", err.Error(), waitRetryInterval) 142 | time.Sleep(waitRetryInterval) 143 | } else if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 { 144 | log.Printf("Received %d from %s\n", resp.StatusCode, u.String()) 145 | // dispose the response body and close it. 146 | io.Copy(io.Discard, resp.Body) 147 | resp.Body.Close() 148 | return 149 | } else { 150 | log.Printf("Received %d from %s. Sleeping %s\n", resp.StatusCode, u.String(), waitRetryInterval) 151 | io.Copy(io.Discard, resp.Body) 152 | resp.Body.Close() 153 | time.Sleep(waitRetryInterval) 154 | } 155 | } 156 | }(u) 157 | default: 158 | log.Fatalf("invalid host protocol provided: %s. supported protocols are: tcp, tcp4, tcp6 and http", u.Scheme) 159 | } 160 | } 161 | wg.Wait() 162 | close(dependencyChan) 163 | }() 164 | 165 | select { 166 | case <-dependencyChan: 167 | break 168 | case <-time.After(waitTimeoutFlag): 169 | log.Fatalf("Timeout after %s waiting on dependencies to become available: %v", waitTimeoutFlag, waitFlag) 170 | } 171 | 172 | } 173 | 174 | func waitForSocket(scheme, addr string, timeout time.Duration) { 175 | wg.Add(1) 176 | go func() { 177 | defer wg.Done() 178 | for { 179 | conn, err := net.DialTimeout(scheme, addr, waitTimeoutFlag) 180 | if err != nil { 181 | log.Printf("Problem with dial: %v. Sleeping %s\n", err.Error(), waitRetryInterval) 182 | time.Sleep(waitRetryInterval) 183 | } 184 | if conn != nil { 185 | log.Printf("Connected to %s://%s\n", scheme, addr) 186 | conn.Close() 187 | return 188 | } 189 | } 190 | }() 191 | } 192 | 193 | func usage() { 194 | println(`Usage: dockerize [options] [command] 195 | 196 | Utility to simplify running applications in docker containers 197 | 198 | Options:`) 199 | flag.PrintDefaults() 200 | 201 | println(` 202 | Arguments: 203 | command - command to be executed 204 | `) 205 | 206 | println(`Examples: 207 | `) 208 | println(` Generate /etc/nginx/nginx.conf using nginx.tmpl as a template, tail /var/log/nginx/access.log 209 | and /var/log/nginx/error.log, waiting for a website to become available on port 8000 and start nginx.`) 210 | println(` 211 | dockerize -template nginx.tmpl:/etc/nginx/nginx.conf \ 212 | -stdout /var/log/nginx/access.log \ 213 | -stderr /var/log/nginx/error.log \ 214 | -wait tcp://web:8000 nginx 215 | `) 216 | 217 | println(`For more information, see https://github.com/jwilder/dockerize`) 218 | } 219 | 220 | func main() { 221 | 222 | flag.BoolVar(&version, "version", false, "show version") 223 | flag.BoolVar(&poll, "poll", false, "enable polling") 224 | 225 | flag.Var(&templatesFlag, "template", "Template (/template:/dest). Can be passed multiple times. Does also support directories") 226 | flag.BoolVar(&noOverwriteFlag, "no-overwrite", false, "Do not overwrite destination file if it already exists.") 227 | flag.Var(&stdoutTailFlag, "stdout", "Tails a file to stdout. Can be passed multiple times") 228 | flag.Var(&stderrTailFlag, "stderr", "Tails a file to stderr. Can be passed multiple times") 229 | flag.StringVar(&delimsFlag, "delims", "", `template tag delimiters. default "{{":"}}" `) 230 | flag.Var(&headersFlag, "wait-http-header", "HTTP headers, colon separated. e.g \"Accept-Encoding: gzip\". Can be passed multiple times") 231 | flag.Var(&waitFlag, "wait", "Host (tcp/tcp4/tcp6/http/https/unix/file) to wait for before this container starts. Can be passed multiple times. e.g. tcp://db:5432") 232 | flag.DurationVar(&waitTimeoutFlag, "timeout", 10*time.Second, "Host wait timeout") 233 | flag.DurationVar(&waitRetryInterval, "wait-retry-interval", defaultWaitRetryInterval, "Duration to wait before retrying") 234 | 235 | flag.Usage = usage 236 | flag.Parse() 237 | 238 | if version { 239 | fmt.Println(buildVersion) 240 | return 241 | } 242 | 243 | if flag.NArg() == 0 && flag.NFlag() == 0 { 244 | usage() 245 | os.Exit(1) 246 | } 247 | 248 | if delimsFlag != "" { 249 | delims = strings.Split(delimsFlag, ":") 250 | if len(delims) != 2 { 251 | log.Fatalf("bad delimiters argument: %s. expected \"left:right\"", delimsFlag) 252 | } 253 | } 254 | 255 | for _, host := range waitFlag { 256 | u, err := url.Parse(host) 257 | if err != nil { 258 | log.Fatalf("bad hostname provided: %s. %s", host, err.Error()) 259 | } 260 | urls = append(urls, *u) 261 | } 262 | 263 | for _, h := range headersFlag { 264 | //validate headers need -wait options 265 | if len(waitFlag) == 0 { 266 | log.Fatalf("-wait-http-header \"%s\" provided with no -wait option", h) 267 | } 268 | 269 | const errMsg = "bad HTTP Headers argument: %s. expected \"headerName: headerValue\"" 270 | if strings.Contains(h, ":") { 271 | parts := strings.Split(h, ":") 272 | if len(parts) != 2 { 273 | log.Fatalf(errMsg, headersFlag) 274 | } 275 | headers = append(headers, HttpHeader{name: strings.TrimSpace(parts[0]), value: strings.TrimSpace(parts[1])}) 276 | } else { 277 | log.Fatalf(errMsg, headersFlag) 278 | } 279 | 280 | } 281 | 282 | for _, t := range templatesFlag { 283 | template, dest := t, "" 284 | if strings.Contains(t, ":") { 285 | parts := strings.Split(t, ":") 286 | if len(parts) != 2 { 287 | log.Fatalf("bad template argument: %s. expected \"/template:/dest\"", t) 288 | } 289 | template, dest = parts[0], parts[1] 290 | } 291 | 292 | fi, err := os.Stat(template) 293 | if err != nil { 294 | log.Fatalf("unable to stat %s, error: %s", template, err) 295 | } 296 | if fi.IsDir() { 297 | generateDir(template, dest) 298 | } else { 299 | generateFile(template, dest) 300 | } 301 | } 302 | 303 | waitForDependencies() 304 | 305 | // Setup context 306 | ctx, cancel = context.WithCancel(context.Background()) 307 | 308 | if flag.NArg() > 0 { 309 | wg.Add(1) 310 | go runCmd(ctx, cancel, flag.Arg(0), flag.Args()[1:]...) 311 | } 312 | 313 | for _, out := range stdoutTailFlag { 314 | wg.Add(1) 315 | go tailFile(ctx, out, poll, os.Stdout) 316 | } 317 | 318 | for _, err := range stderrTailFlag { 319 | wg.Add(1) 320 | go tailFile(ctx, err, poll, os.Stderr) 321 | } 322 | 323 | wg.Wait() 324 | } 325 | -------------------------------------------------------------------------------- /tail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/hpcloud/tail" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | func tailFile(ctx context.Context, file string, poll bool, dest *os.File) { 14 | defer wg.Done() 15 | 16 | var isPipe bool 17 | var errCount int 18 | 19 | s, err := os.Stat(file) 20 | if err != nil { 21 | log.Printf("Warning: unable to stat %s: %s", file, err) 22 | errCount++ 23 | isPipe = false 24 | } else { 25 | isPipe = s.Mode()&os.ModeNamedPipe != 0 26 | } 27 | 28 | t, err := tail.TailFile(file, tail.Config{ 29 | Follow: true, 30 | ReOpen: true, 31 | Poll: poll, 32 | Logger: tail.DiscardingLogger, 33 | Pipe: isPipe, 34 | }) 35 | if err != nil { 36 | log.Fatalf("unable to tail %s: %s", file, err) 37 | } 38 | 39 | defer func() { 40 | t.Stop() 41 | t.Cleanup() 42 | }() 43 | 44 | // main loop 45 | for { 46 | select { 47 | // if the channel is done, then exit the loop 48 | case <-ctx.Done(): 49 | return 50 | // get the next log line and echo it out 51 | case line := <-t.Lines: 52 | if t.Err() != nil { 53 | log.Printf("Warning: unable to tail %s: %s", file, t.Err()) 54 | errCount++ 55 | if errCount > 30 { 56 | log.Fatalf("Logged %d consecutive errors while tailing. Exiting", errCount) 57 | } 58 | time.Sleep(2 * time.Second) // Sleep for 2 seconds before retrying 59 | } else if line == nil { 60 | return 61 | } else { 62 | fmt.Fprintln(dest, line.Text) 63 | errCount = 0 // Zero the error count 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "syscall" 12 | "text/template" 13 | 14 | "github.com/Masterminds/sprig/v3" 15 | "github.com/jwilder/gojq" 16 | ) 17 | 18 | func exists(path string) (bool, error) { 19 | _, err := os.Stat(path) 20 | if err == nil { 21 | return true, nil 22 | } 23 | if os.IsNotExist(err) { 24 | return false, nil 25 | } 26 | return false, err 27 | } 28 | 29 | func contains(item map[string]string, key string) bool { 30 | if _, ok := item[key]; ok { 31 | return true 32 | } 33 | return false 34 | } 35 | 36 | func defaultValue(args ...interface{}) (string, error) { 37 | if len(args) == 0 { 38 | return "", fmt.Errorf("default called with no values!") 39 | } 40 | 41 | if len(args) > 0 { 42 | if args[0] != nil { 43 | return args[0].(string), nil 44 | } 45 | } 46 | 47 | if len(args) > 1 { 48 | if args[1] == nil { 49 | return "", fmt.Errorf("default called with nil default value!") 50 | } 51 | 52 | if _, ok := args[1].(string); !ok { 53 | return "", fmt.Errorf("default is not a string value. hint: surround it w/ double quotes.") 54 | } 55 | 56 | return args[1].(string), nil 57 | } 58 | 59 | return "", fmt.Errorf("default called with no default value") 60 | } 61 | 62 | func parseUrl(rawurl string) *url.URL { 63 | u, err := url.Parse(rawurl) 64 | if err != nil { 65 | log.Fatalf("unable to parse url %s: %s", rawurl, err) 66 | } 67 | return u 68 | } 69 | 70 | func add(arg1, arg2 int) int { 71 | return arg1 + arg2 72 | } 73 | 74 | func isTrue(s string) bool { 75 | b, err := strconv.ParseBool(strings.ToLower(s)) 76 | if err == nil { 77 | return b 78 | } 79 | return false 80 | } 81 | 82 | func jsonQuery(jsonObj string, query string) (interface{}, error) { 83 | parser, err := gojq.NewStringQuery(jsonObj) 84 | if err != nil { 85 | return "", err 86 | } 87 | res, err := parser.Query(query) 88 | if err != nil { 89 | return "", err 90 | } 91 | return res, nil 92 | } 93 | 94 | func loop(args ...int) (<-chan int, error) { 95 | var start, stop, step int 96 | switch len(args) { 97 | case 1: 98 | start, stop, step = 0, args[0], 1 99 | case 2: 100 | start, stop, step = args[0], args[1], 1 101 | case 3: 102 | start, stop, step = args[0], args[1], args[2] 103 | default: 104 | return nil, fmt.Errorf("wrong number of arguments, expected 1-3"+ 105 | ", but got %d", len(args)) 106 | } 107 | 108 | c := make(chan int) 109 | go func() { 110 | for i := start; i < stop; i += step { 111 | c <- i 112 | } 113 | close(c) 114 | }() 115 | return c, nil 116 | } 117 | 118 | func generateFile(templatePath, destPath string) bool { 119 | templateMap := template.FuncMap{ 120 | "contains": contains, 121 | "exists": exists, 122 | "split": strings.Split, 123 | "replace": strings.Replace, 124 | "default": defaultValue, 125 | "parseUrl": parseUrl, 126 | "atoi": strconv.Atoi, 127 | "add": add, 128 | "isTrue": isTrue, 129 | "lower": strings.ToLower, 130 | "upper": strings.ToUpper, 131 | "jsonQuery": jsonQuery, 132 | "loop": loop, 133 | } 134 | 135 | combinedFuncMap := sprig.TxtFuncMap() 136 | for k, v := range templateMap { 137 | combinedFuncMap[k] = v 138 | } 139 | tmpl := template.New(filepath.Base(templatePath)).Funcs(combinedFuncMap) 140 | 141 | if len(delims) > 0 { 142 | tmpl = tmpl.Delims(delims[0], delims[1]) 143 | } 144 | tmpl, err := tmpl.ParseFiles(templatePath) 145 | if err != nil { 146 | log.Fatalf("unable to parse template: %s", err) 147 | } 148 | 149 | // Don't overwrite destination file if it exists and no-overwrite flag passed 150 | if _, err := os.Stat(destPath); err == nil && noOverwriteFlag { 151 | return false 152 | } 153 | 154 | dest := os.Stdout 155 | if destPath != "" { 156 | dest, err = os.Create(destPath) 157 | if err != nil { 158 | log.Fatalf("unable to create %s", err) 159 | } 160 | defer dest.Close() 161 | } 162 | 163 | err = tmpl.ExecuteTemplate(dest, filepath.Base(templatePath), &Context{}) 164 | if err != nil { 165 | log.Fatalf("template error: %s\n", err) 166 | } 167 | 168 | if fi, err := os.Stat(destPath); err == nil { 169 | if err := dest.Chmod(fi.Mode()); err != nil { 170 | log.Fatalf("unable to chmod temp file: %s\n", err) 171 | } 172 | if err := dest.Chown(int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)); err != nil { 173 | log.Fatalf("unable to chown temp file: %s\n", err) 174 | } 175 | } 176 | 177 | return true 178 | } 179 | 180 | func generateDir(templateDir, destDir string) bool { 181 | if destDir != "" { 182 | fiDest, err := os.Stat(destDir) 183 | if err != nil { 184 | log.Fatalf("unable to stat %s, error: %s", destDir, err) 185 | } 186 | if !fiDest.IsDir() { 187 | log.Fatalf("if template is a directory, dest must also be a directory (or stdout)") 188 | } 189 | } 190 | 191 | files, err := os.ReadDir(templateDir) 192 | if err != nil { 193 | log.Fatalf("bad directory: %s, error: %s", templateDir, err) 194 | } 195 | 196 | for _, file := range files { 197 | if destDir == "" { 198 | generateFile(filepath.Join(templateDir, file.Name()), "") 199 | } else { 200 | generateFile(filepath.Join(templateDir, file.Name()), filepath.Join(destDir, file.Name())) 201 | } 202 | } 203 | 204 | return true 205 | } 206 | --------------------------------------------------------------------------------