Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser {
func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string) oci.CredentialsProvider {
additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(), k8s.GetGoogleCredentialLoader()...)
additionalLoaders = append(additionalLoaders, k8s.GetECRCredentialLoader()...)
additionalLoaders = append(additionalLoaders, k8s.GetACRCredentialLoader()...)

additionalLoaders = append(additionalLoaders,
func(registry string) (oci.Credentials, error) {
Expand All @@ -126,11 +125,14 @@ func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath
},
)

contextLoaders := k8s.GetACRCredentialLoader()

options := []creds.Opt{
creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin, os.Stdout, os.Stderr)),
creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()),
creds.WithTransport(t),
creds.WithAdditionalCredentialLoaders(additionalLoaders...),
creds.WithContextCredentialLoaders(contextLoaders...),
}

// If a custom auth file path is provided, use it
Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ replace knative.dev/pkg => knative.dev/pkg v0.0.0-20250716115900-19d3cc2da0b9

require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/BurntSushi/toml v1.5.0
github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.6.2
Expand Down Expand Up @@ -83,6 +85,7 @@ require (
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
Expand All @@ -92,6 +95,7 @@ require (
github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.2 // indirect
github.com/Azure/go-autorest/tracing v0.6.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/GoogleContainerTools/kaniko v1.24.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Microsoft/hcsshim v0.13.0 // indirect
Expand Down Expand Up @@ -167,6 +171,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
Expand Down Expand Up @@ -199,6 +204,7 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
Expand Down Expand Up @@ -239,6 +245,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
Expand Down
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
Expand Down Expand Up @@ -96,6 +104,10 @@ github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0=
github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
Expand Down Expand Up @@ -464,6 +476,8 @@ github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -682,6 +696,8 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
Expand Down Expand Up @@ -888,6 +904,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
41 changes: 36 additions & 5 deletions pkg/creds/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import (

type CredentialsCallback func(registry string) (oci.Credentials, error)

// ContextCredentialsCallback represents a credential retrieval callback that supports context for cancellation and timeouts.
// It should return ErrCredentialsNotFound if no credentials are available for the given registry.
type ContextCredentialsCallback func(ctx context.Context, registry string) (oci.Credentials, error)

var ErrUnauthorized = errors.New("bad credentials")

var ErrCredentialsNotFound = errors.New("credentials not found")
Expand Down Expand Up @@ -88,6 +92,7 @@ type credentialsProvider struct {
verifyCredentials VerifyCredentialsCallback
promptForCredentialStore ChooseCredentialHelperCallback
credentialLoaders []CredentialsCallback
contextCredentialLoaders []ContextCredentialsCallback
authFilePath string
transport http.RoundTripper
}
Expand Down Expand Up @@ -146,6 +151,22 @@ func WithAdditionalCredentialLoaders(loaders ...CredentialsCallback) Opt {
}
}

// WithContextCredentialLoaders adds custom context-aware callbacks for credential retrieval.
// These callbacks accept context for cancellation and timeout support,
// and must return ErrCredentialsNotFound if the credentials are not found.
// The callbacks are intended to be non-interactive, as opposed to WithPromptForCredentials.
//
// This is particularly useful when credential retrieval may need to be interrupted
// (for example, due to network delays, API timeouts, or user cancel actions).
//
// Example usage: Azure Container Registry loader supports context cancellation
// for credential acquisition routines.
func WithContextCredentialLoaders(loaders ...ContextCredentialsCallback) Opt {
return func(opts *credentialsProvider) {
opts.contextCredentialLoaders = append(opts.contextCredentialLoaders, loaders...)
}
}

// NewCredentialsProvider returns new CredentialsProvider that tries to get credentials from docker/func config files.
//
// In case getting credentials from the config files fails
Expand Down Expand Up @@ -261,6 +282,19 @@ func NewCredentialsProvider(configPath string, opts ...Opt) oci.CredentialsProvi
return c.getCredentials
}

func (c *credentialsProvider) getAllCredentialLoaders() []ContextCredentialsCallback {
// Unify all callbacks into a single slice
var allLoaders []ContextCredentialsCallback
// Wrap non-context loaders to match the ContextCredentialsCallback signature
for _, load := range c.credentialLoaders {
allLoaders = append(allLoaders, func(ctx context.Context, registry string) (oci.Credentials, error) {
return load(registry)
})
}
allLoaders = append(allLoaders, c.contextCredentialLoaders...)
return allLoaders
}

func (c *credentialsProvider) getCredentials(ctx context.Context, image string) (oci.Credentials, error) {
var err error
result := oci.Credentials{}
Expand All @@ -271,10 +305,8 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string)
}

registry := ref.Context().RegistryStr()
for _, load := range c.credentialLoaders {

result, err = load(registry)

for _, load := range c.getAllCredentialLoaders() {
result, err = load(ctx, registry)
if err != nil {
if errors.Is(err, ErrCredentialsNotFound) {
continue
Expand All @@ -290,7 +322,6 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string)
return oci.Credentials{}, err
}
}

}

if c.promptForCredentials == nil {
Expand Down
48 changes: 19 additions & 29 deletions pkg/k8s/keychains.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package k8s

import (
"encoding/json"
"context"
"fmt"
"os"
"path"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/google"

Expand Down Expand Up @@ -48,38 +48,28 @@ 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) {
func GetACRCredentialLoader() []creds.ContextCredentialsCallback {
return []creds.ContextCredentialsCallback{
func(ctx context.Context, registry string) (oci.Credentials, error) {
if !strings.HasSuffix(registry, ".azurecr.io") {
return oci.Credentials{}, creds.ErrCredentialsNotFound
}

f, err := os.Open(path.Join(os.Getenv("HOME"), ".azure", "accessTokens.json"))
// Use Azure SDK to get access token
azCredentials, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return oci.Credentials{}, fmt.Errorf("open Azure access tokens: %w", err)
return oci.Credentials{}, fmt.Errorf("failed to create default azure credentials: %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
}
scope := "https://containerregistry.azure.net/.default"
token, err := azCredentials.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{scope},
})
if err != nil {
return oci.Credentials{}, fmt.Errorf("failed to get azure access token: %w", err)
}
return oci.Credentials{}, creds.ErrCredentialsNotFound
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that creds.ErrCredentialsNotFound is no longer returned. This sentinel value may be depended upon by other systems to fall-through (despite using error types as flow-control being an antipattern; it's life). It might be a good idea to double-check that there's no other code depending on exactly that error type to ensure we're not breaking any "error fallback" systems if one of the other generic errors is returned instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we are still returning ErrCredentialsNotFound when the registry's domain is not azure.io. And if I'm not mistaken, returning ErrCredentialsNotFound in this line made sense at the time, because it was trying to fetch the token from a JSON file. But now we try to generate the token using the SDK. I think if the SDK fails, we should return a plain error. What do you think?

return oci.Credentials{
Username: "00000000-0000-0000-0000-000000000000",
Password: token.Token,
}, nil
},
}
}
20 changes: 20 additions & 0 deletions pkg/k8s/keychains_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package k8s

import (
"context"
"testing"
)

func TestACRCredentialLoader_ContextCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
loader := GetACRCredentialLoader()[0]

registry := "example.azurecr.io"

_, err := loader(ctx, registry)
if err == nil {
t.Fatal("expected error due to context cancellation, got nil")
}
t.Logf("Successfully caught cancellation error: %v", err)
}