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:
commit
f7236ab041
@ -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
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
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=
|
||||
|
58
registry/proxy/proxyauth_exec.go
Normal file
58
registry/proxy/proxyauth_exec.go
Normal file
@ -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
|
||||
}
|
175
registry/proxy/proxyauth_exec_test.go
Normal file
175
registry/proxy/proxyauth_exec_test.go
Normal file
@ -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
|
||||
}
|
||||
|
20
vendor/github.com/docker/docker-credential-helpers/LICENSE
generated
vendored
Normal file
20
vendor/github.com/docker/docker-credential-helpers/LICENSE
generated
vendored
Normal file
@ -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.
|
114
vendor/github.com/docker/docker-credential-helpers/client/client.go
generated
vendored
Normal file
114
vendor/github.com/docker/docker-credential-helpers/client/client.go
generated
vendored
Normal file
@ -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
|
||||
}
|
54
vendor/github.com/docker/docker-credential-helpers/client/command.go
generated
vendored
Normal file
54
vendor/github.com/docker/docker-credential-helpers/client/command.go
generated
vendored
Normal file
@ -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
|
||||
}
|
209
vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go
generated
vendored
Normal file
209
vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go
generated
vendored
Normal file
@ -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
|
||||
}
|
124
vendor/github.com/docker/docker-credential-helpers/credentials/error.go
generated
vendored
Normal file
124
vendor/github.com/docker/docker-credential-helpers/credentials/error.go
generated
vendored
Normal file
@ -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
|
||||
}
|
14
vendor/github.com/docker/docker-credential-helpers/credentials/helper.go
generated
vendored
Normal file
14
vendor/github.com/docker/docker-credential-helpers/credentials/helper.go
generated
vendored
Normal file
@ -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)
|
||||
}
|
16
vendor/github.com/docker/docker-credential-helpers/credentials/version.go
generated
vendored
Normal file
16
vendor/github.com/docker/docker-credential-helpers/credentials/version.go
generated
vendored
Normal file
@ -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
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
|
||||
|
Loading…
Reference in New Issue
Block a user