Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cli/command/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package auth

import (
"fmt"

"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/server"
"github.com/spf13/cobra"
)

func NewAuthCommand() *cobra.Command {
authCmd := &cobra.Command{
Use: "auth",
}

proxyServerCmd := &cobra.Command{
Use: "credential-server",
RunE: func(cmd *cobra.Command, args []string) error {
file := config.LoadDefaultConfigFile(cmd.ErrOrStderr())
fmt.Fprint(cmd.OutOrStdout(), "Starting credential server...\n")
err := server.StartCredentialsServer(cmd.Context(), config.Dir(), file)
if err != nil {
return err
}
return nil
},
}
authCmd.AddCommand(proxyServerCmd)

return authCmd
}
3 changes: 3 additions & 0 deletions cli/command/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/auth"
"github.com/docker/cli/cli/command/builder"
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
Expand Down Expand Up @@ -55,6 +56,8 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
trust.NewTrustCommand(dockerCli),
volume.NewVolumeCommand(dockerCli),

auth.NewAuthCommand(),

// orchestration (swarm) commands
config.NewConfigCommand(dockerCli),
node.NewNodeCommand(dockerCli),
Expand Down
5 changes: 5 additions & 0 deletions cli/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/server"
"github.com/docker/cli/cli/config/types"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -135,6 +136,10 @@ func load(configDir string) (*configfile.ConfigFile, error) {
filename := filepath.Join(configDir, ConfigFileName)
configFile := configfile.New(filename)

if addr, err := server.CheckCredentialServer(configDir); err == nil {
configFile.SocketCredentialStoreAddr = addr
}

file, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) {
Expand Down
56 changes: 30 additions & 26 deletions cli/config/configfile/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,33 @@ import (

// ConfigFile ~/.docker/config.json file info
type ConfigFile struct {
AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
NetworksFormat string `json:"networksFormat,omitempty"`
PluginsFormat string `json:"pluginsFormat,omitempty"`
VolumesFormat string `json:"volumesFormat,omitempty"`
StatsFormat string `json:"statsFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
Filename string `json:"-"` // Note: for internal use only
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
ServicesFormat string `json:"servicesFormat,omitempty"`
TasksFormat string `json:"tasksFormat,omitempty"`
SecretFormat string `json:"secretFormat,omitempty"`
ConfigFormat string `json:"configFormat,omitempty"`
NodesFormat string `json:"nodesFormat,omitempty"`
PruneFilters []string `json:"pruneFilters,omitempty"`
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
Plugins map[string]map[string]string `json:"plugins,omitempty"`
Aliases map[string]string `json:"aliases,omitempty"`
Features map[string]string `json:"features,omitempty"`
AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
NetworksFormat string `json:"networksFormat,omitempty"`
PluginsFormat string `json:"pluginsFormat,omitempty"`
VolumesFormat string `json:"volumesFormat,omitempty"`
StatsFormat string `json:"statsFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
Filename string `json:"-"` // Note: for internal use only
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
ServicesFormat string `json:"servicesFormat,omitempty"`
TasksFormat string `json:"tasksFormat,omitempty"`
SecretFormat string `json:"secretFormat,omitempty"`
ConfigFormat string `json:"configFormat,omitempty"`
NodesFormat string `json:"nodesFormat,omitempty"`
PruneFilters []string `json:"pruneFilters,omitempty"`
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
Plugins map[string]map[string]string `json:"plugins,omitempty"`
Aliases map[string]string `json:"aliases,omitempty"`
Features map[string]string `json:"features,omitempty"`
SocketCredentialStoreAddr string `json:"-"`
}

// ProxyConfig contains proxy configuration settings
Expand Down Expand Up @@ -254,6 +255,9 @@ func decodeAuth(authStr string) (string, string, error) {
// GetCredentialsStore returns a new credentials store from the settings in the
// configuration file
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
if configFile.SocketCredentialStoreAddr != "" {
return credentials.NewSocketStore(configFile.SocketCredentialStoreAddr)
}
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
return newNativeStore(configFile, helper)
}
Expand Down
108 changes: 108 additions & 0 deletions cli/config/credentials/socket_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package credentials

import (
"bytes"
"context"
"encoding/json"
"errors"
"net"
"net/http"
"net/url"

"github.com/docker/cli/cli/config/types"
)

type socketStore struct {
socketPath string
client http.Client
}

// Erase implements Store.
func (s *socketStore) Erase(serverAddress string) error {
q := url.Values{"key": {serverAddress}}
req, err := http.NewRequest(http.MethodDelete, "http://localhost/credentials?"+q.Encode(), nil)
if err != nil {
return err
}
resp, err := s.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New("failed to erase credentials")
}
return nil
}

// Get implements Store.
func (s *socketStore) Get(serverAddress string) (types.AuthConfig, error) {
q := url.Values{"key": {serverAddress}}
req, err := http.NewRequest(http.MethodGet, "http://localhost/credentials?"+q.Encode(), nil)
if err != nil {
return types.AuthConfig{}, err
}
resp, err := s.client.Do(req)
if err != nil {
return types.AuthConfig{}, err
}
defer resp.Body.Close()

var authConfig types.AuthConfig
if err := json.NewDecoder(resp.Body).Decode(&authConfig); err != nil {
return types.AuthConfig{}, err
}
return authConfig, nil
}

// GetAll implements Store.
func (s *socketStore) GetAll() (map[string]types.AuthConfig, error) {
req, err := http.NewRequest(http.MethodGet, "http://localhost/credentials", nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var authConfigs map[string]types.AuthConfig
if err := json.NewDecoder(resp.Body).Decode(&authConfigs); err != nil {
return nil, err
}
return authConfigs, nil
}

// Store implements Store.
func (s *socketStore) Store(authConfig types.AuthConfig) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(authConfig); err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, "http://localhost/credentials", &buf)
if err != nil {
return err
}
resp, err := s.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("failed to store credentials")
}
return nil
}

func NewSocketStore(socketPath string) Store {
return &socketStore{
socketPath: socketPath,
client: http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
},
}
}
127 changes: 127 additions & 0 deletions cli/config/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package server

import (
"context"
"encoding/json"
"log"
"net"
"net/http"
"path/filepath"
"sync/atomic"
"time"

"github.com/docker/cli/cli/config/types"
)

const CredentialServerSocket = "docker_cli_credential_server.sock"

// GetCredentialServerSocket returns the path to the Unix socket
// configDir is the directory where the docker configuration file is stored
func GetCredentialServerSocket(configDir string) string {
return filepath.Join(configDir, "run", CredentialServerSocket)
}

type CredentialConfig interface {
GetAuthConfig(serverAddress string) (types.AuthConfig, error)
GetAllCredentials() (map[string]types.AuthConfig, error)
}

// CheckCredentialServer checks if the credential server is running
// in the configDir directory by attempting to connect to the Unix socket.
// It returns the absolute path of the Unix socket if the server is running.
func CheckCredentialServer(configDir string) (string, error) {
addr, err := net.ResolveUnixAddr("unix", GetCredentialServerSocket(configDir))
if err != nil {
return "", err
}
_, err = net.Dial(addr.Network(), addr.String())
return addr.String(), err
}

// StartCredentialsServer hosts a Unix socket server that exposes
// the credentials store to the Docker CLI running in a container.
func StartCredentialsServer(ctx context.Context, configDir string, config CredentialConfig) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

l, err := net.ListenUnix("unix", &net.UnixAddr{
Name: GetCredentialServerSocket(configDir),
Net: "unix",
})
if err != nil {
return err
}

mux := http.NewServeMux()
mux.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
log.Println("GET /credentials")
if key := r.URL.Query().Get("key"); key != "" {
log.Printf("GET /credentials?key=%s", key)
credential, err := config.GetAuthConfig(key)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(credential); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
// Get credentials
credentials, err := config.GetAllCredentials()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Write credentials
err = json.NewEncoder(w).Encode(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
case http.MethodPost:
// Store credentials
case http.MethodDelete:
// Erase credentials
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
})

timer := time.NewTimer(1000 * time.Second)
activeConnections := atomic.Int32{}
s := http.Server{
BaseContext: func(l net.Listener) context.Context { return ctx },
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
ConnState: func(c net.Conn, cs http.ConnState) {
switch cs {
case http.StateActive, http.StateNew, http.StateHijacked:
if activeConnections.Load() == 0 {
timer.Stop()
}
activeConnections.Add(1)
case http.StateClosed, http.StateIdle:
if activeConnections.Load() == 0 {
timer.Reset(10 * time.Second)
}
activeConnections.Add(-1)
}
},
Handler: mux,
}

go func() {
select {
case <-ctx.Done():
case <-timer.C:
}
s.Shutdown(ctx)
}()

return s.Serve(l)
}
Loading