diff --git a/agent/go.mod b/agent/go.mod index ed6685a5832..d035b2a0456 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -7,6 +7,7 @@ require ( github.com/determined-ai/determined/master v0.0.0 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v1.13.1 + github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/go-connections v0.4.0 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-ole/go-ole v1.2.4 // indirect diff --git a/agent/go.sum b/agent/go.sum index 17b7e4b0a63..56a0f6d375f 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -37,6 +37,8 @@ github.com/docker/distribution v0.0.0-20180720172123-0dae0957e5fe/go.mod h1:J2gT github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.0.0-20170502054910-90d35abf7b35/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/engine v1.4.2-0.20191113042239-ea84732a7725 h1:aEPcmLOLK4xi6fKbzrWyiAAM+SN9sN1Pi6WrfLnPDog= github.com/docker/engine v1.4.2-0.20191113042239-ea84732a7725/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= diff --git a/agent/internal/docker.go b/agent/internal/docker.go index d2dbe68f534..f3bb81a52dd 100644 --- a/agent/internal/docker.go +++ b/agent/internal/docker.go @@ -27,6 +27,7 @@ import ( type dockerActor struct { *client.Client + credentialStores map[string]*credentialStore } type ( @@ -65,6 +66,11 @@ func registryToString(reg types.AuthConfig) (string, error) { func (d *dockerActor) Receive(ctx *actor.Context) error { switch msg := ctx.Message().(type) { case actor.PreStart: + stores, err := getAllCredentialStores() + if err != nil { + ctx.Log().Warn(fmt.Sprintf("can't retrieve any credential stores: %v", err)) + } + d.credentialStores = stores case pullImage: go d.pullImage(ctx, msg) @@ -127,6 +133,18 @@ func (d *dockerActor) pullImage(ctx *actor.Context, msg pullImage) { sendErr(ctx, errors.Wrap(err, "error encoding registry credentials")) return } + } else if store, ok := d.credentialStores[reference.Domain(ref)]; ok { + var creds types.AuthConfig + creds, err = store.get() + if err != nil { + sendErr(ctx, errors.Wrap(err, "unable to get credentials from helper")) + return + } + reg, err = registryToString(creds) + if err != nil { + sendErr(ctx, errors.Wrap(err, "error encoding registry credentials from helper")) + return + } } opts := types.ImagePullOptions{ diff --git a/agent/internal/docker_credential_store.go b/agent/internal/docker_credential_store.go new file mode 100644 index 00000000000..b3284bd5cfc --- /dev/null +++ b/agent/internal/docker_credential_store.go @@ -0,0 +1,80 @@ +package internal + +import ( + "encoding/json" + "io/ioutil" + "os" + + hclient "github.com/docker/docker-credential-helpers/client" + "github.com/docker/docker/api/types" + "github.com/pkg/errors" +) + +const ( + dockerConfigFile = "/root/.docker/config.json" + credentialsHelperPrefix = "docker-credential-" + tokenUsername = "" +) + +type credentialStore struct { + registry string + store hclient.ProgramFunc +} + +// getAllCredentialStores returns the credential helpers configured in the default docker +// config or an error. +func getAllCredentialStores() (map[string]*credentialStore, error) { + type ConfigFile struct { + CredentialHelpers map[string]string `json:"credHelpers,omitempty"` + } + + credentialsStores := map[string]*credentialStore{} + configFile, err := os.Open(dockerConfigFile) + if err != nil { + return credentialsStores, errors.Wrap(err, "can't open docker config") + } + + b, err := ioutil.ReadAll(configFile) + if err != nil { + return credentialsStores, errors.Wrap(err, "can't read docker config") + } + + var config ConfigFile + err = json.Unmarshal(b, &config) + if err != nil { + return credentialsStores, errors.Wrap(err, "can't parse docker config") + } + + if config.CredentialHelpers == nil { + return credentialsStores, nil + } + + for hostname, helper := range config.CredentialHelpers { + credentialsStores[hostname] = &credentialStore{ + registry: hostname, + store: hclient.NewShellProgramFunc(credentialsHelperPrefix + helper), + } + } + + return credentialsStores, nil +} + +// get executes the command to get the credentials from the native store. +func (s *credentialStore) get() (types.AuthConfig, error) { + var ret types.AuthConfig + + creds, err := hclient.Get(s.store, s.registry) + if err != nil { + return ret, err + } + + if creds.Username == tokenUsername { + ret.IdentityToken = creds.Secret + } else { + ret.Password = creds.Secret + ret.Username = creds.Username + } + + ret.ServerAddress = s.registry + return ret, nil +}