diff --git a/go.mod b/go.mod index 105f4dbca..9bd366319 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,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.27 ) diff --git a/go.sum b/go.sum index 19846efd1..015e7b1f6 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,10 @@ github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= 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 0bde2ce77..1d7196b62 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -125,6 +125,9 @@ github.com/Azure/go-autorest/logger # github.com/Azure/go-autorest/tracing v0.6.1 ## explicit; go 1.15 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