Skip to content

Commit 1ce3e62

Browse files
committed
fix: use local and remote Docker credentials to pull image from private registry
1 parent 4be8339 commit 1ce3e62

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

internal/machine/docker/client.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,27 @@ func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.
196196
return err
197197
}
198198

199+
// PullOptions defines the options for pulling an image from a remote registry.
200+
// This is a copy of image.PullOptions from the Docker API without the PrivilegeFunc field that is non-serialisable.
201+
type PullOptions struct {
202+
All bool
203+
// RegistryAuth is the base64 encoded credentials for the registry.
204+
RegistryAuth string
205+
Platform string
206+
}
207+
199208
type PullImageMessage struct {
200209
Message jsonmessage.JSONMessage
201210
Err error
202211
}
203212

204-
func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageMessage, error) {
205-
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image})
213+
func (c *Client) PullImage(ctx context.Context, image string, opts PullOptions) (<-chan PullImageMessage, error) {
214+
optsBytes, err := json.Marshal(opts)
215+
if err != nil {
216+
return nil, fmt.Errorf("marshal options: %w", err)
217+
}
218+
219+
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image, Options: optsBytes})
206220
if err != nil {
207221
return nil, err
208222
}

internal/machine/docker/server.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"io"
99
"log/slog"
1010
"net/netip"
11+
"os"
1112
"regexp"
1213
"slices"
1314
"strconv"
1415
"strings"
1516

1617
"github.com/distribution/reference"
18+
dockercommand "github.com/docker/cli/cli/command"
19+
dockerconfig "github.com/docker/cli/cli/config"
1720
"github.com/docker/docker/api/types"
1821
"github.com/docker/docker/api/types/container"
1922
"github.com/docker/docker/api/types/filters"
@@ -266,14 +269,21 @@ func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerReq
266269
func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
267270
ctx := stream.Context()
268271

269-
// TODO: replace with another JSON serializable type (PullOptions.PrivilegeFunc is not serializable).
270272
var opts image.PullOptions
271273
if len(req.Options) > 0 {
272274
if err := json.Unmarshal(req.Options, &opts); err != nil {
273275
return status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
274276
}
275277
}
276278

279+
if opts.RegistryAuth == "" {
280+
// Try to retrieve the authentication token for the image from the default local Docker config file.
281+
dockerConfig := dockerconfig.LoadDefaultConfigFile(os.Stderr)
282+
if encodedAuth, err := dockercommand.RetrieveAuthTokenFromImage(dockerConfig, req.Image); err == nil {
283+
opts.RegistryAuth = encodedAuth
284+
}
285+
}
286+
277287
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
278288
if err != nil {
279289
return status.Errorf(codes.Internal, err.Error())

pkg/client/container.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"os"
78
"strings"
89

10+
dockercommand "github.com/docker/cli/cli/command"
11+
dockerconfig "github.com/docker/cli/cli/config"
912
"github.com/docker/compose/v2/pkg/progress"
1013
"github.com/docker/docker/api/types/container"
14+
"github.com/docker/docker/api/types/registry"
1115
dockerclient "github.com/docker/docker/client"
1216
"github.com/docker/docker/pkg/jsonmessage"
17+
"github.com/psviderski/uncloud/internal/machine/docker"
1318
"github.com/psviderski/uncloud/internal/secret"
1419
"github.com/psviderski/uncloud/pkg/api"
1520
"google.golang.org/grpc/status"
@@ -89,7 +94,14 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName
8994
StatusText: "Pulling",
9095
})
9196

92-
pullCh, err := cli.Docker.PullImage(ctx, image)
97+
opts := docker.PullOptions{}
98+
// Try to retrieve the authentication token for the image from the default local Docker config file.
99+
if encodedAuth, err := retrieveRegistryAuthFromDocker(image); err == nil && encodedAuth != "" {
100+
// If RegistryAuth is empty, Uncloud daemon will try to retrieve the credentials from its own Docker config.
101+
opts.RegistryAuth = encodedAuth
102+
}
103+
104+
pullCh, err := cli.Docker.PullImage(ctx, image, opts)
93105
if err != nil {
94106
statusErr := status.Convert(err)
95107
pw.Event(progress.Event{
@@ -143,6 +155,34 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName
143155
return nil
144156
}
145157

158+
// retrieveRegistryAuthFromDocker retrieves the authentication token for the specified image from the local Docker
159+
// config file. It returns the encoded authentication token if it contains any credentials, or an empty string if
160+
// no credentials are found.
161+
func retrieveRegistryAuthFromDocker(image string) (string, error) {
162+
// Try to retrieve the authentication token for the image from the default local Docker config file.
163+
dockerConfig := dockerconfig.LoadDefaultConfigFile(os.Stderr)
164+
encodedAuth, err := dockercommand.RetrieveAuthTokenFromImage(dockerConfig, image)
165+
if err != nil {
166+
return "", err
167+
}
168+
// The encodedAuth can be a base64-encoded "{}" (empty JSON object) or include a server address but no credentials.
169+
// Return encodedAuth only if it contains any credentials.
170+
auth, err := registry.DecodeAuthConfig(encodedAuth)
171+
if err != nil {
172+
return "", fmt.Errorf("decode auth config: %w", err)
173+
}
174+
175+
if auth.Username == "" &&
176+
auth.Password == "" &&
177+
auth.Auth == "" &&
178+
auth.IdentityToken == "" &&
179+
auth.RegistryToken == "" {
180+
return "", nil
181+
}
182+
183+
return encodedAuth, nil
184+
}
185+
146186
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
147187
// It's based on toPullProgressEvent from Docker Compose.
148188
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {

scripts/install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ RestartSec=2
166166
NoNewPrivileges=true
167167
ProtectSystem=full
168168
ProtectControlGroups=true
169-
ProtectHome=true
169+
ProtectHome=read-only
170170
ProtectKernelTunables=true
171171
PrivateTmp=true
172172
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK

0 commit comments

Comments
 (0)