From 1c59200565d2ecca26531631c7181d2848570dd7 Mon Sep 17 00:00:00 2001 From: Ross Cadogan Date: Tue, 18 Oct 2016 13:54:41 +0100 Subject: [PATCH] Registry authentication was failing silently when pulling images. Load authentication credentials for available credential stores in order of preference: 1. Environment variables REPO_USER, REPO_PASS 2. Docker config files Request image pull with authentication header. Wait until pull request is complete before exiting function. --- README.md | 10 +++++ container/client.go | 25 +++++++----- container/trust.go | 93 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 18 ++++----- 4 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 container/trust.go diff --git a/README.md b/README.md index 91f6c31..fd98414 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,16 @@ docker run -d \ centurylink/watchtower ``` +If pulling images from a private Docker registry, supply any authentication credentials with the environment variables `REPO_USER` and `REPO_PASS` or omit to leave watchtower load credentials from the default Docker config (`~/.docker/config.json`): + +``` +docker run -d \ + --name watchtower \ + -e REPO_USER="" -e REPO_PASS="" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + drud/watchtower container_to_watch --debug +``` + ### Arguments By default, watchtower will monitor all containers running within the Docker daemon to which it is pointed (in most cases this will be the local Docker daemon, but you can override it with the `--host` option described in the next section). However, you can restrict watchtower to monitoring a subset of the running containers by specifying the container names as arguments when launching watchtower. diff --git a/container/client.go b/container/client.go index b41e6eb..0917c9b 100644 --- a/container/client.go +++ b/container/client.go @@ -3,11 +3,11 @@ package container import ( "fmt" "time" + "io/ioutil" log "github.com/Sirupsen/logrus" dockerclient "github.com/docker/docker/client" "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" "golang.org/x/net/context" ) @@ -146,22 +146,28 @@ func (client dockerClient) IsContainerStale(c Container) (bool, error) { if client.pullImages { log.Debugf("Pulling %s for %s", imageName, c.Name()) - auth := types.AuthConfig { - Username: "testuser", - Password: "testpassword", + var opts types.ImagePullOptions // ImagePullOptions can take a RegistryAuth arg to authenticate against a private registry + auth, err := EncodedEnvAuth(imageName) + if err != nil { + // credentials environment vars not set, trying Docker config instead + auth, err = EncodedConfigAuth(imageName) } - encodedAuth, err := command.EncodeAuthToBase64(auth) if err != nil { - return false, err + log.Debug("No authentication credentials found") + opts = types.ImagePullOptions{} + } else { + opts = types.ImagePullOptions{RegistryAuth: auth, PrivilegeFunc: DefaultAuthHandler} } - // Note: ImagePullOptions below can take a RegistryAuth arg if 401 on private registry - closer, err := client.api.ImagePull(bg, imageName, types.ImagePullOptions{RegistryAuth: encodedAuth}) + response, err := client.api.ImagePull(bg, imageName, opts) if err != nil { log.Debugf("Error pulling image %s, %s", imageName, err) return false, err } - defer closer.Close() + defer response.Close() + + // the pull request will be aborted prematurely unless the response is read + _, err = ioutil.ReadAll(response) } newImageInfo, _, err := client.api.ImageInspectWithRaw(bg, imageName) @@ -174,7 +180,6 @@ func (client dockerClient) IsContainerStale(c Container) (bool, error) { return true, nil } else { log.Debugf("No new images found for %s", c.Name()) - log.Debugf("Old image ID %s is the same as New Image ID %s", oldImageInfo.ID, newImageInfo.ID) } diff --git a/container/trust.go b/container/trust.go new file mode 100644 index 0000000..871e6aa --- /dev/null +++ b/container/trust.go @@ -0,0 +1,93 @@ +package container + +import ( + "errors" + "os" + "strings" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/reference" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cliconfig/configfile" + "github.com/docker/docker/cliconfig/credentials" +) + + +/* + * Return an encoded auth config for the given registry + * hardcoded for a test environment + */ +func EncodedTestAuth(ref string) (string, error) { + auth := types.AuthConfig { + Username: "testuser", + Password: "testpassword", + } + return EncodeAuth(auth) +} + +/* + * Return an encoded auth config for the given registry + * loaded from environment variables + */ +func EncodedEnvAuth(ref string) (string, error) { + username := os.Getenv("REPO_USER") + password := os.Getenv("REPO_PASS") + if username != "" && password != "" { + auth := types.AuthConfig { + Username: username, + Password: password, + } + log.Debugf("Loaded auth credentials %s from environment for %s", auth, ref) + return EncodeAuth(auth) + } else { + return "", errors.New("Registry auth environment variables (REPO_USER, REPO_PASS) not set") + } +} + +/* + * Return an encoded auth config for the given registry + * loaded from the docker config + */ +func EncodedConfigAuth(ref string) (string, error) { + server, err := ParseServerAddress(ref) + configFile := command.LoadDefaultConfigFile(log.StandardLogger().Out) + credStore := CredentialsStore(*configFile) + auth, err := credStore.Get(server) + if err != nil { + return "", err + } + log.Debugf("Loaded auth credentials %s from Docker config for reference %s", auth, ref) + return EncodeAuth(auth) +} + +func ParseServerAddress(ref string) (string, error) { + repository, _, err := reference.Parse(ref) + if err != nil { + return ref, err + } + parts := strings.Split(repository, "/") + return parts[0], nil + +} + +// CredentialsStore returns a new credentials store based +// on the settings provided in the configuration file. +func CredentialsStore(configFile configfile.ConfigFile) credentials.Store { + if configFile.CredentialsStore != "" { + return credentials.NewNativeStore(&configFile) + } + return credentials.NewFileStore(&configFile) +} + +/* + * Base64 encode an AuthConfig struct for transmission over HTTP + */ +func EncodeAuth(auth types.AuthConfig) (string, error) { + return command.EncodeAuthToBase64(auth) +} + +func DefaultAuthHandler() (string, error) { + log.Error("Authentication requested") + return "", fmt.Errorf("Error requesting privilege") +} diff --git a/main.go b/main.go index 99f1326..bdbf31c 100644 --- a/main.go +++ b/main.go @@ -15,10 +15,10 @@ import ( ) var ( - wg sync.WaitGroup - client container.Client - pollInterval time.Duration - cleanup bool + wg sync.WaitGroup + client container.Client + pollInterval time.Duration + cleanup bool ) func init() { @@ -61,8 +61,8 @@ func main() { Usage: "enable debug mode with verbose logging", }, cli.StringFlag{ - Name: "apiversion", - Usage: "the version of the docker api", + Name: "apiversion", + Usage: "the version of the docker api", EnvVar: "DOCKER_API_VERSION", }, } @@ -124,9 +124,9 @@ func handleSignals() { } func setEnvOptStr(env string, opt string) error { - if (opt != "" && opt != os.Getenv(env)) { + if opt != "" && opt != os.Getenv(env) { err := os.Setenv(env, opt) - if (err != nil) { + if err != nil { return err } } @@ -134,7 +134,7 @@ func setEnvOptStr(env string, opt string) error { } func setEnvOptBool(env string, opt bool) error { - if (opt == true) { + if opt == true { return setEnvOptStr(env, "1") } return nil