diff --git a/internal/machine/docker/client.go b/internal/machine/docker/client.go index b400705c..eb358ebf 100644 --- a/internal/machine/docker/client.go +++ b/internal/machine/docker/client.go @@ -196,13 +196,27 @@ func (c *Client) RemoveContainer(ctx context.Context, id string, opts container. return err } +// PullOptions defines the options for pulling an image from a remote registry. +// This is a copy of image.PullOptions from the Docker API without the PrivilegeFunc field that is non-serialisable. +type PullOptions struct { + All bool + // RegistryAuth is the base64 encoded credentials for the registry. + RegistryAuth string + Platform string +} + type PullImageMessage struct { Message jsonmessage.JSONMessage Err error } -func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageMessage, error) { - stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image}) +func (c *Client) PullImage(ctx context.Context, image string, opts PullOptions) (<-chan PullImageMessage, error) { + optsBytes, err := json.Marshal(opts) + if err != nil { + return nil, fmt.Errorf("marshal options: %w", err) + } + + stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image, Options: optsBytes}) if err != nil { return nil, err } diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index 52ec6f24..4fe27e07 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -8,12 +8,15 @@ import ( "io" "log/slog" "net/netip" + "os" "regexp" "slices" "strconv" "strings" "github.com/distribution/reference" + dockercommand "github.com/docker/cli/cli/command" + dockerconfig "github.com/docker/cli/cli/config" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -266,7 +269,6 @@ func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerReq func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error { ctx := stream.Context() - // TODO: replace with another JSON serializable type (PullOptions.PrivilegeFunc is not serializable). var opts image.PullOptions if len(req.Options) > 0 { if err := json.Unmarshal(req.Options, &opts); err != nil { @@ -274,6 +276,14 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming } } + if opts.RegistryAuth == "" { + // Try to retrieve the authentication token for the image from the default local Docker config file. + dockerConfig := dockerconfig.LoadDefaultConfigFile(os.Stderr) + if encodedAuth, err := dockercommand.RetrieveAuthTokenFromImage(dockerConfig, req.Image); err == nil { + opts.RegistryAuth = encodedAuth + } + } + respBody, err := s.client.ImagePull(ctx, req.Image, opts) if err != nil { return status.Errorf(codes.Internal, err.Error()) diff --git a/pkg/client/container.go b/pkg/client/container.go index c1e59981..c122bc2e 100644 --- a/pkg/client/container.go +++ b/pkg/client/container.go @@ -4,12 +4,17 @@ import ( "context" "errors" "fmt" + "os" "strings" + dockercommand "github.com/docker/cli/cli/command" + dockerconfig "github.com/docker/cli/cli/config" "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/registry" dockerclient "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" + "github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/pkg/api" "google.golang.org/grpc/status" @@ -89,7 +94,14 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName StatusText: "Pulling", }) - pullCh, err := cli.Docker.PullImage(ctx, image) + opts := docker.PullOptions{} + // Try to retrieve the authentication token for the image from the default local Docker config file. + if encodedAuth, err := retrieveRegistryAuthFromDocker(image); err == nil && encodedAuth != "" { + // If RegistryAuth is empty, Uncloud daemon will try to retrieve the credentials from its own Docker config. + opts.RegistryAuth = encodedAuth + } + + pullCh, err := cli.Docker.PullImage(ctx, image, opts) if err != nil { statusErr := status.Convert(err) pw.Event(progress.Event{ @@ -143,6 +155,34 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName return nil } +// retrieveRegistryAuthFromDocker 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 retrieveRegistryAuthFromDocker(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 +} + // toPullProgressEvent converts a JSON progress message from the Docker API to a progress event. // It's based on toPullProgressEvent from Docker Compose. func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event { diff --git a/scripts/install.sh b/scripts/install.sh index f1497580..3c073615 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -166,7 +166,7 @@ RestartSec=2 NoNewPrivileges=true ProtectSystem=full ProtectControlGroups=true -ProtectHome=true +ProtectHome=read-only ProtectKernelTunables=true PrivateTmp=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK