From b52ddfb7f2fcdc75aeeac6f5b570ad34f870eea6 Mon Sep 17 00:00:00 2001 From: kapil Date: Wed, 20 Aug 2025 00:12:50 +0530 Subject: [PATCH] oci: refactors pusher auth Signed-off-by: kapil --- cmd/build.go | 7 +- cmd/client.go | 12 ++- cmd/prompt/prompt.go | 18 ++--- cmd/prompt/prompt_test.go | 5 +- pkg/{docker => }/creds/credentials.go | 67 ++++++++--------- pkg/{docker => }/creds/credentials_test.go | 16 ++-- pkg/docker/pusher.go | 24 ++---- pkg/docker/pusher_test.go | 5 +- pkg/k8s/keychains.go | 85 +++++++++++++++++++++ pkg/k8s/openshift.go | 10 +-- pkg/oci/pusher.go | 87 +++++++++++++++------- pkg/pipelines/tekton/gitlab_int_test.go | 6 +- pkg/pipelines/tekton/pipelines_int_test.go | 5 +- pkg/pipelines/tekton/pipelines_provider.go | 5 +- 14 files changed, 236 insertions(+), 116 deletions(-) rename pkg/{docker => }/creds/credentials.go (88%) rename pkg/{docker => }/creds/credentials_test.go (98%) create mode 100644 pkg/k8s/keychains.go diff --git a/cmd/build.go b/cmd/build.go index d64a71a43..ec358946e 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -394,9 +394,14 @@ func (c buildConfig) clientOptions() ([]fn.Option, error) { o := []fn.Option{fn.WithRegistry(c.Registry)} switch c.Builder { case builders.Host: + t := newTransport(c.RegistryInsecure) // may provide a custom impl which proxies + creds := newCredentialsProvider(config.Dir(), t) o = append(o, fn.WithBuilder(oci.NewBuilder(builders.Host, c.Verbose)), - fn.WithPusher(oci.NewPusher(c.RegistryInsecure, false, c.Verbose))) + fn.WithPusher(oci.NewPusher(c.RegistryInsecure, false, c.Verbose, + oci.WithCredentialsProvider(creds), + oci.WithVerbose(c.Verbose))), + ) case builders.Pack: o = append(o, fn.WithBuilder(pack.NewBuilder( diff --git a/cmd/client.go b/cmd/client.go index f2903197a..9c4fc8198 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -8,12 +8,13 @@ import ( "knative.dev/func/cmd/prompt" "knative.dev/func/pkg/builders/buildpacks" "knative.dev/func/pkg/config" + "knative.dev/func/pkg/creds" "knative.dev/func/pkg/docker" - "knative.dev/func/pkg/docker/creds" fn "knative.dev/func/pkg/functions" fnhttp "knative.dev/func/pkg/http" "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" + "knative.dev/func/pkg/oci" "knative.dev/func/pkg/pipelines/tekton" ) @@ -100,19 +101,22 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser { // newCredentialsProvider returns a credentials provider which possibly // has cluster-flavor specific additional credential loaders to take advantage // of features or configuration nuances of cluster variants. -func newCredentialsProvider(configPath string, t http.RoundTripper) docker.CredentialsProvider { +func newCredentialsProvider(configPath string, t http.RoundTripper) oci.CredentialsProvider { + additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(), k8s.GetGoogleCredentialLoader()...) + additionalLoaders = append(additionalLoaders, k8s.GetECRCredentialLoader()...) + additionalLoaders = append(additionalLoaders, k8s.GetACRCredentialLoader()...) options := []creds.Opt{ creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin, os.Stdout, os.Stderr)), creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()), creds.WithTransport(t), - creds.WithAdditionalCredentialLoaders(k8s.GetOpenShiftDockerCredentialLoaders()...), + creds.WithAdditionalCredentialLoaders(additionalLoaders...), } // Other cluster variants can be supported here return creds.NewCredentialsProvider(configPath, options...) } -func newTektonPipelinesProvider(creds docker.CredentialsProvider, verbose bool) *tekton.PipelinesProvider { +func newTektonPipelinesProvider(creds oci.CredentialsProvider, verbose bool) *tekton.PipelinesProvider { options := []tekton.Opt{ tekton.WithCredentialsProvider(creds), tekton.WithVerbose(verbose), diff --git a/cmd/prompt/prompt.go b/cmd/prompt/prompt.go index 109620afd..5c0502f1e 100644 --- a/cmd/prompt/prompt.go +++ b/cmd/prompt/prompt.go @@ -11,14 +11,14 @@ import ( "github.com/AlecAivazis/survey/v2/terminal" "golang.org/x/term" - "knative.dev/func/pkg/docker" - "knative.dev/func/pkg/docker/creds" + "knative.dev/func/pkg/creds" + "knative.dev/func/pkg/oci" ) -func NewPromptForCredentials(in io.Reader, out, errOut io.Writer) func(repository string) (docker.Credentials, error) { +func NewPromptForCredentials(in io.Reader, out, errOut io.Writer) func(repository string) (oci.Credentials, error) { firstTime := true - return func(repository string) (docker.Credentials, error) { - var result docker.Credentials + return func(repository string) (oci.Credentials, error) { + var result oci.Credentials if firstTime { firstTime = false fmt.Fprintf(out, "Please provide credentials for image repository '%s'.\n", repository) @@ -56,7 +56,7 @@ func NewPromptForCredentials(in io.Reader, out, errOut io.Writer) func(repositor if isTerm { err := survey.Ask(qs, &result, survey.WithStdio(fr, out.(terminal.FileWriter), errOut)) if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } } else { reader := bufio.NewReader(in) @@ -64,18 +64,18 @@ func NewPromptForCredentials(in io.Reader, out, errOut io.Writer) func(repositor fmt.Fprintf(out, "Username: ") u, err := reader.ReadString('\n') if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } u = strings.Trim(u, "\r\n") fmt.Fprintf(out, "Password: ") p, err := reader.ReadString('\n') if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } p = strings.Trim(p, "\r\n") - result = docker.Credentials{Username: u, Password: p} + result = oci.Credentials{Username: u, Password: p} } return result, nil diff --git a/cmd/prompt/prompt_test.go b/cmd/prompt/prompt_test.go index 2015c6581..9b2607076 100644 --- a/cmd/prompt/prompt_test.go +++ b/cmd/prompt/prompt_test.go @@ -12,8 +12,7 @@ import ( "github.com/Netflix/go-expect" "github.com/creack/pty" "github.com/hinshun/vt10x" - - "knative.dev/func/pkg/docker" + "knative.dev/func/pkg/oci" ) const ( @@ -21,7 +20,7 @@ const ( ) func Test_NewPromptForCredentials(t *testing.T) { - expectedCreds := docker.Credentials{ + expectedCreds := oci.Credentials{ Username: "testuser", Password: "testpwd", } diff --git a/pkg/docker/creds/credentials.go b/pkg/creds/credentials.go similarity index 88% rename from pkg/docker/creds/credentials.go rename to pkg/creds/credentials.go index 47e15a28e..8635594ae 100644 --- a/pkg/docker/creds/credentials.go +++ b/pkg/creds/credentials.go @@ -22,10 +22,10 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" - "knative.dev/func/pkg/docker" + "knative.dev/func/pkg/oci" ) -type CredentialsCallback func(registry string) (docker.Credentials, error) +type CredentialsCallback func(registry string) (oci.Credentials, error) var ErrUnauthorized = errors.New("bad credentials") @@ -33,7 +33,7 @@ var ErrCredentialsNotFound = errors.New("credentials not found") // VerifyCredentialsCallback checks if credentials are authorized for image push. // If credentials are incorrect this callback shall return ErrUnauthorized. -type VerifyCredentialsCallback func(ctx context.Context, image string, credentials docker.Credentials) error +type VerifyCredentialsCallback func(ctx context.Context, image string, credentials oci.Credentials) error type keyChain struct { user string @@ -48,7 +48,7 @@ func (k keyChain) Resolve(resource authn.Resource) (authn.Authenticator, error) } // CheckAuth verifies that credentials can be used for image push -func CheckAuth(ctx context.Context, image string, credentials docker.Credentials, trans http.RoundTripper) error { +func CheckAuth(ctx context.Context, image string, credentials oci.Credentials, trans http.RoundTripper) error { ref, err := name.ParseReference(image) if err != nil { @@ -142,9 +142,8 @@ func WithAdditionalCredentialLoaders(loaders ...CredentialsCallback) Opt { // The picked value will be saved in the func config. // // To verify that credentials are correct custom callback can be used (see WithVerifyCredentials). -func NewCredentialsProvider(configPath string, opts ...Opt) docker.CredentialsProvider { +func NewCredentialsProvider(configPath string, opts ...Opt) oci.CredentialsProvider { var c credentialsProvider - for _, o := range opts { o(&c) } @@ -154,7 +153,7 @@ func NewCredentialsProvider(configPath string, opts ...Opt) docker.CredentialsPr } if c.verifyCredentials == nil { - c.verifyCredentials = func(ctx context.Context, registry string, credentials docker.Credentials) error { + c.verifyCredentials = func(ctx context.Context, registry string, credentials oci.Credentials) error { return CheckAuth(ctx, registry, credentials, c.transport) } } @@ -175,7 +174,7 @@ func NewCredentialsProvider(configPath string, opts ...Opt) docker.CredentialsPr if _, err := os.Stat(c.authFilePath); err == nil { defaultCredentialLoaders = append(defaultCredentialLoaders, - func(registry string) (docker.Credentials, error) { + func(registry string) (oci.Credentials, error) { return getCredentialsByCredentialHelper(c.authFilePath, registry) }) } @@ -185,40 +184,40 @@ func NewCredentialsProvider(configPath string, opts ...Opt) docker.CredentialsPr if err == nil { dockerConfigPath := filepath.Join(home, ".docker", "config.json") defaultCredentialLoaders = append(defaultCredentialLoaders, - func(registry string) (docker.Credentials, error) { + func(registry string) (oci.Credentials, error) { return getCredentialsByCredentialHelper(dockerConfigPath, registry) }) } defaultCredentialLoaders = append(defaultCredentialLoaders, - func(registry string) (docker.Credentials, error) { + func(registry string) (oci.Credentials, error) { creds, err := dockerConfig.GetCredentials(sys, registry) if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } if creds.Username == "" || creds.Password == "" { - return docker.Credentials{}, ErrCredentialsNotFound + return oci.Credentials{}, ErrCredentialsNotFound } - return docker.Credentials{ + return oci.Credentials{ Username: creds.Username, Password: creds.Password, }, nil }) defaultCredentialLoaders = append(defaultCredentialLoaders, - func(registry string) (docker.Credentials, error) { + func(registry string) (oci.Credentials, error) { // Fallback onto default docker config locations emptySys := &containersTypes.SystemContext{} creds, err := dockerConfig.GetCredentials(emptySys, registry) if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } - return docker.Credentials{ + return oci.Credentials{ Username: creds.Username, Password: creds.Password, }, nil }) defaultCredentialLoaders = append(defaultCredentialLoaders, - func(registry string) (docker.Credentials, error) { // empty credentials provider for unsecured registries - return docker.Credentials{}, nil + func(registry string) (oci.Credentials, error) { // empty credentials provider for unsecured registries + return oci.Credentials{}, nil }) c.credentialLoaders = append(c.credentialLoaders, defaultCredentialLoaders...) @@ -226,13 +225,13 @@ func NewCredentialsProvider(configPath string, opts ...Opt) docker.CredentialsPr return c.getCredentials } -func (c *credentialsProvider) getCredentials(ctx context.Context, image string) (docker.Credentials, error) { +func (c *credentialsProvider) getCredentials(ctx context.Context, image string) (oci.Credentials, error) { var err error - result := docker.Credentials{} + result := oci.Credentials{} ref, err := name.ParseReference(image) if err != nil { - return docker.Credentials{}, fmt.Errorf("cannot parse the image reference: %w", err) + return oci.Credentials{}, fmt.Errorf("cannot parse the image reference: %w", err) } registry := ref.Context().RegistryStr() @@ -244,7 +243,7 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string) if errors.Is(err, ErrCredentialsNotFound) { continue } - return docker.Credentials{}, err + return oci.Credentials{}, err } err = c.verifyCredentials(ctx, image, result) @@ -252,14 +251,14 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string) return result, nil } else { if !errors.Is(err, ErrUnauthorized) { - return docker.Credentials{}, err + return oci.Credentials{}, err } } } if c.promptForCredentials == nil { - return docker.Credentials{}, ErrCredentialsNotFound + return oci.Credentials{}, ErrCredentialsNotFound } // this is [registry] / [repository] @@ -271,7 +270,7 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string) // use repo here to print it out in prompt result, err = c.promptForCredentials(repository) if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } err = c.verifyCredentials(ctx, image, result) @@ -282,21 +281,21 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string) // This shouldn't be fatal error. if strings.Contains(err.Error(), "not implemented") { fmt.Fprintf(os.Stderr, "the cred-helper does not support write operation (consider changing the cred-helper it in auth.json)\n") - return docker.Credentials{}, nil + return oci.Credentials{}, nil } if !errors.Is(err, errNoCredentialHelperConfigured) { - return docker.Credentials{}, err + return oci.Credentials{}, err } helpers := listCredentialHelpers() helper, err := c.promptForCredentialStore(helpers) if err != nil { - return docker.Credentials{}, err + return oci.Credentials{}, err } helper = strings.TrimPrefix(helper, "docker-credential-") err = setCredentialHelperToConfig(c.authFilePath, helper) if err != nil { - return docker.Credentials{}, fmt.Errorf("faild to set the helper to the config: %w", err) + return oci.Credentials{}, fmt.Errorf("faild to set the helper to the config: %w", err) } err = setCredentialsByCredentialHelper(c.authFilePath, registry, result.Username, result.Password) if err != nil { @@ -304,11 +303,11 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string) // This shouldn't be fatal error. if strings.Contains(err.Error(), "not implemented") { fmt.Fprintf(os.Stderr, "the cred-helper does not support write operation (consider changing the cred-helper it in auth.json)\n") - return docker.Credentials{}, nil + return oci.Credentials{}, nil } if !errors.Is(err, errNoCredentialHelperConfigured) { - return docker.Credentials{}, err + return oci.Credentials{}, err } } } @@ -317,7 +316,7 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string) if errors.Is(err, ErrUnauthorized) { continue } - return docker.Credentials{}, err + return oci.Credentials{}, err } } } @@ -374,8 +373,8 @@ func setCredentialHelperToConfig(confFilePath, helper string) error { return nil } -func getCredentialsByCredentialHelper(confFilePath, registry string) (docker.Credentials, error) { - result := docker.Credentials{} +func getCredentialsByCredentialHelper(confFilePath, registry string) (oci.Credentials, error) { + result := oci.Credentials{} helper, err := getCredentialHelperFromConfig(confFilePath) if err != nil && !os.IsNotExist(err) { diff --git a/pkg/docker/creds/credentials_test.go b/pkg/creds/credentials_test.go similarity index 98% rename from pkg/docker/creds/credentials_test.go rename to pkg/creds/credentials_test.go index 679f9c8b3..7bb3e78e1 100644 --- a/pkg/docker/creds/credentials_test.go +++ b/pkg/creds/credentials_test.go @@ -26,8 +26,8 @@ import ( "github.com/docker/docker-credential-helpers/credentials" - "knative.dev/func/pkg/docker" - "knative.dev/func/pkg/docker/creds" + "knative.dev/func/pkg/creds" + "knative.dev/func/pkg/oci" . "knative.dev/func/pkg/testing" ) @@ -162,7 +162,7 @@ func TestCheckAuth(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := docker.Credentials{ + c := oci.Credentials{ Username: tt.args.username, Password: tt.args.password, } @@ -199,7 +199,7 @@ func TestCheckAuth(t *testing.T) { func TestCheckAuthEmptyCreds(t *testing.T) { localhost, _, _ := startServer(t, "", "") - err := creds.CheckAuth(context.Background(), localhost+"/someorg/someimage:sometag", docker.Credentials{}, http.DefaultTransport) + err := creds.CheckAuth(context.Background(), localhost+"/someorg/someimage:sometag", oci.Credentials{}, http.DefaultTransport) if err != nil { t.Error(err) } @@ -334,7 +334,7 @@ const ( quayIoUserPwd = "goodPwd2" ) -type Credentials = docker.Credentials +type Credentials = oci.Credentials func TestNewCredentialsProvider(t *testing.T) { helperWithQuayIO := newInMemoryHelper() @@ -460,8 +460,8 @@ func TestNewCredentialsProvider(t *testing.T) { func TestNewCredentialsProviderEmptyCreds(t *testing.T) { resetHomeDir(t) - credentialsProvider := creds.NewCredentialsProvider(testConfigPath(t), creds.WithVerifyCredentials(func(ctx context.Context, image string, credentials docker.Credentials) error { - if image == "localhost:5555/someorg/someimage:sometag" && credentials == (docker.Credentials{}) { + credentialsProvider := creds.NewCredentialsProvider(testConfigPath(t), creds.WithVerifyCredentials(func(ctx context.Context, image string, credentials oci.Credentials) error { + if image == "localhost:5555/someorg/someimage:sometag" && credentials == (oci.Credentials{}) { return nil } t.Fatal("unreachable") @@ -471,7 +471,7 @@ func TestNewCredentialsProviderEmptyCreds(t *testing.T) { if err != nil { t.Error(err) } - if c != (docker.Credentials{}) { + if c != (oci.Credentials{}) { t.Error("unexpected credentials") } } diff --git a/pkg/docker/pusher.go b/pkg/docker/pusher.go index f6f6499c6..ecc734504 100644 --- a/pkg/docker/pusher.go +++ b/pkg/docker/pusher.go @@ -14,6 +14,7 @@ import ( "strings" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/oci" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" @@ -33,13 +34,6 @@ import ( type Opt func(*Pusher) -type Credentials struct { - Username string - Password string -} - -type CredentialsProvider func(ctx context.Context, image string) (Credentials, error) - // PusherDockerClient is sub-interface of client.CommonAPIClient required by pusher. type PusherDockerClient interface { daemon.Client @@ -52,12 +46,12 @@ type PusherDockerClientFactory func() (PusherDockerClient, error) // Pusher of images from local to remote registry. type Pusher struct { verbose bool // verbose logging. - credentialsProvider CredentialsProvider + credentialsProvider oci.CredentialsProvider transport http.RoundTripper dockerClientFactory PusherDockerClientFactory } -func WithCredentialsProvider(cp CredentialsProvider) Opt { +func WithCredentialsProvider(cp oci.CredentialsProvider) Opt { return func(p *Pusher) { p.credentialsProvider = cp } @@ -81,14 +75,10 @@ func WithVerbose(verbose bool) Opt { } } -func EmptyCredentialsProvider(ctx context.Context, registry string) (Credentials, error) { - return Credentials{}, nil -} - // NewPusher creates an instance of a docker-based image pusher. func NewPusher(opts ...Opt) *Pusher { result := &Pusher{ - credentialsProvider: EmptyCredentialsProvider, + credentialsProvider: oci.EmptyCredentialsProvider, transport: http.DefaultTransport, dockerClientFactory: func() (PusherDockerClient, error) { c, _, err := NewClient(client.DefaultDockerHost) @@ -176,7 +166,7 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (string, error) { return d.String(), nil } -func (n *Pusher) pushImage(ctx context.Context, f fn.Function, credentials Credentials) (digest string, err error) { +func (n *Pusher) pushImage(ctx context.Context, f fn.Function, credentials oci.Credentials) (digest string, err error) { var output io.Writer @@ -211,7 +201,7 @@ func (n *Pusher) pushImage(ctx context.Context, f fn.Function, credentials Crede return "", err } -func (n *Pusher) daemonPush(ctx context.Context, f fn.Function, credentials Credentials, output io.Writer) (digest string, err error) { +func (n *Pusher) daemonPush(ctx context.Context, f fn.Function, credentials oci.Credentials, output io.Writer) (digest string, err error) { cli, err := n.dockerClientFactory() if err != nil { return "", fmt.Errorf("failed to create docker api client: %w", err) @@ -267,7 +257,7 @@ func ParseDigest(output string) string { return "" } -func (n *Pusher) push(ctx context.Context, f fn.Function, credentials Credentials, output io.Writer) (digest string, err error) { +func (n *Pusher) push(ctx context.Context, f fn.Function, credentials oci.Credentials, output io.Writer) (digest string, err error) { auth := &authn.Basic{ Username: credentials.Username, Password: credentials.Password, diff --git a/pkg/docker/pusher_test.go b/pkg/docker/pusher_test.go index d94eaca18..26e774ece 100644 --- a/pkg/docker/pusher_test.go +++ b/pkg/docker/pusher_test.go @@ -38,6 +38,7 @@ import ( "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/oci" ) func TestGetRegistry(t *testing.T) { @@ -77,8 +78,8 @@ const ( imageRepoDigest = "sha256:00af51d125f3092e157a7f8a717029412dc9d266c017e89cecdfeccb4cc3d7a7" ) -var testCredProvider = docker.CredentialsProvider(func(ctx context.Context, registry string) (docker.Credentials, error) { - return docker.Credentials{ +var testCredProvider = oci.CredentialsProvider(func(ctx context.Context, registry string) (oci.Credentials, error) { + return oci.Credentials{ Username: testUser, Password: testPwd, }, nil diff --git a/pkg/k8s/keychains.go b/pkg/k8s/keychains.go new file mode 100644 index 000000000..6276e708e --- /dev/null +++ b/pkg/k8s/keychains.go @@ -0,0 +1,85 @@ +package k8s + +import ( + "encoding/json" + "fmt" + "os" + "path" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/google" + + "knative.dev/func/pkg/creds" + "knative.dev/func/pkg/oci" +) + +func GetGoogleCredentialLoader() []creds.CredentialsCallback { + return []creds.CredentialsCallback{ + func(registry string) (oci.Credentials, error) { + if registry != "gcr.io" { + return oci.Credentials{}, nil // skip if not GCR + } + + res, err := name.NewRegistry(registry) + if err != nil { + return oci.Credentials{}, fmt.Errorf("parse registry: %w", err) + } + + authenticator, err := google.Keychain.Resolve(res) + if err != nil { + return oci.Credentials{}, fmt.Errorf("resolve google keychain: %w", err) + } + + authCfg, err := authenticator.Authorization() + if err != nil { + return oci.Credentials{}, fmt.Errorf("get authorization: %w", err) + } + + return oci.Credentials{ + Username: authCfg.Username, + Password: authCfg.Password, + }, nil + }, + } +} + +func GetECRCredentialLoader() []creds.CredentialsCallback { + return []creds.CredentialsCallback{} // TODO: Implement ECR credentials loader +} + +func GetACRCredentialLoader() []creds.CredentialsCallback { + return []creds.CredentialsCallback{ + func(registry string) (oci.Credentials, error) { + if !strings.HasSuffix(registry, ".azurecr.io") { + return oci.Credentials{}, nil + } + + f, err := os.Open(path.Join(os.Getenv("HOME"), ".azure", "accessTokens.json")) + if err != nil { + return oci.Credentials{}, fmt.Errorf("open Azure access tokens: %w", err) + } + defer f.Close() + + var tokens []struct { + AccessToken string `json:"accessToken"` + Resource string `json:"resource"` + } + + if err := json.NewDecoder(f).Decode(&tokens); err != nil { + return oci.Credentials{}, fmt.Errorf("decode Azure access tokens: %w", err) + } + + target := "https://" + registry + for _, t := range tokens { + if t.Resource == target { + return oci.Credentials{ + Username: "00000000-0000-0000-0000-000000000000", + Password: t.AccessToken, + }, nil + } + } + return oci.Credentials{}, nil + }, + } +} diff --git a/pkg/k8s/openshift.go b/pkg/k8s/openshift.go index 5797e5986..c50a9459d 100644 --- a/pkg/k8s/openshift.go +++ b/pkg/k8s/openshift.go @@ -14,9 +14,9 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/rand" - "knative.dev/func/pkg/docker" - "knative.dev/func/pkg/docker/creds" + "knative.dev/func/pkg/creds" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/oci" ) const ( @@ -106,7 +106,7 @@ func GetOpenShiftDockerCredentialLoaders() []creds.CredentialsCallback { if !ok { return nil } - var credentials docker.Credentials + var credentials oci.Credentials if authInfo := rawConf.AuthInfos[cc.AuthInfo]; authInfo != nil { credentials.Username = "openshift" @@ -114,11 +114,11 @@ func GetOpenShiftDockerCredentialLoaders() []creds.CredentialsCallback { } return []creds.CredentialsCallback{ - func(registry string) (docker.Credentials, error) { + func(registry string) (oci.Credentials, error) { if registry == openShiftRegistryHostPort { return credentials, nil } - return docker.Credentials{}, creds.ErrCredentialsNotFound + return oci.Credentials{}, creds.ErrCredentialsNotFound }, } diff --git a/pkg/oci/pusher.go b/pkg/oci/pusher.go index 3c8c52a08..9ea1f7e97 100644 --- a/pkg/oci/pusher.go +++ b/pkg/oci/pusher.go @@ -13,7 +13,6 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/google" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/pkg/errors" @@ -22,29 +21,63 @@ import ( fn "knative.dev/func/pkg/functions" ) +type Credentials struct { + Username string + Password string +} + +type CredentialsProvider func(ctx context.Context, image string) (Credentials, error) + +type Opt func(*Pusher) + // Pusher of OCI multi-arch layout directories. type Pusher struct { - Anonymous bool - Insecure bool - Token string - Username string - Verbose bool + Anonymous bool + credentialsProvider CredentialsProvider + + Insecure bool + Token string + Username string + Verbose bool updates chan v1.Update done chan bool } -func NewPusher(insecure, anon, verbose bool) *Pusher { - return &Pusher{ - Insecure: insecure, - Anonymous: anon, - Verbose: verbose, - updates: make(chan v1.Update, 10), - done: make(chan bool, 1), +func EmptyCredentialsProvider(ctx context.Context, registry string) (Credentials, error) { + return Credentials{}, nil +} + +func WithCredentialsProvider(cp CredentialsProvider) Opt { + return func(p *Pusher) { + p.credentialsProvider = cp } } +func WithVerbose(verbose bool) Opt { + return func(pusher *Pusher) { + pusher.Verbose = verbose + } +} + +func NewPusher(insecure, anon, verbose bool, opts ...Opt) *Pusher { + result := &Pusher{ + credentialsProvider: EmptyCredentialsProvider, + Insecure: insecure, + Anonymous: anon, + Verbose: verbose, + updates: make(chan v1.Update, 10), + done: make(chan bool, 1), + } + for _, opt := range opts { + opt(result) + } + return result +} + func (p *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err error) { + credentials, _ := p.credentialsProvider(ctx, f.Build.Image) + go p.handleUpdates(ctx) defer func() { p.done <- true }() buildDir, err := getLastBuildDir(f) @@ -67,7 +100,7 @@ func (p *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err er if err != nil { return } - if err = p.writeIndex(ctx, ref, ii); err != nil { + if err = p.writeIndex(ctx, ref, ii, credentials); err != nil { return } h, err := ii.Digest() @@ -120,7 +153,7 @@ func getLastBuildDir(f fn.Function) (string, error) { } // writeIndex to its defined registry. -func (p *Pusher) writeIndex(ctx context.Context, ref name.Reference, ii v1.ImageIndex) error { +func (p *Pusher) writeIndex(ctx context.Context, ref name.Reference, ii v1.ImageIndex, creds Credentials) error { oo := []remote.Option{ remote.WithContext(ctx), remote.WithProgress(p.updates), @@ -135,10 +168,13 @@ func (p *Pusher) writeIndex(ctx context.Context, ref name.Reference, ii v1.Image } if !p.Anonymous { - a, err := p.authOption(ctx) + a, err := p.authOption(ctx, creds) if err != nil { return err } + if a == nil { + return errors.New("no authentication option provided") + } oo = append(oo, a) } @@ -148,13 +184,14 @@ func (p *Pusher) writeIndex(ctx context.Context, ref name.Reference, ii v1.Image // authOption selects an appropriate authentication option. // If user provided = basic auth (secret is password) // If only secret provided = bearer token auth -// If neither are provided = Returned is a cascading keychain auth mthod +// If neither are provided = creds from credentials provider // which performs the following in order: // - Default Keychain (docker and podman config files) // - Google Keychain // - TODO: ECR Amazon // - TODO: ACR Azure -func (p *Pusher) authOption(ctx context.Context) (remote.Option, error) { +// - interactive prompt for username and password +func (p *Pusher) authOption(ctx context.Context, creds Credentials) (remote.Option, error) { // Basic Auth if provided username, _ := ctx.Value(fn.PushUsernameKey{}).(string) @@ -168,12 +205,10 @@ func (p *Pusher) authOption(ctx context.Context) (remote.Option, error) { return remote.WithAuth(&authn.Basic{Username: username, Password: password}), nil } - // Default chain - return remote.WithAuthFromKeychain(authn.NewMultiKeychain( - authn.DefaultKeychain, // Podman and Docker config files - google.Keychain, // Google - // TODO: Integrate and test ECR and ACR credential helpers: - // authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}), - // authn.NewKeychainFromHelper(acr.ACRCredHelper{}), - )), nil + // Use provided credentials if available or prompt for them + if creds.Username != "" && creds.Password != "" { + return remote.WithAuth(&authn.Basic{Username: creds.Username, Password: creds.Password}), nil + } + + return nil, nil } diff --git a/pkg/pipelines/tekton/gitlab_int_test.go b/pkg/pipelines/tekton/gitlab_int_test.go index 5da692903..2f6bd4776 100644 --- a/pkg/pipelines/tekton/gitlab_int_test.go +++ b/pkg/pipelines/tekton/gitlab_int_test.go @@ -37,9 +37,9 @@ import ( "knative.dev/pkg/apis" "knative.dev/func/pkg/builders/buildpacks" - "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" + "knative.dev/func/pkg/oci" "knative.dev/func/pkg/pipelines" "knative.dev/func/pkg/pipelines/tekton" "knative.dev/func/pkg/random" @@ -102,8 +102,8 @@ func TestGitlab(t *testing.T) { t.Fatal(err) } - credentialsProvider := func(ctx context.Context, image string) (docker.Credentials, error) { - return docker.Credentials{ + credentialsProvider := func(ctx context.Context, image string) (oci.Credentials, error) { + return oci.Credentials{ Username: "", Password: "", }, nil diff --git a/pkg/pipelines/tekton/pipelines_int_test.go b/pkg/pipelines/tekton/pipelines_int_test.go index 997471efe..103dc220b 100644 --- a/pkg/pipelines/tekton/pipelines_int_test.go +++ b/pkg/pipelines/tekton/pipelines_int_test.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" + "knative.dev/func/pkg/oci" "knative.dev/func/pkg/builders/buildpacks" pack "knative.dev/func/pkg/builders/buildpacks" @@ -34,8 +35,8 @@ import ( . "knative.dev/func/pkg/testing" ) -var testCP = func(_ context.Context, _ string) (docker.Credentials, error) { - return docker.Credentials{ +var testCP = func(_ context.Context, _ string) (oci.Credentials, error) { + return oci.Credentials{ Username: "", Password: "", }, nil diff --git a/pkg/pipelines/tekton/pipelines_provider.go b/pkg/pipelines/tekton/pipelines_provider.go index 470eebca1..0d60ba7d4 100644 --- a/pkg/pipelines/tekton/pipelines_provider.go +++ b/pkg/pipelines/tekton/pipelines_provider.go @@ -34,6 +34,7 @@ import ( "knative.dev/func/pkg/k8s" fnlabels "knative.dev/func/pkg/k8s/labels" "knative.dev/func/pkg/knative" + "knative.dev/func/pkg/oci" "knative.dev/pkg/apis" ) @@ -54,11 +55,11 @@ type pacURLCallback = func() (string, error) type PipelinesProvider struct { verbose bool getPacURL pacURLCallback - credentialsProvider docker.CredentialsProvider + credentialsProvider oci.CredentialsProvider decorator PipelineDecorator } -func WithCredentialsProvider(credentialsProvider docker.CredentialsProvider) Opt { +func WithCredentialsProvider(credentialsProvider oci.CredentialsProvider) Opt { return func(pp *PipelinesProvider) { pp.credentialsProvider = credentialsProvider }