diff --git a/dockerize-alpine-linux-amd64-v0.6.1.tar.gz b/dockerize-alpine-linux-amd64-v0.6.1.tar.gz new file mode 100644 index 0000000..928d006 Binary files /dev/null and b/dockerize-alpine-linux-amd64-v0.6.1.tar.gz differ diff --git a/dockerize-darwin-amd64-v0.6.1.tar.gz b/dockerize-darwin-amd64-v0.6.1.tar.gz new file mode 100644 index 0000000..94859fc Binary files /dev/null and b/dockerize-darwin-amd64-v0.6.1.tar.gz differ diff --git a/dockerize-linux-386-v0.6.1.tar.gz b/dockerize-linux-386-v0.6.1.tar.gz new file mode 100644 index 0000000..ec12518 Binary files /dev/null and b/dockerize-linux-386-v0.6.1.tar.gz differ diff --git a/dockerize-linux-amd64-v0.6.1.tar.gz b/dockerize-linux-amd64-v0.6.1.tar.gz new file mode 100644 index 0000000..752beff Binary files /dev/null and b/dockerize-linux-amd64-v0.6.1.tar.gz differ diff --git a/dockerize-linux-armel-v0.6.1.tar.gz b/dockerize-linux-armel-v0.6.1.tar.gz new file mode 100644 index 0000000..62fb754 Binary files /dev/null and b/dockerize-linux-armel-v0.6.1.tar.gz differ diff --git a/dockerize-linux-armhf-v0.6.1.tar.gz b/dockerize-linux-armhf-v0.6.1.tar.gz new file mode 100644 index 0000000..bc83e18 Binary files /dev/null and b/dockerize-linux-armhf-v0.6.1.tar.gz differ diff --git a/exec.go b/exec.go index 07eaef0..8e8a27a 100644 --- a/exec.go +++ b/exec.go @@ -14,6 +14,22 @@ import ( func runCmd(ctx context.Context, cancel context.CancelFunc, cmd string, args ...string) { defer wg.Done() + if eGID >= 0 { + log.Printf("Setting effective gid to %d", eGID) + err := Setgid(eGID) + if err != nil { + log.Fatalf("Error while setting GID to %d: %s", eGID, err) + } + } + + if eUID >= 0 { + log.Printf("Setting effective uid to %d", eUID) + err := Setuid(eUID) + if err != nil { + log.Fatalf("Error while setting UID to %d: %s", eUID, err) + } + } + process := exec.Command(cmd, args...) process.Stdin = os.Stdin process.Stdout = os.Stdout diff --git a/main.go b/main.go index c9e1475..979b88a 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,11 @@ package main import ( + "crypto/tls" + "errors" "flag" "fmt" + "io/ioutil" "log" "net" "net/http" @@ -13,6 +16,7 @@ import ( "time" "golang.org/x/net/context" + "gopkg.in/ini.v1" ) const defaultWaitRetryInterval = time.Second @@ -20,14 +24,18 @@ const defaultWaitRetryInterval = time.Second type sliceVar []string type hostFlagsVar []string +// Context is the type passed into the template renderer type Context struct { } -type HttpHeader struct { +// HTTPHeader this is an optional header passed on http checks +type HTTPHeader struct { name string value string } +// Env is bound to the template rendering Context and returns the +// environment variables passed to the program func (c *Context) Env() map[string]string { env := make(map[string]string) for _, i := range os.Environ() { @@ -43,6 +51,11 @@ var ( poll bool wg sync.WaitGroup + envFlag string + multiline bool + envSection string + envHdrFlag sliceVar + validateCert bool templatesFlag sliceVar templateDirsFlag sliceVar stdoutTailFlag sliceVar @@ -50,13 +63,15 @@ var ( headersFlag sliceVar delimsFlag string delims []string - headers []HttpHeader + headers []HTTPHeader urls []url.URL waitFlag hostFlagsVar waitRetryInterval time.Duration waitTimeoutFlag time.Duration dependencyChan chan struct{} noOverwriteFlag bool + eUID int + eGID int ctx context.Context cancel context.CancelFunc @@ -114,8 +129,15 @@ func waitForDependencies() { case "http", "https": wg.Add(1) go func(u url.URL) { + var tr = http.DefaultTransport + if !validateCert { + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } client := &http.Client{ - Timeout: waitTimeoutFlag, + Timeout: waitTimeoutFlag, + Transport: tr, } defer wg.Done() @@ -206,11 +228,76 @@ Arguments: println(`For more information, see https://github.com/jwilder/dockerize`) } +func getINI(envFlag string, envHdrFlag []string) (iniFile []byte, err error) { + + // See if envFlag parses like an absolute URL, if so use http, otherwise treat as filename + url, urlERR := url.ParseRequestURI(envFlag) + if urlERR == nil && url.IsAbs() { + var resp *http.Response + var req *http.Request + var hdr string + var client *http.Client + var tr = http.DefaultTransport + // Define redirect handler to disallow redirects + var redir = func(req *http.Request, via []*http.Request) error { + return errors.New("Redirects disallowed") + } + + if !validateCert { + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + client = &http.Client{Transport: tr, CheckRedirect: redir} + req, err = http.NewRequest("GET", envFlag, nil) + if err != nil { + // Weird problem with declaring client, bail + return + } + // Handle headers for request - are they headers or filepaths? + for _, h := range envHdrFlag { + if strings.Contains(h, ":") { + // This will break if path includes colon - don't use colons in path! + hdr = h + } else { // Treat this is a path to a secrets file containing header + var hdrFile []byte + hdrFile, err = ioutil.ReadFile(h) + if err != nil { // Could not read file, error out + return + } + hdr = string(hdrFile) + } + parts := strings.Split(hdr, ":") + if len(parts) != 2 { + log.Fatalf("Bad env-headers argument: %s. expected \"headerName: headerValue\"", hdr) + } + req.Header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) + } + resp, err = client.Do(req) + if err == nil && resp.StatusCode == 200 { + defer resp.Body.Close() + iniFile, err = ioutil.ReadAll(resp.Body) + } else if err == nil { // Request completed with unexpected HTTP status code, bail + err = errors.New(resp.Status) + return + } + } else { + iniFile, err = ioutil.ReadFile(envFlag) + } + return +} + func main() { flag.BoolVar(&version, "version", false, "show version") flag.BoolVar(&poll, "poll", false, "enable polling") - + flag.StringVar(&envFlag, "env", "", "Optional path to INI file for injecting env vars. Does not overwrite existing env vars") + flag.BoolVar(&multiline, "multiline", false, "enable parsing multiline INI entries in INI environment file") + flag.StringVar(&envSection, "env-section", "", "Optional section of INI file to use for loading env vars. Defaults to \"\"") + flag.Var(&envHdrFlag, "env-header", "Optional string or path to secrets file for http headers passed if -env is a URL") + flag.BoolVar(&validateCert, "validate-cert", true, "Verify SSL certs for https connections") + flag.IntVar(&eGID, "egid", -1, "Set the numeric group ID for the running program") // Check for -1 later to skip + flag.IntVar(&eUID, "euid", -1, "Set the numeric user id for the running program") flag.Var(&templatesFlag, "template", "Template (/template:/dest). Can be passed multiple times. Does also support directories") flag.BoolVar(&noOverwriteFlag, "no-overwrite", false, "Do not overwrite destination file if it already exists.") flag.Var(&stdoutTailFlag, "stdout", "Tails a file to stdout. Can be passed multiple times") @@ -234,6 +321,25 @@ func main() { os.Exit(1) } + if envFlag != "" { + iniFile, err := getINI(envFlag, envHdrFlag) + if err != nil { + log.Fatalf("unreadable INI file %s: %s", envFlag, err) + } + cfg, err := ini.LoadSources(ini.LoadOptions{ AllowPythonMultilineValues: multiline }, iniFile) + if err != nil { + log.Fatalf("error parsing contents of %s as INI format: %s", envFlag, err) + } + envHash := cfg.Section(envSection).KeysHash() + + for k, v := range envHash { + if _, ok := os.LookupEnv(k); !ok { + // log.Printf("Setting %s to %s", k, v) + os.Setenv(k, v) + } + } + } + if delimsFlag != "" { delims = strings.Split(delimsFlag, ":") if len(delims) != 2 { @@ -261,7 +367,7 @@ func main() { if len(parts) != 2 { log.Fatalf(errMsg, headersFlag) } - headers = append(headers, HttpHeader{name: strings.TrimSpace(parts[0]), value: strings.TrimSpace(parts[1])}) + headers = append(headers, HTTPHeader{name: strings.TrimSpace(parts[0]), value: strings.TrimSpace(parts[1])}) } else { log.Fatalf(errMsg, headersFlag) } @@ -296,6 +402,8 @@ func main() { if flag.NArg() > 0 { wg.Add(1) + // Drop privs if passed the euid or egid params + go runCmd(ctx, cancel, flag.Arg(0), flag.Args()[1:]...) } diff --git a/system.go b/system.go new file mode 100644 index 0000000..7f6a084 --- /dev/null +++ b/system.go @@ -0,0 +1,26 @@ +package main + +// This has been cut/pasted from +// https://github.com/opencontainers/runc/blob/master/libcontainer/system/syscall_linux_64.go + +import ( + "golang.org/x/sys/unix" +) + +// Setuid sets the uid of the calling thread to the specified uid. +func Setuid(uid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETUID, uintptr(uid), 0, 0) + if e1 != 0 { + err = e1 + } + return +} + +// Setgid sets the gid of the calling thread to the specified gid. +func Setgid(gid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETGID, uintptr(gid), 0, 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/tail.go b/tail.go index 6037aaa..3cbdcf6 100644 --- a/tail.go +++ b/tail.go @@ -14,12 +14,13 @@ func tailFile(ctx context.Context, file string, poll bool, dest *os.File) { defer wg.Done() var isPipe bool - var errCount int + var errCount int = 0 + const maxErr = 30 + const sleepDur = 2 * time.Second s, err := os.Stat(file) if err != nil { log.Printf("Warning: unable to stat %s: %s", file, err) - errCount++ isPipe = false } else { isPipe = s.Mode()&os.ModeNamedPipe != 0 @@ -49,19 +50,19 @@ func tailFile(ctx context.Context, file string, poll bool, dest *os.File) { return // get the next log line and echo it out case line := <-t.Lines: - if t.Err() != nil { + if line.Err != nil || (line == nil && t.Err() != nil) { log.Printf("Warning: unable to tail %s: %s", file, t.Err()) errCount++ - if errCount > 30 { + if errCount > maxErr { log.Fatalf("Logged %d consecutive errors while tailing. Exiting", errCount) } - time.Sleep(2 * time.Second) // Sleep for 2 seconds before retrying + time.Sleep(sleepDur) + continue } else if line == nil { return - } else { - fmt.Fprintln(dest, line.Text) - errCount = 0 // Zero the error count } + fmt.Fprintln(dest, line.Text) + errCount = 0 // Zero the error count } } } diff --git a/template.go b/template.go index a42d15c..20ce9f9 100644 --- a/template.go +++ b/template.go @@ -35,7 +35,7 @@ func contains(item map[string]string, key string) bool { func defaultValue(args ...interface{}) (string, error) { if len(args) == 0 { - return "", fmt.Errorf("default called with no values!") + return "", fmt.Errorf("default called with no values") } if len(args) > 0 { @@ -46,11 +46,11 @@ func defaultValue(args ...interface{}) (string, error) { if len(args) > 1 { if args[1] == nil { - return "", fmt.Errorf("default called with nil default value!") + 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 "", fmt.Errorf("default is not a string value. hint: surround it w/ double quotes") } return args[1].(string), nil @@ -59,7 +59,7 @@ func defaultValue(args ...interface{}) (string, error) { return "", fmt.Errorf("default called with no default value") } -func parseUrl(rawurl string) *url.URL { +func parseURL(rawurl string) *url.URL { u, err := url.Parse(rawurl) if err != nil { log.Fatalf("unable to parse url %s: %s", rawurl, err) @@ -122,7 +122,7 @@ func generateFile(templatePath, destPath string) bool { "split": strings.Split, "replace": strings.Replace, "default": defaultValue, - "parseUrl": parseUrl, + "parseUrl": parseURL, "atoi": strconv.Atoi, "add": add, "isTrue": isTrue,