Skip to content

Commit

Permalink
feat(build): retry 429 err for "werf cr login <registry>"
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandr Zaytsev <alexandr.zaytsev@flant.com>
  • Loading branch information
nervgh authored and alexey-igrychev committed Jan 22, 2025
1 parent 3cacfba commit 792493b
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 35 deletions.
46 changes: 25 additions & 21 deletions cmd/werf/cr/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/docker/cli/cli/config/types"
"github.com/goware/urlx"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"oras.land/oras-go/pkg/auth"
"oras.land/oras-go/pkg/auth/docker"

"github.com/werf/common-go/pkg/util"
"github.com/werf/logboek"
"github.com/werf/werf/v2/cmd/werf/common"
secret_common "github.com/werf/werf/v2/cmd/werf/helm/secret/common"
"github.com/werf/werf/v2/pkg/docker"
"github.com/werf/werf/v2/pkg/docker_registry/auth"
"github.com/werf/werf/v2/pkg/werf"
"github.com/werf/werf/v2/pkg/werf/global_warnings"
)
Expand Down Expand Up @@ -94,16 +95,9 @@ type LoginOptions struct {
}

func Login(ctx context.Context, registry string, opts LoginOptions) error {
var dockerConfigDir string
if opts.DockerConfigDir != "" {
dockerConfigDir = opts.DockerConfigDir
} else {
dockerConfigDir = filepath.Join(os.Getenv("HOME"), ".docker")
}

cli, err := docker.NewClient(filepath.Join(dockerConfigDir, "config.json"))
u, err := urlx.Parse(registry)
if err != nil {
return fmt.Errorf("unable to create oras auth client: %w", err)
return fmt.Errorf("unable to parse %q: %w", registry, err)
}

if opts.Username == "" {
Expand Down Expand Up @@ -136,15 +130,25 @@ func Login(ctx context.Context, registry string, opts LoginOptions) error {
return fmt.Errorf("provide --password or --password-stdin")
}

if err := cli.LoginWithOpts(func(settings *auth.LoginSettings) {
settings.Context = ctx
settings.Hostname = registry
settings.Username = opts.Username
settings.Secret = password
settings.Insecure = opts.InsecureRegistry
settings.UserAgent = werf.UserAgent
}); err != nil {
return fmt.Errorf("unable to login into %q: %w", registry, err)
token, err := auth.Auth(ctx, auth.Options{
Username: opts.Username,
Password: password,
UserAgent: werf.UserAgent,
Hostname: u.Host,
Insecure: opts.InsecureRegistry,
})
if err != nil {
return fmt.Errorf("unable to authenticate into %q: %w", registry, err)
}

err = docker.StoreCredentials(opts.DockerConfigDir, types.AuthConfig{
ServerAddress: u.Host,
Username: opts.Username,
Password: password,
IdentityToken: token,
})
if err != nil {
return fmt.Errorf("unable to store credentials: %w", err)
}

logboek.Context(ctx).Default().LogFHighlight("Successful login\n")
Expand Down
22 changes: 9 additions & 13 deletions cmd/werf/cr/logout/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package logout
import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/docker/cli/cli/config/types"
"github.com/goware/urlx"
"github.com/spf13/cobra"
"oras.land/oras-go/pkg/auth/docker"

"github.com/werf/logboek"
"github.com/werf/werf/v2/cmd/werf/common"
"github.com/werf/werf/v2/pkg/docker"
"github.com/werf/werf/v2/pkg/werf/global_warnings"
)

Expand Down Expand Up @@ -61,19 +61,15 @@ type LogoutOptions struct {
}

func Logout(ctx context.Context, registry string, opts LogoutOptions) error {
var dockerConfigDir string
if opts.DockerConfigDir != "" {
dockerConfigDir = opts.DockerConfigDir
} else {
dockerConfigDir = filepath.Join(os.Getenv("HOME"), ".docker")
}

cli, err := docker.NewClient(filepath.Join(dockerConfigDir, "config.json"))
u, err := urlx.Parse(registry)
if err != nil {
return fmt.Errorf("unable to create auth client: %w", err)
return fmt.Errorf("unable to parse %q: %w", registry, err)
}

if err := cli.Logout(ctx, registry); err != nil {
err = docker.EraseCredentials(opts.DockerConfigDir, types.AuthConfig{
ServerAddress: u.Host,
})
if err != nil {
return fmt.Errorf("unable to logout from %q: %w", registry, err)
}

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gookit/color v1.5.4
github.com/gosuri/uitable v0.0.4
github.com/goware/urlx v0.3.2
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.6.0
github.com/mitchellh/copystructure v1.2.0
Expand Down Expand Up @@ -92,12 +93,13 @@ require (
k8s.io/kubectl v0.29.3
k8s.io/utils v0.0.0-20240310230437-4693a0247e57
mvdan.cc/xurls v1.1.0
oras.land/oras-go v1.2.5
sigs.k8s.io/yaml v1.4.0
)

require (
github.com/Ladicle/tabwriter v1.0.0 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/chainguard-dev/git-urls v1.0.2 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
Expand All @@ -114,6 +116,7 @@ require (
github.com/zeebo/xxh3 v1.0.2 // indirect
golang.org/x/mod v0.21.0 // indirect
mvdan.cc/sh/v3 v3.10.0 // indirect
oras.land/oras-go v1.2.5 // indirect
)

require (
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20230117174420-439a4b8ba167 h1:4sc2y3LzYFk3Na8xMtfnJ5T1N5kA+MsU8dTJN6IjJqk=
github.com/Shopify/logrus-bugsnag v0.0.0-20230117174420-439a4b8ba167/go.mod h1:nBISMsZeFRL0qdZ1pKCSFv0j4veW0nOdVpOa49IMLeI=
Expand Down Expand Up @@ -724,6 +728,8 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/goware/urlx v0.3.2 h1:gdoo4kBHlkqZNaf6XlQ12LGtQOmpKJrR04Rc3RnpJEo=
github.com/goware/urlx v0.3.2/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
Expand Down
34 changes: 34 additions & 0 deletions pkg/docker/config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package docker

import (
"fmt"
"path/filepath"

"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/types"
"github.com/docker/docker/pkg/homedir"
)

Expand All @@ -14,3 +16,35 @@ func GetDockerConfigCredentialsFile(configDir string) string {
return filepath.Join(configDir, config.ConfigFileName)
}
}

// StoreCredentials
//
// Inspired with https://github.com/google/go-containerregistry/blob/v0.20.3/cmd/crane/cmd/auth.go#L242
func StoreCredentials(configDir string, authConf types.AuthConfig) error {
conf, err := config.Load(configDir)
if err != nil {
return fmt.Errorf("unable to load %s: %w", GetDockerConfigCredentialsFile(configDir), err)
}
creds := conf.GetCredentialsStore(authConf.ServerAddress)
err = creds.Store(authConf)
if err != nil {
return fmt.Errorf("unable to store credentials: %w", err)
}
return nil
}

// EraseCredentials
//
// Inspired with https://github.com/google/go-containerregistry/blob/v0.20.3/cmd/crane/cmd/auth.go#L279
func EraseCredentials(configDir string, authConf types.AuthConfig) error {
conf, err := config.Load(configDir)
if err != nil {
return fmt.Errorf("unable to load %s: %w", GetDockerConfigCredentialsFile(configDir), err)
}
creds := conf.GetCredentialsStore(authConf.ServerAddress)
err = creds.Erase(authConf.ServerAddress)
if err != nil {
return fmt.Errorf("unable to erase credentials: %w", err)
}
return nil
}
65 changes: 65 additions & 0 deletions pkg/docker_registry/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package auth

import (
"context"
"fmt"
"strings"
"time"

"github.com/cenkalti/backoff/v4"
apiregistry "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
)

type Options struct {
Hostname string
Username string
Password string
Insecure bool
UserAgent string
}

// Auth
//
// Inspired with https://github.com/oras-project/oras-go/blob/v1/pkg/auth/docker/login.go#L52
func Auth(ctx context.Context, options Options) (string, error) {
registryOptions := registry.ServiceOptions{}

if options.Insecure {
registryOptions.InsecureRegistries = []string{options.Hostname}
}

remote, err := registry.NewService(registryOptions)
if err != nil {
return "", fmt.Errorf("unable to initiatiate registry service: %w", err)
}

authConfig := &apiregistry.AuthConfig{
Username: options.Username,
ServerAddress: options.Hostname,
}

if options.Username == "" {
authConfig.IdentityToken = options.Password
} else {
authConfig.Password = options.Password
}

operation := func() (string, error) {
_, token, err := remote.Auth(ctx, authConfig, options.UserAgent)
if err != nil && strings.Contains(err.Error(), "failed with status: 429") {
return "", err
}
return token, nil
}

eb := backoff.NewExponentialBackOff()
eb.MaxElapsedTime = 5 * time.Minute // Maximum time for all retries.

token, err := backoff.RetryWithData(operation, eb)
if err != nil {
return "", err
}

return token, nil
}

0 comments on commit 792493b

Please sign in to comment.