Skip to content

Commit f8eee8f

Browse files
committed
ACR Login: Use Azure SDK library to get the credential and token
* Add Azure SDK and implement ACR Login * Unify credential loader interface with context support * Add Test. Ensure canceling context works
1 parent 94711b0 commit f8eee8f

6 files changed

Lines changed: 114 additions & 35 deletions

File tree

cmd/client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser {
108108
func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string) oci.CredentialsProvider {
109109
additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(), k8s.GetGoogleCredentialLoader()...)
110110
additionalLoaders = append(additionalLoaders, k8s.GetECRCredentialLoader()...)
111-
additionalLoaders = append(additionalLoaders, k8s.GetACRCredentialLoader()...)
112111

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

128+
contextLoaders := k8s.GetACRCredentialLoader()
129+
129130
options := []creds.Opt{
130131
creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin, os.Stdout, os.Stderr)),
131132
creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()),
132133
creds.WithTransport(t),
133134
creds.WithAdditionalCredentialLoaders(additionalLoaders...),
135+
creds.WithContextCredentialLoaders(contextLoaders...),
134136
}
135137

136138
// If a custom auth file path is provided, use it

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ replace knative.dev/pkg => knative.dev/pkg v0.0.0-20250716115900-19d3cc2da0b9
1111

1212
require (
1313
github.com/AlecAivazis/survey/v2 v2.3.7
14+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
15+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
1416
github.com/BurntSushi/toml v1.5.0
1517
github.com/Masterminds/semver v1.5.0
1618
github.com/Microsoft/go-winio v0.6.2
@@ -83,6 +85,7 @@ require (
8385
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
8486
dario.cat/mergo v1.0.2 // indirect
8587
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
88+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
8689
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
8790
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
8891
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
@@ -92,6 +95,7 @@ require (
9295
github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect
9396
github.com/Azure/go-autorest/logger v0.2.2 // indirect
9497
github.com/Azure/go-autorest/tracing v0.6.1 // indirect
98+
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
9599
github.com/GoogleContainerTools/kaniko v1.24.0 // indirect
96100
github.com/Masterminds/semver/v3 v3.2.1 // indirect
97101
github.com/Microsoft/hcsshim v0.13.0 // indirect
@@ -167,6 +171,7 @@ require (
167171
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
168172
github.com/gogo/protobuf v1.3.2 // indirect
169173
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
174+
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
170175
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
171176
github.com/golang/protobuf v1.5.4 // indirect
172177
github.com/google/btree v1.1.3 // indirect
@@ -199,6 +204,7 @@ require (
199204
github.com/kevinburke/ssh_config v1.2.0 // indirect
200205
github.com/klauspost/compress v1.18.0 // indirect
201206
github.com/klauspost/pgzip v1.2.6 // indirect
207+
github.com/kylelemons/godebug v1.1.0 // indirect
202208
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
203209
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
204210
github.com/magiconair/properties v1.8.7 // indirect
@@ -239,6 +245,7 @@ require (
239245
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
240246
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
241247
github.com/pjbgf/sha1cd v0.3.2 // indirect
248+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
242249
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
243250
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
244251
github.com/prometheus/client_golang v1.23.2 // indirect

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
6565
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
6666
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
6767
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
68+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
69+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
70+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
71+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
72+
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
73+
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
74+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
75+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
6876
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
6977
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
7078
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
@@ -96,6 +104,10 @@ github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos
96104
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
97105
github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0=
98106
github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc=
107+
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
108+
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
109+
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
110+
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
99111
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
100112
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
101113
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -464,6 +476,8 @@ github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
464476
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
465477
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
466478
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
479+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
480+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
467481
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
468482
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
469483
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -682,6 +696,8 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv
682696
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
683697
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
684698
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
699+
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
700+
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
685701
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
686702
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
687703
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -888,6 +904,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
888904
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
889905
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
890906
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
907+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
908+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
891909
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
892910
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
893911
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

pkg/creds/credentials.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import (
2828

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

31+
// ContextCredentialsCallback represents a credential retrieval callback that supports context for cancellation and timeouts.
32+
// It should return ErrCredentialsNotFound if no credentials are available for the given registry.
33+
type ContextCredentialsCallback func(ctx context.Context, registry string) (oci.Credentials, error)
34+
3135
var ErrUnauthorized = errors.New("bad credentials")
3236

3337
var ErrCredentialsNotFound = errors.New("credentials not found")
@@ -88,6 +92,7 @@ type credentialsProvider struct {
8892
verifyCredentials VerifyCredentialsCallback
8993
promptForCredentialStore ChooseCredentialHelperCallback
9094
credentialLoaders []CredentialsCallback
95+
contextCredentialLoaders []ContextCredentialsCallback
9196
authFilePath string
9297
transport http.RoundTripper
9398
}
@@ -146,6 +151,22 @@ func WithAdditionalCredentialLoaders(loaders ...CredentialsCallback) Opt {
146151
}
147152
}
148153

154+
// WithContextCredentialLoaders adds custom context-aware callbacks for credential retrieval.
155+
// These callbacks accept context for cancellation and timeout support,
156+
// and must return ErrCredentialsNotFound if the credentials are not found.
157+
// The callbacks are intended to be non-interactive, as opposed to WithPromptForCredentials.
158+
//
159+
// This is particularly useful when credential retrieval may need to be interrupted
160+
// (for example, due to network delays, API timeouts, or user cancel actions).
161+
//
162+
// Example usage: Azure Container Registry loader supports context cancellation
163+
// for credential acquisition routines.
164+
func WithContextCredentialLoaders(loaders ...ContextCredentialsCallback) Opt {
165+
return func(opts *credentialsProvider) {
166+
opts.contextCredentialLoaders = append(opts.contextCredentialLoaders, loaders...)
167+
}
168+
}
169+
149170
// NewCredentialsProvider returns new CredentialsProvider that tries to get credentials from docker/func config files.
150171
//
151172
// In case getting credentials from the config files fails
@@ -261,6 +282,19 @@ func NewCredentialsProvider(configPath string, opts ...Opt) oci.CredentialsProvi
261282
return c.getCredentials
262283
}
263284

285+
func (c *credentialsProvider) getAllCredentialLoaders() []ContextCredentialsCallback {
286+
// Unify all callbacks into a single slice
287+
var allLoaders []ContextCredentialsCallback
288+
// Wrap non-context loaders to match the ContextCredentialsCallback signature
289+
for _, load := range c.credentialLoaders {
290+
allLoaders = append(allLoaders, func(ctx context.Context, registry string) (oci.Credentials, error) {
291+
return load(registry)
292+
})
293+
}
294+
allLoaders = append(allLoaders, c.contextCredentialLoaders...)
295+
return allLoaders
296+
}
297+
264298
func (c *credentialsProvider) getCredentials(ctx context.Context, image string) (oci.Credentials, error) {
265299
var err error
266300
result := oci.Credentials{}
@@ -271,10 +305,8 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string)
271305
}
272306

273307
registry := ref.Context().RegistryStr()
274-
for _, load := range c.credentialLoaders {
275-
276-
result, err = load(registry)
277-
308+
for _, load := range c.getAllCredentialLoaders() {
309+
result, err = load(ctx, registry)
278310
if err != nil {
279311
if errors.Is(err, ErrCredentialsNotFound) {
280312
continue
@@ -290,7 +322,6 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string)
290322
return oci.Credentials{}, err
291323
}
292324
}
293-
294325
}
295326

296327
if c.promptForCredentials == nil {

pkg/k8s/keychains.go

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package k8s
22

33
import (
4-
"encoding/json"
4+
"context"
55
"fmt"
6-
"os"
7-
"path"
86
"strings"
97

8+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
9+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
1010
"github.com/google/go-containerregistry/pkg/name"
1111
"github.com/google/go-containerregistry/pkg/v1/google"
1212

@@ -48,38 +48,28 @@ func GetECRCredentialLoader() []creds.CredentialsCallback {
4848
return []creds.CredentialsCallback{} // TODO: Implement ECR credentials loader
4949
}
5050

51-
func GetACRCredentialLoader() []creds.CredentialsCallback {
52-
return []creds.CredentialsCallback{
53-
func(registry string) (oci.Credentials, error) {
51+
func GetACRCredentialLoader() []creds.ContextCredentialsCallback {
52+
return []creds.ContextCredentialsCallback{
53+
func(ctx context.Context, registry string) (oci.Credentials, error) {
5454
if !strings.HasSuffix(registry, ".azurecr.io") {
5555
return oci.Credentials{}, creds.ErrCredentialsNotFound
5656
}
57-
58-
f, err := os.Open(path.Join(os.Getenv("HOME"), ".azure", "accessTokens.json"))
57+
// Use Azure SDK to get access token
58+
azCredentials, err := azidentity.NewDefaultAzureCredential(nil)
5959
if err != nil {
60-
return oci.Credentials{}, fmt.Errorf("open Azure access tokens: %w", err)
60+
return oci.Credentials{}, fmt.Errorf("failed to create default azure credentials: %w", err)
6161
}
62-
defer f.Close()
63-
64-
var tokens []struct {
65-
AccessToken string `json:"accessToken"`
66-
Resource string `json:"resource"`
67-
}
68-
69-
if err := json.NewDecoder(f).Decode(&tokens); err != nil {
70-
return oci.Credentials{}, fmt.Errorf("decode Azure access tokens: %w", err)
71-
}
72-
73-
target := "https://" + registry
74-
for _, t := range tokens {
75-
if t.Resource == target {
76-
return oci.Credentials{
77-
Username: "00000000-0000-0000-0000-000000000000",
78-
Password: t.AccessToken,
79-
}, nil
80-
}
62+
scope := "https://containerregistry.azure.net/.default"
63+
token, err := azCredentials.GetToken(ctx, policy.TokenRequestOptions{
64+
Scopes: []string{scope},
65+
})
66+
if err != nil {
67+
return oci.Credentials{}, fmt.Errorf("failed to get azure access token: %w", err)
8168
}
82-
return oci.Credentials{}, creds.ErrCredentialsNotFound
69+
return oci.Credentials{
70+
Username: "00000000-0000-0000-0000-000000000000",
71+
Password: token.Token,
72+
}, nil
8373
},
8474
}
8575
}

pkg/k8s/keychains_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
"os"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestACRCredentialLoader_ContextCancellation(t *testing.T) {
11+
ctx, cancel := context.WithCancel(context.Background())
12+
cancel()
13+
loader := GetACRCredentialLoader()[0]
14+
registry := "example.azurecr.io"
15+
16+
// Set dummy environment variables to ensure the loader attempts to authenticate
17+
// it will fail due to the context cancellation, but we want to ensure it fails for the right reason
18+
os.Setenv("AZURE_TENANT_ID", "dummy-tenant-id")
19+
os.Setenv("AZURE_CLIENT_ID", "dummy-client-id")
20+
os.Setenv("AZURE_CLIENT_SECRET", "dummy-client-secret")
21+
_, err := loader(ctx, registry)
22+
if err == nil {
23+
t.Fatal("expected error due to context cancellation, got nil")
24+
}
25+
26+
if !strings.Contains(err.Error(), "context canceled") {
27+
t.Fatalf("unexpected error: %v", err)
28+
}
29+
30+
t.Logf("Successfully caught context cancellation error: %v", err)
31+
}

0 commit comments

Comments
 (0)