mirror of
https://github.com/distribution/distribution.git
synced 2025-07-17 00:44:34 +00: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 of the hub user
|
||||||
Password string `yaml:"password"`
|
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
|
// 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 not set, defaults to 7 * 24 hours
|
||||||
// If set to zero, will never expire cache
|
// If set to zero, will never expire cache
|
||||||
TTL *time.Duration `yaml:"ttl,omitempty"`
|
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 {
|
type Validation struct {
|
||||||
// Enabled enables the other options in this section. This field is
|
// Enabled enables the other options in this section. This field is
|
||||||
// deprecated in favor of Disabled.
|
// deprecated in favor of Disabled.
|
||||||
|
@ -288,6 +288,9 @@ proxy:
|
|||||||
remoteurl: https://registry-1.docker.io
|
remoteurl: https://registry-1.docker.io
|
||||||
username: [username]
|
username: [username]
|
||||||
password: [password]
|
password: [password]
|
||||||
|
exec:
|
||||||
|
command: docker-credential-helper
|
||||||
|
lifetime: 1h
|
||||||
ttl: 168h
|
ttl: 168h
|
||||||
validation:
|
validation:
|
||||||
manifests:
|
manifests:
|
||||||
@ -1165,7 +1168,7 @@ proxy:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `proxy` structure allows a registry to be configured as a pull-through cache
|
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)
|
[mirror](../recipes/mirror.md)
|
||||||
for more information. Pushing to a registry configured as a pull-through cache
|
for more information. Pushing to a registry configured as a pull-through cache
|
||||||
is unsupported.
|
is unsupported.
|
||||||
@ -1173,13 +1176,28 @@ is unsupported.
|
|||||||
| Parameter | Required | Description |
|
| Parameter | Required | Description |
|
||||||
|-----------|----------|-------------------------------------------------------|
|
|-----------|----------|-------------------------------------------------------|
|
||||||
| `remoteurl`| yes | The URL for the repository on Docker Hub. |
|
| `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. |
|
| `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.
|
> **Note**: These private repositories are stored in the proxy cache's storage.
|
||||||
> Take appropriate measures to protect access to the proxy cache.
|
> 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/bshuster-repo/logrus-logstash-hook v1.0.0
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0
|
github.com/coreos/go-systemd/v22 v22.5.0
|
||||||
github.com/distribution/reference v0.6.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-events v0.0.0-20190806004212-e31b211e4f1c
|
||||||
github.com/docker/go-metrics v0.0.1
|
github.com/docker/go-metrics v0.0.1
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2
|
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/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 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
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 h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
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=
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
# github.com/distribution/reference v0.6.0
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/distribution/reference
|
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
|
# github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
|
||||||
## explicit
|
## explicit
|
||||||
github.com/docker/go-events
|
github.com/docker/go-events
|
||||||
|
Loading…
Reference in New Issue
Block a user