1
0
mirror of https://github.com/distribution/distribution synced 2024-12-25 15:05:51 +01:00

feat: support custom exec-based credential helper in proxy mode (#4438)

This commit is contained in:
Milos Gajdos 2024-11-05 11:48:33 +00:00 committed by GitHub
commit f7236ab041
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 839 additions and 6 deletions

@ -600,12 +600,28 @@ type Proxy struct {
// Password of the hub user
Password string `yaml:"password"`
// Exec specifies a custom exec-based command to retrieve credentials.
// If set, Username and Password are ignored.
Exec *ExecConfig `yaml:"exec,omitempty"`
// TTL is the expiry time of the content and will be cleaned up when it expires
// if not set, defaults to 7 * 24 hours
// If set to zero, will never expire cache
TTL *time.Duration `yaml:"ttl,omitempty"`
}
type ExecConfig struct {
// Command is the command to execute.
Command string `yaml:"command"`
// Lifetime is the expiry period of the credentials. The credentials
// returned by the command is reused through the configured lifetime, then
// the command will be re-executed to retrieve new credentials.
// If set to zero, the command will be executed for every request.
// If not set, the command will only be executed once.
Lifetime *time.Duration `yaml:"lifetime,omitempty"`
}
type Validation struct {
// Enabled enables the other options in this section. This field is
// deprecated in favor of Disabled.

@ -288,6 +288,9 @@ proxy:
remoteurl: https://registry-1.docker.io
username: [username]
password: [password]
exec:
command: docker-credential-helper
lifetime: 1h
ttl: 168h
validation:
manifests:
@ -1165,7 +1168,7 @@ proxy:
```
The `proxy` structure allows a registry to be configured as a pull-through cache
to Docker Hub. See
to an upstream registry such as Docker Hub. See
[mirror](../recipes/mirror.md)
for more information. Pushing to a registry configured as a pull-through cache
is unsupported.
@ -1173,13 +1176,28 @@ is unsupported.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `remoteurl`| yes | The URL for the repository on Docker Hub. |
| `username` | no | The username registered with Docker Hub which has access to the repository. |
| `password` | no | The password used to authenticate to Docker Hub using the username specified in `username`. |
| `ttl` | no | Expire proxy cache configured in "storage" after this time. Cache 168h(7 days) by default, set to 0 to disable cache expiration, The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. |
To enable pulling private repositories (e.g. `batman/robin`), specify one of the
following authentication methods for the pull-through cache to authenticate with
the upstream registry via the [v2 Distribution registry authentication
scheme](https://distribution.github.io/distribution/spec/auth/token/).]
### `username` and `password`
The username and password used to authenticate with the upstream registry to
access the private repositories.
### `exec`
Run a custom exec-based [Docker credential helper](https://github.com/docker/docker-credential-helpers)
to retrieve the credentials to authenticate with the upstream registry.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `command` | yes | The command to execute. |
| `lifetime`| no | The expiry period of the credentials. The credentials returned by the command is reused through the configured lifetime, then the command will be re-executed to retrieve new credentials. If set to zero, the command will be executed for every request. If not set, the command will only be executed once. |
To enable pulling private repositories (e.g. `batman/robin`) specify the
username (such as `batman`) and the password for that username.
> **Note**: These private repositories are stored in the proxy cache's storage.
> Take appropriate measures to protect access to the proxy cache.

1
go.mod

@ -12,6 +12,7 @@ require (
github.com/bshuster-repo/logrus-logstash-hook v1.0.0
github.com/coreos/go-systemd/v22 v22.5.0
github.com/distribution/reference v0.6.0
github.com/docker/docker-credential-helpers v0.8.2
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
github.com/docker/go-metrics v0.0.1
github.com/go-jose/go-jose/v4 v4.0.2

2
go.sum

@ -66,6 +66,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=

@ -0,0 +1,58 @@
package proxy
import (
"net/url"
"sync"
"time"
"github.com/docker/docker-credential-helpers/client"
credspkg "github.com/docker/docker-credential-helpers/credentials"
"github.com/sirupsen/logrus"
"github.com/distribution/distribution/v3/configuration"
"github.com/distribution/distribution/v3/internal/client/auth"
)
type execCredentials struct {
m sync.Mutex
helper client.ProgramFunc
lifetime *time.Duration
creds *credspkg.Credentials
expiry time.Time
}
func (c *execCredentials) Basic(url *url.URL) (string, string) {
c.m.Lock()
defer c.m.Unlock()
now := time.Now()
if c.creds != nil && (c.lifetime == nil || now.Before(c.expiry)) {
return c.creds.Username, c.creds.Secret
}
creds, err := client.Get(c.helper, url.Host)
if err != nil {
logrus.Errorf("failed to run command: %v", err)
return "", ""
}
c.creds = creds
if c.lifetime != nil && *c.lifetime > 0 {
c.expiry = now.Add(*c.lifetime)
}
return c.creds.Username, c.creds.Secret
}
func (c *execCredentials) RefreshToken(_ *url.URL, _ string) string {
return ""
}
func (c *execCredentials) SetRefreshToken(_ *url.URL, _, _ string) {
}
func configureExecAuth(cfg configuration.ExecConfig) (auth.CredentialStore, error) {
return &execCredentials{
helper: client.NewShellProgramFunc(cfg.Command),
lifetime: cfg.Lifetime,
}, nil
}

@ -0,0 +1,175 @@
package proxy
import (
"fmt"
"io"
"net/url"
"testing"
"time"
"github.com/docker/docker-credential-helpers/client"
credspkg "github.com/docker/docker-credential-helpers/credentials"
)
type testHelper struct {
username string
secret string
err error
}
func (h *testHelper) Output() ([]byte, error) {
return []byte(fmt.Sprintf(`{"Username":%q,"Secret":%q}`, h.username, h.secret)), h.err
}
func (h *testHelper) Input(in io.Reader) {
}
var _ client.Program = (*testHelper)(nil)
func TestExecAuth(t *testing.T) {
ptrDuration := func(t time.Duration) *time.Duration { return &t }
for _, tc := range []struct {
name string
helper client.ProgramFunc
lifetime *time.Duration
currCreds *credspkg.Credentials
currExpiry time.Time
wantUsername string
wantPassword string
wantExpiry time.Time
}{{
name: "first auth without lifetime",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
wantUsername: "user",
wantPassword: "nextpass",
}, {
name: "first auth with zero lifetime",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
lifetime: ptrDuration(0),
wantUsername: "user",
wantPassword: "nextpass",
}, {
name: "first auth with lifetime",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
lifetime: ptrDuration(time.Hour),
wantUsername: "user",
wantPassword: "nextpass",
wantExpiry: time.Now().Add(time.Hour),
}, {
name: "re-auth without lifetime",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
currCreds: &credspkg.Credentials{
Username: "user",
Secret: "currpass",
},
wantUsername: "user",
wantPassword: "currpass",
}, {
name: "re-auth with zero lifetime",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
lifetime: ptrDuration(0),
currCreds: &credspkg.Credentials{
Username: "user",
Secret: "currpass",
},
wantUsername: "user",
wantPassword: "nextpass",
}, {
name: "re-auth when not expired",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
lifetime: ptrDuration(time.Hour),
currCreds: &credspkg.Credentials{
Username: "user",
Secret: "currpass",
},
currExpiry: time.Now().Add(time.Minute),
wantUsername: "user",
wantPassword: "currpass",
wantExpiry: time.Now().Add(time.Minute),
}, {
name: "re-auth when expired",
helper: func(...string) client.Program {
return &testHelper{
username: "user",
secret: "nextpass",
}
},
lifetime: ptrDuration(time.Hour),
currCreds: &credspkg.Credentials{
Username: "user",
Secret: "currpass",
},
currExpiry: time.Now().Add(-1),
wantUsername: "user",
wantPassword: "nextpass",
wantExpiry: time.Now().Add(time.Hour),
}, {
name: "exec error",
helper: func(...string) client.Program {
return &testHelper{
err: fmt.Errorf("exec error"),
}
},
lifetime: ptrDuration(time.Hour),
currCreds: &credspkg.Credentials{
Username: "user",
Secret: "currpass",
},
currExpiry: time.Now().Add(-1),
wantUsername: "",
wantPassword: "",
wantExpiry: time.Now().Add(-1),
}} {
t.Run(tc.name, func(t *testing.T) {
cs := &execCredentials{
helper: tc.helper,
lifetime: tc.lifetime,
creds: tc.currCreds,
expiry: tc.currExpiry,
}
url := &url.URL{
Scheme: "https",
Host: "example.com",
}
user, pass := cs.Basic(url)
if user != tc.wantUsername || pass != tc.wantPassword {
t.Errorf("execCredentials.Basic(%q) = (%q, %q), want (%q, %q)", url, user, pass, tc.wantUsername, tc.wantPassword)
}
// All tests should finish within seconds, so the time error should be less than a minute.
if cs.expiry.Sub(tc.wantExpiry).Abs() > time.Minute {
t.Errorf("execCredentials.expiry = %v, want %v", cs.expiry, tc.wantExpiry)
}
})
}
}

@ -114,7 +114,15 @@ func NewRegistryPullThroughCache(ctx context.Context, registry distribution.Name
}
}
cs, b, err := configureAuth(config.Username, config.Password, config.RemoteURL)
cs, b, err := func() (auth.CredentialStore, auth.CredentialStore, error) {
switch {
case config.Exec != nil:
cs, err := configureExecAuth(*config.Exec)
return cs, cs, err
default:
return configureAuth(config.Username, config.Password, config.RemoteURL)
}
}()
if err != nil {
return nil, err
}

@ -0,0 +1,20 @@
Copyright (c) 2016 David Calavera
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,114 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/docker/docker-credential-helpers/credentials"
)
// isValidCredsMessage checks if 'msg' contains invalid credentials error message.
// It returns whether the logs are free of invalid credentials errors and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername.
func isValidCredsMessage(msg string) error {
if credentials.IsCredentialsMissingServerURLMessage(msg) {
return credentials.NewErrCredentialsMissingServerURL()
}
if credentials.IsCredentialsMissingUsernameMessage(msg) {
return credentials.NewErrCredentialsMissingUsername()
}
return nil
}
// Store uses an external program to save credentials.
func Store(program ProgramFunc, creds *credentials.Credentials) error {
cmd := program(credentials.ActionStore)
buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(creds); err != nil {
return err
}
cmd.Input(buffer)
out, err := cmd.Output()
if err != nil {
if isValidErr := isValidCredsMessage(string(out)); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, strings.TrimSpace(string(out)))
}
return nil
}
// Get executes an external program to get the credentials from a native store.
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
cmd := program(credentials.ActionGet)
cmd.Input(strings.NewReader(serverURL))
out, err := cmd.Output()
if err != nil {
if credentials.IsErrCredentialsNotFoundMessage(string(out)) {
return nil, credentials.NewErrCredentialsNotFound()
}
if isValidErr := isValidCredsMessage(string(out)); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, strings.TrimSpace(string(out)))
}
resp := &credentials.Credentials{
ServerURL: serverURL,
}
if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil {
return nil, err
}
return resp, nil
}
// Erase executes a program to remove the server credentials from the native store.
func Erase(program ProgramFunc, serverURL string) error {
cmd := program(credentials.ActionErase)
cmd.Input(strings.NewReader(serverURL))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
}
return nil
}
// List executes a program to list server credentials in the native store.
func List(program ProgramFunc) (map[string]string, error) {
cmd := program(credentials.ActionList)
cmd.Input(strings.NewReader("unused"))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
}
var resp map[string]string
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
return nil, err
}
return resp, nil
}

@ -0,0 +1,54 @@
package client
import (
"io"
"os"
"os/exec"
)
// Program is an interface to execute external programs.
type Program interface {
Output() ([]byte, error)
Input(in io.Reader)
}
// ProgramFunc is a type of function that initializes programs based on arguments.
type ProgramFunc func(args ...string) Program
// NewShellProgramFunc creates programs that are executed in a Shell.
func NewShellProgramFunc(name string) ProgramFunc {
return NewShellProgramFuncWithEnv(name, nil)
}
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
return func(args ...string) Program {
return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
}
}
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
programCmd := exec.Command(commandName, args...)
if env != nil {
for k, v := range *env {
programCmd.Env = append(programCmd.Environ(), k+"="+v)
}
}
programCmd.Stderr = os.Stderr
return programCmd
}
// Shell invokes shell commands to talk with a remote credentials-helper.
type Shell struct {
cmd *exec.Cmd
}
// Output returns responses from the remote credentials-helper.
func (s *Shell) Output() ([]byte, error) {
return s.cmd.Output()
}
// Input sets the input to send to a remote credentials-helper.
func (s *Shell) Input(in io.Reader) {
s.cmd.Stdin = in
}

@ -0,0 +1,209 @@
package credentials
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
)
// Action defines the name of an action (sub-command) supported by a
// credential-helper binary. It is an alias for "string", and mostly
// for convenience.
type Action = string
// List of actions (sub-commands) supported by credential-helper binaries.
const (
ActionStore Action = "store"
ActionGet Action = "get"
ActionErase Action = "erase"
ActionList Action = "list"
ActionVersion Action = "version"
)
// Credentials holds the information shared between docker and the credentials store.
type Credentials struct {
ServerURL string
Username string
Secret string
}
// isValid checks the integrity of Credentials object such that no credentials lack
// a server URL or a username.
// It returns whether the credentials are valid and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername
func (c *Credentials) isValid() (bool, error) {
if len(c.ServerURL) == 0 {
return false, NewErrCredentialsMissingServerURL()
}
if len(c.Username) == 0 {
return false, NewErrCredentialsMissingUsername()
}
return true, nil
}
// CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling.
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
var CredsLabel = "Docker Credentials"
// SetCredsLabel is a simple setter for CredsLabel
func SetCredsLabel(label string) {
CredsLabel = label
}
// Serve initializes the credentials-helper and parses the action argument.
// This function is designed to be called from a command line interface.
// It uses os.Args[1] as the key for the action.
// It uses os.Stdin as input and os.Stdout as output.
// This function terminates the program with os.Exit(1) if there is an error.
func Serve(helper Helper) {
if len(os.Args) != 2 {
_, _ = fmt.Fprintln(os.Stdout, usage())
os.Exit(1)
}
switch os.Args[1] {
case "--version", "-v":
_ = PrintVersion(os.Stdout)
os.Exit(0)
case "--help", "-h":
_, _ = fmt.Fprintln(os.Stdout, usage())
os.Exit(0)
}
if err := HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout); err != nil {
_, _ = fmt.Fprintln(os.Stdout, err)
os.Exit(1)
}
}
func usage() string {
return fmt.Sprintf("Usage: %s <store|get|erase|list|version>", Name)
}
// HandleCommand runs a helper to execute a credential action.
func HandleCommand(helper Helper, action Action, in io.Reader, out io.Writer) error {
switch action {
case ActionStore:
return Store(helper, in)
case ActionGet:
return Get(helper, in, out)
case ActionErase:
return Erase(helper, in)
case ActionList:
return List(helper, out)
case ActionVersion:
return PrintVersion(out)
default:
return fmt.Errorf("%s: unknown action: %s", Name, action)
}
}
// Store uses a helper and an input reader to save credentials.
// The reader must contain the JSON serialization of a Credentials struct.
func Store(helper Helper, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
buffer := new(bytes.Buffer)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil && err != io.EOF {
return err
}
var creds Credentials
if err := json.NewDecoder(buffer).Decode(&creds); err != nil {
return err
}
if ok, err := creds.isValid(); !ok {
return err
}
return helper.Add(&creds)
}
// Get retrieves the credentials for a given server url.
// The reader must contain the server URL to search.
// The writer is used to write the JSON serialization of the credentials.
func Get(helper Helper, reader io.Reader, writer io.Writer) error {
scanner := bufio.NewScanner(reader)
buffer := new(bytes.Buffer)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil && err != io.EOF {
return err
}
serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
username, secret, err := helper.Get(serverURL)
if err != nil {
return err
}
buffer.Reset()
err = json.NewEncoder(buffer).Encode(Credentials{
ServerURL: serverURL,
Username: username,
Secret: secret,
})
if err != nil {
return err
}
_, _ = fmt.Fprint(writer, buffer.String())
return nil
}
// Erase removes credentials from the store.
// The reader must contain the server URL to remove.
func Erase(helper Helper, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
buffer := new(bytes.Buffer)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil && err != io.EOF {
return err
}
serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
return helper.Delete(serverURL)
}
// List returns all the serverURLs of keys in
// the OS store as a list of strings
func List(helper Helper, writer io.Writer) error {
accts, err := helper.List()
if err != nil {
return err
}
return json.NewEncoder(writer).Encode(accts)
}
// PrintVersion outputs the current version.
func PrintVersion(writer io.Writer) error {
_, _ = fmt.Fprintf(writer, "%s (%s) %s\n", Name, Package, Version)
return nil
}

@ -0,0 +1,124 @@
package credentials
import (
"errors"
"strings"
)
const (
// ErrCredentialsNotFound standardizes the not found error, so every helper returns
// the same message and docker can handle it properly.
errCredentialsNotFoundMessage = "credentials not found in native keychain"
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
// invalid credentials or credentials management operations
errCredentialsMissingServerURLMessage = "no credentials server URL"
errCredentialsMissingUsernameMessage = "no credentials username"
)
// errCredentialsNotFound represents an error
// raised when credentials are not in the store.
type errCredentialsNotFound struct{}
// Error returns the standard error message
// for when the credentials are not in the store.
func (errCredentialsNotFound) Error() string {
return errCredentialsNotFoundMessage
}
// NotFound implements the [ErrNotFound][errdefs.ErrNotFound] interface.
//
// [errdefs.ErrNotFound]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrNotFound
func (errCredentialsNotFound) NotFound() {}
// NewErrCredentialsNotFound creates a new error
// for when the credentials are not in the store.
func NewErrCredentialsNotFound() error {
return errCredentialsNotFound{}
}
// IsErrCredentialsNotFound returns true if the error
// was caused by not having a set of credentials in a store.
func IsErrCredentialsNotFound(err error) bool {
var target errCredentialsNotFound
return errors.As(err, &target)
}
// IsErrCredentialsNotFoundMessage returns true if the error
// was caused by not having a set of credentials in a store.
//
// This function helps to check messages returned by an
// external program via its standard output.
func IsErrCredentialsNotFoundMessage(err string) bool {
return strings.TrimSpace(err) == errCredentialsNotFoundMessage
}
// errCredentialsMissingServerURL represents an error raised
// when the credentials object has no server URL or when no
// server URL is provided to a credentials operation requiring
// one.
type errCredentialsMissingServerURL struct{}
func (errCredentialsMissingServerURL) Error() string {
return errCredentialsMissingServerURLMessage
}
// InvalidParameter implements the [ErrInvalidParameter][errdefs.ErrInvalidParameter]
// interface.
//
// [errdefs.ErrInvalidParameter]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrInvalidParameter
func (errCredentialsMissingServerURL) InvalidParameter() {}
// errCredentialsMissingUsername represents an error raised
// when the credentials object has no username or when no
// username is provided to a credentials operation requiring
// one.
type errCredentialsMissingUsername struct{}
func (errCredentialsMissingUsername) Error() string {
return errCredentialsMissingUsernameMessage
}
// InvalidParameter implements the [ErrInvalidParameter][errdefs.ErrInvalidParameter]
// interface.
//
// [errdefs.ErrInvalidParameter]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrInvalidParameter
func (errCredentialsMissingUsername) InvalidParameter() {}
// NewErrCredentialsMissingServerURL creates a new error for
// errCredentialsMissingServerURL.
func NewErrCredentialsMissingServerURL() error {
return errCredentialsMissingServerURL{}
}
// NewErrCredentialsMissingUsername creates a new error for
// errCredentialsMissingUsername.
func NewErrCredentialsMissingUsername() error {
return errCredentialsMissingUsername{}
}
// IsCredentialsMissingServerURL returns true if the error
// was an errCredentialsMissingServerURL.
func IsCredentialsMissingServerURL(err error) bool {
var target errCredentialsMissingServerURL
return errors.As(err, &target)
}
// IsCredentialsMissingServerURLMessage checks for an
// errCredentialsMissingServerURL in the error message.
func IsCredentialsMissingServerURLMessage(err string) bool {
return strings.TrimSpace(err) == errCredentialsMissingServerURLMessage
}
// IsCredentialsMissingUsername returns true if the error
// was an errCredentialsMissingUsername.
func IsCredentialsMissingUsername(err error) bool {
var target errCredentialsMissingUsername
return errors.As(err, &target)
}
// IsCredentialsMissingUsernameMessage checks for an
// errCredentialsMissingUsername in the error message.
func IsCredentialsMissingUsernameMessage(err string) bool {
return strings.TrimSpace(err) == errCredentialsMissingUsernameMessage
}

@ -0,0 +1,14 @@
package credentials
// Helper is the interface a credentials store helper must implement.
type Helper interface {
// Add appends credentials to the store.
Add(*Credentials) error
// Delete removes credentials from the store.
Delete(serverURL string) error
// Get retrieves credentials from the store.
// It returns username and secret as strings.
Get(serverURL string) (string, string, error)
// List returns the stored serverURLs and their associated usernames.
List() (map[string]string, error)
}

@ -0,0 +1,16 @@
package credentials
var (
// Name is filled at linking time
Name = ""
// Package is filled at linking time
Package = "github.com/docker/docker-credential-helpers"
// Version holds the complete version number. Filled in at linking time.
Version = "v0.0.0+unknown"
// Revision is filled with the VCS (e.g. git) revision being used to build
// the program at linking time.
Revision = ""
)

4
vendor/modules.txt vendored

@ -177,6 +177,10 @@ github.com/dgryski/go-rendezvous
# github.com/distribution/reference v0.6.0
## explicit; go 1.20
github.com/distribution/reference
# github.com/docker/docker-credential-helpers v0.8.2
## explicit; go 1.19
github.com/docker/docker-credential-helpers/client
github.com/docker/docker-credential-helpers/credentials
# github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
## explicit
github.com/docker/go-events