mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
fix: use local and remote Docker credentials to pull image from private registry
This commit is contained in:
@@ -196,13 +196,27 @@ func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.
|
|||||||
return err
|
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 {
|
type PullImageMessage struct {
|
||||||
Message jsonmessage.JSONMessage
|
Message jsonmessage.JSONMessage
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageMessage, error) {
|
func (c *Client) PullImage(ctx context.Context, image string, opts PullOptions) (<-chan PullImageMessage, error) {
|
||||||
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image})
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"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"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"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 {
|
func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
|
||||||
ctx := stream.Context()
|
ctx := stream.Context()
|
||||||
|
|
||||||
// TODO: replace with another JSON serializable type (PullOptions.PrivilegeFunc is not serializable).
|
|
||||||
var opts image.PullOptions
|
var opts image.PullOptions
|
||||||
if len(req.Options) > 0 {
|
if len(req.Options) > 0 {
|
||||||
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
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)
|
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(codes.Internal, err.Error())
|
return status.Errorf(codes.Internal, err.Error())
|
||||||
|
|||||||
+41
-1
@@ -4,12 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"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/compose/v2/pkg/progress"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/registry"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@@ -89,7 +94,14 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName
|
|||||||
StatusText: "Pulling",
|
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 {
|
if err != nil {
|
||||||
statusErr := status.Convert(err)
|
statusErr := status.Convert(err)
|
||||||
pw.Event(progress.Event{
|
pw.Event(progress.Event{
|
||||||
@@ -143,6 +155,34 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName
|
|||||||
return nil
|
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.
|
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
|
||||||
// It's based on toPullProgressEvent from Docker Compose.
|
// It's based on toPullProgressEvent from Docker Compose.
|
||||||
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
||||||
|
|||||||
+1
-1
@@ -166,7 +166,7 @@ RestartSec=2
|
|||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
ProtectControlGroups=true
|
ProtectControlGroups=true
|
||||||
ProtectHome=true
|
ProtectHome=read-only
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
||||||
|
|||||||
Reference in New Issue
Block a user