diff --git a/go.mod b/go.mod index 94102a961..f611b71a2 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( ) require ( + github.com/DefangLabs/docker-credential-digitalocean v0.0.0-20240610205821-a2ef21e94d2a github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.22 github.com/containerd/containerd v1.7.18 ) diff --git a/go.sum b/go.sum index 31a7002f2..624bab15e 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,10 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DefangLabs/docker-credential-digitalocean v0.0.0-20240610200406-4d010eb955c7 h1:jMHbuQDsR9b88eSR67RST0N6KzPw2AMAjU/7CMXI10o= +github.com/DefangLabs/docker-credential-digitalocean v0.0.0-20240610200406-4d010eb955c7/go.mod h1:CxvWG/LMHXIzKpqjwzHE2VOJmVV/37DJSCls+6Ldh2g= +github.com/DefangLabs/docker-credential-digitalocean v0.0.0-20240610205821-a2ef21e94d2a h1:4DjV2A5Cajan+V1jUi2UeQjq5CgWrXVVeC4hzs7aJfI= +github.com/DefangLabs/docker-credential-digitalocean v0.0.0-20240610205821-a2ef21e94d2a/go.mod h1:CxvWG/LMHXIzKpqjwzHE2VOJmVV/37DJSCls+6Ldh2g= github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.22 h1:HevuUpLsTedep2D6wnIp6AAJbVgP0BiVxaMt3HXeOyA= github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.22/go.mod h1:nzCpg7DFIIkQIZB3mdUPXVvqQ5f/GahA6xgWXTjnK7w= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= diff --git a/pkg/creds/creds.go b/pkg/creds/creds.go index 294fe85ef..7a8ed3b5a 100644 --- a/pkg/creds/creds.go +++ b/pkg/creds/creds.go @@ -19,6 +19,7 @@ package creds import ( "io" + do "github.com/DefangLabs/docker-credential-digitalocean/pkg/credhelper" ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" gitlab "github.com/ePirat/docker-credential-gitlabci/pkg/credhelper" @@ -34,5 +35,6 @@ func GetKeychain() authn.Keychain { authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard))), authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()), authn.NewKeychainFromHelper(gitlab.NewGitLabCredentialsHelper()), + authn.NewKeychainFromHelper(do.NewDigitalOceanCredentialHelper(do.WithReadWrite(), do.WithExpiry(3600))), ) } diff --git a/vendor/github.com/DefangLabs/docker-credential-digitalocean/LICENSE b/vendor/github.com/DefangLabs/docker-credential-digitalocean/LICENSE new file mode 100644 index 000000000..b97658177 --- /dev/null +++ b/vendor/github.com/DefangLabs/docker-credential-digitalocean/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Defang Software Labs + +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. diff --git a/vendor/github.com/DefangLabs/docker-credential-digitalocean/pkg/credhelper/credhelper.go b/vendor/github.com/DefangLabs/docker-credential-digitalocean/pkg/credhelper/credhelper.go new file mode 100644 index 000000000..52cce27d5 --- /dev/null +++ b/vendor/github.com/DefangLabs/docker-credential-digitalocean/pkg/credhelper/credhelper.go @@ -0,0 +1,125 @@ +package credhelper + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "slices" + "strconv" +) + +const ( + doRegistry = "registry.digitalocean.com" +) + +var ( + apiEndpoint = "api.digitalocean.com" + client = http.DefaultClient +) + +type DigitalOceanCredentialHelper struct { + // The duration in seconds that the returned registry credentials will be valid. If not set or 0, the credentials will not expire. + ExpirySeconds int + // By default, the registry credentials allow for read-only access. Set this query parameter to true to obtain read-write credentials. + ReadWrite bool + + token string +} + +type Option func(*DigitalOceanCredentialHelper) + +// NewDigitalOceanCredentialHelper creates a new credential helper with the given options. +// By default, the API token is read from the DIGITALOCEAN_TOKEN environment variable, +// but it can be overridden with the WithToken option. +// The ExpirySeconds and ReadWrite options default to 0 (never) and false, respectively. +func NewDigitalOceanCredentialHelper(options ...Option) *DigitalOceanCredentialHelper { + do := &DigitalOceanCredentialHelper{ + token: os.Getenv("DIGITALOCEAN_TOKEN"), + } + for _, option := range options { + option(do) + } + return do +} + +func WithExpiry(seconds int) Option { + return func(d *DigitalOceanCredentialHelper) { + d.ExpirySeconds = seconds + } +} + +func WithReadWrite() Option { + return func(d *DigitalOceanCredentialHelper) { + d.ReadWrite = true + } +} + +func WithToken(token string) Option { + return func(d *DigitalOceanCredentialHelper) { + d.token = token + } +} + +func (d DigitalOceanCredentialHelper) Get(serverURL string) (string, string, error) { + serverUrl, err := url.Parse("https://" + serverURL) + if err != nil { + return "", "", fmt.Errorf("failed to parse registry URL: %w", err) + } + if serverUrl.Hostname() != doRegistry { + return "", "", fmt.Errorf("not a Digital Ocean registry: %s", serverUrl.Hostname()) + } + + query := url.Values{} + if d.ExpirySeconds > 0 { + query.Set("expiry_seconds", strconv.Itoa(d.ExpirySeconds)) + } + if d.ReadWrite { + query.Set("read_write", "true") + } + + api := url.URL{ + Scheme: "https", + Host: apiEndpoint, + Path: "/v2/registry/docker-credentials", + RawQuery: query.Encode(), + } + req, err := http.NewRequest("GET", api.String(), nil) + if err != nil { + return "", "", fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+d.token) + res, err := client.Do(req) + if err != nil { + return "", "", fmt.Errorf("failed to get credentials from API: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return "", "", fmt.Errorf("failed to get credentials from API: %s", res.Status) + } + var creds dockerCredentialsResponse + if err := json.NewDecoder(res.Body).Decode(&creds); err != nil { + return "", "", fmt.Errorf("failed to decode credentials response: %w", err) + } + + registry := serverUrl.Hostname() + auth := creds.Auths[registry].Auth + if len(auth) == 0 { + return "", "", fmt.Errorf("no credentials for registry %q", registry) + } + colon := slices.Index(auth, ':') + if colon == -1 { + return "", "", fmt.Errorf("invalid credentials") + } + user := string(auth[:colon]) + pass := string(auth[colon+1:]) + return user, pass, nil +} + +type dockerCredentialsResponse struct { + Auths map[string]struct { + Auth []byte `json:"auth"` + } `json:"auths"` +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7ef3089c8..e2fd70fd1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -121,6 +121,9 @@ github.com/Azure/go-autorest/logger # github.com/Azure/go-autorest/tracing v0.6.0 ## explicit; go 1.12 github.com/Azure/go-autorest/tracing +# github.com/DefangLabs/docker-credential-digitalocean v0.0.0-20240610205821-a2ef21e94d2a +## explicit; go 1.21 +github.com/DefangLabs/docker-credential-digitalocean/pkg/credhelper # github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.22 ## explicit; go 1.21 github.com/GoogleCloudPlatform/docker-credential-gcr/v2