mirror of https://github.com/knative/func.git
Refactor credential provider (#707)
* src: cleanup Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: cleanup Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: cleanup Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: cleanup Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: cleanup Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: allow to set custom crednetial loader Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
parent
07062c144a
commit
27e1b0153a
|
@ -28,9 +28,8 @@ func newBuildClient(cfg buildConfig) (*fn.Client, error) {
|
|||
pusherOption := fn.WithPusher(nil)
|
||||
if cfg.Push {
|
||||
credentialsProvider := docker.NewCredentialsProvider(
|
||||
newCredentialsCallback(),
|
||||
docker.CheckAuth,
|
||||
newChooseHelperCallback(),
|
||||
docker.WithPromptForCredentials(newPromptForCredentials()),
|
||||
docker.WithPromptForCredentialStore(newPromptForCredentialStore()),
|
||||
)
|
||||
pusher, err := docker.NewPusher(
|
||||
docker.WithCredentialsProvider(credentialsProvider),
|
||||
|
|
|
@ -27,9 +27,8 @@ func newDeployClient(cfg deployConfig) (*fn.Client, error) {
|
|||
builder := buildpacks.NewBuilder()
|
||||
|
||||
credentialsProvider := docker.NewCredentialsProvider(
|
||||
newCredentialsCallback(),
|
||||
docker.CheckAuth,
|
||||
newChooseHelperCallback())
|
||||
docker.WithPromptForCredentials(newPromptForCredentials()),
|
||||
docker.WithPromptForCredentialStore(newPromptForCredentialStore()))
|
||||
pusher, err := docker.NewPusher(
|
||||
docker.WithCredentialsProvider(credentialsProvider),
|
||||
docker.WithProgressListener(listener))
|
||||
|
@ -195,7 +194,7 @@ func runDeploy(cmd *cobra.Command, _ []string, clientFn deployClientFn) (err err
|
|||
// (for example kubectl usually uses ~/.kube/config)
|
||||
}
|
||||
|
||||
func newCredentialsCallback() func(registry string) (docker.Credentials, error) {
|
||||
func newPromptForCredentials() func(registry string) (docker.Credentials, error) {
|
||||
firstTime := true
|
||||
return func(registry string) (docker.Credentials, error) {
|
||||
var result docker.Credentials
|
||||
|
@ -229,7 +228,7 @@ func newCredentialsCallback() func(registry string) (docker.Credentials, error)
|
|||
}
|
||||
}
|
||||
|
||||
func newChooseHelperCallback() docker.ChooseCredentialHelperCallback {
|
||||
func newPromptForCredentialStore() docker.ChooseCredentialHelperCallback {
|
||||
return func(availableHelpers []string) (string, error) {
|
||||
if len(availableHelpers) < 1 {
|
||||
fmt.Fprintf(os.Stderr, `Credentials will not be saved.
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/docker/docker-credential-helpers/client"
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
@ -65,15 +64,15 @@ func setCredentialHelperToConfig(confFilePath, helper string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getCredentialsByCredentialHelper(confFilePath, registry string) (types.DockerAuthConfig, error) {
|
||||
result := types.DockerAuthConfig{}
|
||||
func getCredentialsByCredentialHelper(confFilePath, registry string) (Credentials, error) {
|
||||
result := Credentials{}
|
||||
|
||||
helper, err := getCredentialHelperFromConfig(confFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return types.DockerAuthConfig{}, fmt.Errorf("failed to get helper from config: %w", err)
|
||||
return result, fmt.Errorf("failed to get helper from config: %w", err)
|
||||
}
|
||||
if helper == "" {
|
||||
return types.DockerAuthConfig{}, errCredentialsNotFound
|
||||
return result, errCredentialsNotFound
|
||||
}
|
||||
|
||||
helperName := fmt.Sprintf("docker-credential-%s", helper)
|
||||
|
|
132
docker/pusher.go
132
docker/pusher.go
|
@ -14,6 +14,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
|
@ -22,7 +24,6 @@ import (
|
|||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
containersTypes "github.com/containers/image/v5/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
@ -42,9 +43,9 @@ var ErrUnauthorized = errors.New("bad credentials")
|
|||
|
||||
// VerifyCredentialsCallback checks if credentials are accepted by the registry.
|
||||
// If credentials are incorrect this callback shall return ErrUnauthorized.
|
||||
type VerifyCredentialsCallback func(ctx context.Context, username, password, registry string) error
|
||||
type VerifyCredentialsCallback func(ctx context.Context, registry string, credentials Credentials) error
|
||||
|
||||
func CheckAuth(ctx context.Context, username, password, registry string) error {
|
||||
func CheckAuth(ctx context.Context, registry string, credentials Credentials) error {
|
||||
serverAddress := registry
|
||||
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
||||
serverAddress = "https://" + serverAddress
|
||||
|
@ -53,8 +54,8 @@ func CheckAuth(ctx context.Context, username, password, registry string) error {
|
|||
url := fmt.Sprintf("%s/v2", serverAddress)
|
||||
|
||||
authenticator := &authn.Basic{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
}
|
||||
|
||||
reg, err := name.NewRegistry(registry)
|
||||
|
@ -92,22 +93,74 @@ func CheckAuth(ctx context.Context, username, password, registry string) error {
|
|||
|
||||
type ChooseCredentialHelperCallback func(available []string) (string, error)
|
||||
|
||||
// NewCredentialsProvider returns new CredentialsProvider that tires to get credentials from `docker` and `func` config files.
|
||||
type credentialProviderConfig struct {
|
||||
promptForCredentials CredentialsCallback
|
||||
verifyCredentials VerifyCredentialsCallback
|
||||
promptForCredentialStore ChooseCredentialHelperCallback
|
||||
additionalCredentialLoaders []CredentialsCallback
|
||||
}
|
||||
|
||||
type CredentialProviderOptions func(opts *credentialProviderConfig)
|
||||
|
||||
// WithPromptForCredentials sets custom callback that is supposed to
|
||||
// interactively ask for credentials in case the credentials cannot be found in configuration files.
|
||||
// The callback may be called multiple times in case incorrect credentials were returned before.
|
||||
func WithPromptForCredentials(cbk CredentialsCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.promptForCredentials = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithVerifyCredentials sets custom callback for credentials validation.
|
||||
func WithVerifyCredentials(cbk VerifyCredentialsCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.verifyCredentials = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithPromptForCredentialStore sets custom callback that is supposed to
|
||||
// interactively ask user which credentials store/helper is used to store credentials obtained
|
||||
// from user.
|
||||
func WithPromptForCredentialStore(cbk ChooseCredentialHelperCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.promptForCredentialStore = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalCredentialLoaders adds custom callbacks for credential retrieval.
|
||||
// The callbacks are supposed to be non-interactive as opposed to WithPromptForCredentials.
|
||||
//
|
||||
// This might be useful when credentials are shared with some other service.
|
||||
//
|
||||
// Example: OpenShift builtin registry shares credentials with the cluster (k8s) credentials.
|
||||
func WithAdditionalCredentialLoaders(loaders ...CredentialsCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.additionalCredentialLoaders = append(opts.additionalCredentialLoaders, loaders...)
|
||||
}
|
||||
}
|
||||
|
||||
// NewCredentialsProvider returns new CredentialsProvider that tires to get credentials from docker/func config files.
|
||||
//
|
||||
// In case getting credentials from the config files fails
|
||||
// the caller provided callback will be invoked to obtain credentials.
|
||||
// The callback may be called multiple times in case it returned credentials that are not correct (see verifyCredentials).
|
||||
// the caller provided callback (see WithPromptForCredentials) will be invoked to obtain credentials.
|
||||
// The callback may be called multiple times in case the returned credentials
|
||||
// are not correct (see WithVerifyCredentials).
|
||||
//
|
||||
// When the callback succeeds the credentials will be saved by using helper defined in the `func` config.
|
||||
// If the helper is not defined in the config the chooseCredentialHelper parameter will be used to pick one.
|
||||
// When the callback succeeds the credentials will be saved by using helper defined in the func config.
|
||||
// If the helper is not defined in the config file
|
||||
// it may be picked by provided callback (see WithPromptForCredentialStore).
|
||||
// The picked value will be saved in the func config.
|
||||
//
|
||||
// To verify that credentials are correct the verifyCredentials parameter is used.
|
||||
// If verifyCredentials is not set then CheckAuth will be used as a fallback.
|
||||
func NewCredentialsProvider(
|
||||
getCredentials CredentialsCallback,
|
||||
verifyCredentials VerifyCredentialsCallback,
|
||||
chooseCredentialHelper ChooseCredentialHelperCallback) CredentialsProvider {
|
||||
// To verify that credentials are correct callback will be used (see WithVerifyCredentials).
|
||||
// If the callback is not set then CheckAuth will be used as a fallback.
|
||||
func NewCredentialsProvider(opts ...CredentialProviderOptions) CredentialsProvider {
|
||||
var conf credentialProviderConfig
|
||||
|
||||
for _, o := range opts {
|
||||
o(&conf)
|
||||
}
|
||||
|
||||
askUser, verifyCredentials, chooseCredentialHelper := conf.promptForCredentials, conf.verifyCredentials, conf.promptForCredentialStore
|
||||
|
||||
if verifyCredentials == nil {
|
||||
verifyCredentials = CheckAuth
|
||||
|
@ -131,31 +184,40 @@ func NewCredentialsProvider(
|
|||
}
|
||||
dockerConfigPath := filepath.Join(home, ".docker", "config.json")
|
||||
|
||||
var authLoaders = []CredentialsCallback{
|
||||
func(registry string) (Credentials, error) {
|
||||
creds, err := config.GetCredentials(sys, registry)
|
||||
if err != nil {
|
||||
return Credentials{}, err
|
||||
}
|
||||
return Credentials{
|
||||
Username: creds.Username,
|
||||
Password: creds.Password,
|
||||
}, nil
|
||||
},
|
||||
func(registry string) (Credentials, error) {
|
||||
return getCredentialsByCredentialHelper(authFilePath, registry)
|
||||
},
|
||||
func(registry string) (Credentials, error) {
|
||||
return getCredentialsByCredentialHelper(dockerConfigPath, registry)
|
||||
},
|
||||
}
|
||||
|
||||
authLoaders = append(conf.additionalCredentialLoaders, authLoaders...)
|
||||
|
||||
return func(ctx context.Context, registry string) (Credentials, error) {
|
||||
result := Credentials{}
|
||||
|
||||
for _, load := range []func() (containersTypes.DockerAuthConfig, error){
|
||||
func() (containersTypes.DockerAuthConfig, error) {
|
||||
return config.GetCredentials(sys, registry)
|
||||
},
|
||||
func() (containersTypes.DockerAuthConfig, error) {
|
||||
return getCredentialsByCredentialHelper(authFilePath, registry)
|
||||
},
|
||||
func() (containersTypes.DockerAuthConfig, error) {
|
||||
return getCredentialsByCredentialHelper(dockerConfigPath, registry)
|
||||
},
|
||||
} {
|
||||
var credentials containersTypes.DockerAuthConfig
|
||||
credentials, err = load()
|
||||
for _, load := range authLoaders {
|
||||
|
||||
result, err = load(registry)
|
||||
|
||||
if err != nil && !errors.Is(err, errCredentialsNotFound) {
|
||||
return Credentials{}, err
|
||||
}
|
||||
|
||||
if credentials != (containersTypes.DockerAuthConfig{}) {
|
||||
result.Username, result.Password = credentials.Username, credentials.Password
|
||||
|
||||
err = verifyCredentials(ctx, result.Username, result.Password, registry)
|
||||
if result != (Credentials{}) {
|
||||
err = verifyCredentials(ctx, registry, result)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
} else {
|
||||
|
@ -167,12 +229,12 @@ func NewCredentialsProvider(
|
|||
}
|
||||
|
||||
for {
|
||||
result, err = getCredentials(registry)
|
||||
result, err = askUser(registry)
|
||||
if err != nil {
|
||||
return Credentials{}, err
|
||||
}
|
||||
|
||||
err = verifyCredentials(ctx, result.Username, result.Password, registry)
|
||||
err = verifyCredentials(ctx, registry, result)
|
||||
if err == nil {
|
||||
err = setCredentialsByCredentialHelper(authFilePath, registry, result.Username, result.Password)
|
||||
if err != nil {
|
||||
|
|
|
@ -107,10 +107,11 @@ func TestNewCredentialsProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
type args struct {
|
||||
credentialsCallback CredentialsCallback
|
||||
verifyCredentials VerifyCredentialsCallback
|
||||
registry string
|
||||
setUpEnv setUpEnv
|
||||
promptUser CredentialsCallback
|
||||
verifyCredentials VerifyCredentialsCallback
|
||||
additionalLoaders []CredentialsCallback
|
||||
registry string
|
||||
setUpEnv setUpEnv
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -120,47 +121,47 @@ func TestNewCredentialsProvider(t *testing.T) {
|
|||
{
|
||||
name: "test user callback correct password on first try",
|
||||
args: args{
|
||||
credentialsCallback: correctPwdCallback,
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
promptUser: correctPwdCallback,
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "test user callback correct password on second try",
|
||||
args: args{
|
||||
credentialsCallback: pwdCbkFirstWrongThenCorrect(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
promptUser: pwdCbkFirstWrongThenCorrect(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get quay-io credentials with func config populated",
|
||||
args: args{
|
||||
credentialsCallback: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "quay.io",
|
||||
setUpEnv: withPopulatedFuncAuthConfig,
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "quay.io",
|
||||
setUpEnv: withPopulatedFuncAuthConfig,
|
||||
},
|
||||
want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get docker-io credentials with func config populated",
|
||||
args: args{
|
||||
credentialsCallback: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
setUpEnv: withPopulatedFuncAuthConfig,
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
setUpEnv: withPopulatedFuncAuthConfig,
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get quay-io credentials with docker config populated",
|
||||
args: args{
|
||||
credentialsCallback: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "quay.io",
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "quay.io",
|
||||
setUpEnv: all(
|
||||
withPopulatedDockerAuthConfig,
|
||||
setUpMockHelper("docker-credential-mock", helperWithQuayIO)),
|
||||
|
@ -170,10 +171,20 @@ func TestNewCredentialsProvider(t *testing.T) {
|
|||
{
|
||||
name: "get docker-io credentials with docker config populated",
|
||||
args: args{
|
||||
credentialsCallback: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
setUpEnv: withPopulatedDockerAuthConfig,
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
setUpEnv: withPopulatedDockerAuthConfig,
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get docker-io credentials from custom loader",
|
||||
args: args{
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
additionalLoaders: []CredentialsCallback{correctPwdCallback},
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
|
@ -186,7 +197,10 @@ func TestNewCredentialsProvider(t *testing.T) {
|
|||
defer tt.args.setUpEnv(t)()
|
||||
}
|
||||
|
||||
credentialsProvider := NewCredentialsProvider(tt.args.credentialsCallback, tt.args.verifyCredentials, nil)
|
||||
credentialsProvider := NewCredentialsProvider(
|
||||
WithPromptForCredentials(tt.args.promptUser),
|
||||
WithVerifyCredentials(tt.args.verifyCredentials),
|
||||
WithAdditionalCredentialLoaders(tt.args.additionalLoaders...))
|
||||
got, err := credentialsProvider(context.Background(), tt.args.registry)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
@ -228,7 +242,10 @@ func TestCredentialsProviderSavingFromUserInput(t *testing.T) {
|
|||
return "", errors.New("this callback shall not be invoked")
|
||||
}
|
||||
|
||||
credentialsProvider := NewCredentialsProvider(pwdCbk, correctVerifyCbk, chooseNoStore)
|
||||
credentialsProvider := NewCredentialsProvider(
|
||||
WithPromptForCredentials(pwdCbk),
|
||||
WithVerifyCredentials(correctVerifyCbk),
|
||||
WithPromptForCredentialStore(chooseNoStore))
|
||||
_, err := credentialsProvider(context.Background(), "docker.io")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
@ -244,7 +261,10 @@ func TestCredentialsProviderSavingFromUserInput(t *testing.T) {
|
|||
if credsInStore != 0 {
|
||||
t.Errorf("expected to have zero credentials in store, but has: %d", credsInStore)
|
||||
}
|
||||
credentialsProvider = NewCredentialsProvider(pwdCbk, correctVerifyCbk, chooseMockStore)
|
||||
credentialsProvider = NewCredentialsProvider(
|
||||
WithPromptForCredentials(pwdCbk),
|
||||
WithVerifyCredentials(correctVerifyCbk),
|
||||
WithPromptForCredentialStore(chooseMockStore))
|
||||
_, err = credentialsProvider(context.Background(), "docker.io")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
@ -263,9 +283,10 @@ func TestCredentialsProviderSavingFromUserInput(t *testing.T) {
|
|||
if len(l) != 1 {
|
||||
t.Errorf("expected to have exactly one credentials in store, but has: %d", credsInStore)
|
||||
}
|
||||
credentialsProvider = NewCredentialsProvider(pwdCbkThatShallNotBeCalled(t),
|
||||
correctVerifyCbk,
|
||||
shallNotBeInvoked)
|
||||
credentialsProvider = NewCredentialsProvider(
|
||||
WithPromptForCredentials(pwdCbkThatShallNotBeCalled(t)),
|
||||
WithVerifyCredentials(correctVerifyCbk),
|
||||
WithPromptForCredentialStore(shallNotBeInvoked))
|
||||
_, err = credentialsProvider(context.Background(), "docker.io")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
@ -381,7 +402,8 @@ func correctPwdCallback(registry string) (Credentials, error) {
|
|||
return Credentials{}, errors.New("this cbk don't know the pwd")
|
||||
}
|
||||
|
||||
func correctVerifyCbk(ctx context.Context, username, password, registry string) error {
|
||||
func correctVerifyCbk(ctx context.Context, registry string, creds Credentials) error {
|
||||
username, password := creds.Username, creds.Password
|
||||
if username == dockerIoUser && password == dockerIoUserPwd && registry == "docker.io" {
|
||||
return nil
|
||||
}
|
||||
|
@ -738,7 +760,11 @@ func TestCheckAuth(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := CheckAuth(tt.args.ctx, tt.args.username, tt.args.password, tt.args.registry); (err != nil) != tt.wantErr {
|
||||
creds := Credentials{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
if err := CheckAuth(tt.args.ctx, tt.args.registry, creds); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckAuth() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue