package docker import ( "context" "encoding/json" "errors" "fmt" "io" "os" dockercommand "github.com/docker/cli/cli/command" dockerconfig "github.com/docker/cli/cli/config" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/pkg/jsonmessage" ) type PullPushImageMessage struct { Message jsonmessage.JSONMessage Err error } // PullImage pulls an image and returns a channel to receive progress messages. func (cli *Client) PullImage( ctx context.Context, image string, opts image.PullOptions, ) (<-chan PullPushImageMessage, error) { if opts.RegistryAuth == "" { // Try to retrieve the authentication token for the image from the default local Docker config file. if encodedAuth, err := RetrieveLocalDockerRegistryAuth(image); err == nil { opts.RegistryAuth = encodedAuth } } respBody, err := cli.ImagePull(ctx, image, opts) if err != nil { return nil, err } ch := make(chan PullPushImageMessage) go processPullPushImageResp(ctx, respBody, ch) return ch, nil } // PushImage pushes an image and returns a channel to receive progress messages. func (cli *Client) PushImage( ctx context.Context, image string, opts image.PushOptions, ) (<-chan PullPushImageMessage, error) { if opts.RegistryAuth == "" { // Try to retrieve the authentication token for the image from the default local Docker config file. encodedAuth, _ := RetrieveLocalDockerRegistryAuth(image) if encodedAuth == "" { // If no credentials are found, provide an encoded empty auth config to work around the bug in Docker: // https://github.com/moby/moby/issues/50729 var err error if encodedAuth, err = registry.EncodeAuthConfig(registry.AuthConfig{}); err != nil { return nil, fmt.Errorf("encode empty auth config: %w", err) } } opts.RegistryAuth = encodedAuth } respBody, err := cli.ImagePush(ctx, image, opts) if err != nil { return nil, err } ch := make(chan PullPushImageMessage) go processPullPushImageResp(ctx, respBody, ch) return ch, nil } // processPullPushImageResp decodes JSON messages from the image pull/push response body and // sends them to the provided channel. func processPullPushImageResp(ctx context.Context, respBody io.ReadCloser, ch chan<- PullPushImageMessage) { defer respBody.Close() defer close(ch) decoder := json.NewDecoder(respBody) for { var jm jsonmessage.JSONMessage if err := decoder.Decode(&jm); err != nil { if errors.Is(err, io.EOF) { return } ch <- PullPushImageMessage{Err: fmt.Errorf("decode image pull/push message: %w", err)} return } msg := PullPushImageMessage{Message: jm} if jm.Error != nil { msg.Err = errors.New(jm.Error.Message) } select { case <-ctx.Done(): ch <- PullPushImageMessage{Err: ctx.Err()} return default: ch <- msg } } } // RetrieveLocalDockerRegistryAuth retrieves the authentication token for the specified image from the local Docker // config file. It returns the encoded authentication token if it contains any credentials, or an empty string if // no credentials are found. func RetrieveLocalDockerRegistryAuth(image string) (string, error) { // Try to retrieve the authentication token for the image from the default local Docker config file. dockerConfig := dockerconfig.LoadDefaultConfigFile(os.Stderr) encodedAuth, err := dockercommand.RetrieveAuthTokenFromImage(dockerConfig, image) if err != nil { return "", err } // The encodedAuth can be a base64-encoded "{}" (empty JSON object) or include a server address but no credentials. // Return encodedAuth only if it contains any credentials. auth, err := registry.DecodeAuthConfig(encodedAuth) if err != nil { return "", fmt.Errorf("decode auth config: %w", err) } if auth.Username == "" && auth.Password == "" && auth.Auth == "" && auth.IdentityToken == "" && auth.RegistryToken == "" { return "", nil } return encodedAuth, nil }