Skip to content
Closed
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 @@ -121,7 +121,6 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser {
func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string, insecure bool) 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 @@ -139,12 +138,15 @@ 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.WithInsecure(insecure),
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 @@ -7,6 +7,8 @@ replace github.com/imdario/mergo => dario.cat/mergo v1.0.1

require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/BurntSushi/toml v1.6.0
github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.6.2
Expand Down Expand Up @@ -85,6 +87,7 @@ require (
cyphar.com/go-pathrs v0.2.4 // 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 @@ -94,6 +97,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/Masterminds/semver/v3 v3.4.0 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
Expand Down Expand Up @@ -177,6 +181,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.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.27.0 // indirect
Expand Down Expand Up @@ -207,6 +212,7 @@ require (
github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.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 @@ -246,6 +252,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.5.0 // 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.24.1 // indirect
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
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 @@ -92,6 +100,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.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
Expand Down Expand Up @@ -695,6 +707,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.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
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 @@ -897,6 +911,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.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
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
34 changes: 29 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 @@ -93,6 +97,7 @@ type credentialsProvider struct {
verifyCredentials VerifyCredentialsCallback
promptForCredentialStore ChooseCredentialHelperCallback
credentialLoaders []CredentialsCallback
contextCredentialLoaders []ContextCredentialsCallback
authFilePath string
transport http.RoundTripper
insecure bool
Expand Down Expand Up @@ -160,6 +165,16 @@ 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.
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 @@ -275,6 +290,18 @@ func NewCredentialsProvider(configPath string, opts ...Opt) oci.CredentialsProvi
return c.getCredentials
}

func (c *credentialsProvider) getAllCredentialLoaders() []ContextCredentialsCallback {
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 @@ -285,10 +312,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 @@ -304,7 +329,6 @@ func (c *credentialsProvider) getCredentials(ctx context.Context, image string)
return oci.Credentials{}, err
}
}

}

if c.promptForCredentials == nil {
Expand Down
47 changes: 18 additions & 29 deletions pkg/k8s/keychains.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ package k8s

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"sync"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
dockercreds "github.com/docker/docker-credential-helpers/credentials"
"github.com/google/go-containerregistry/pkg/authn"
Expand Down Expand Up @@ -170,38 +169,28 @@ func GetECRCredentialLoader() []creds.CredentialsCallback {
}
}

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)
}
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)
return oci.Credentials{}, fmt.Errorf("failed to create default azure credentials: %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
return oci.Credentials{
Username: "00000000-0000-0000-0000-000000000000",
Password: token.Token,
}, nil
},
}
}
25 changes: 25 additions & 0 deletions pkg/k8s/keychains_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package k8s

import (
"context"
"errors"
"strings"
"testing"

"knative.dev/func/pkg/creds"
Expand Down Expand Up @@ -75,3 +77,26 @@
}
})
}

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

// Set dummy environment variables to ensure the loader attempts to authenticate

Check failure on line 87 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead

Check failure on line 87 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / woke

[woke] pkg/k8s/keychains_test.go#L87

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead
Raw output
pkg/k8s/keychains_test.go:87:8: [error] `dummy` may be insensitive, use `placeholder`, `sample` instead
// it will fail due to the context cancelation, but we want to ensure it fails for the right reason
t.Setenv("AZURE_TENANT_ID", "dummy-tenant-id")

Check failure on line 89 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead

Check failure on line 89 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / woke

[woke] pkg/k8s/keychains_test.go#L89

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead
Raw output
pkg/k8s/keychains_test.go:89:30: [error] `dummy` may be insensitive, use `placeholder`, `sample` instead
t.Setenv("AZURE_CLIENT_ID", "dummy-client-id")

Check failure on line 90 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead

Check failure on line 90 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / woke

[woke] pkg/k8s/keychains_test.go#L90

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead
Raw output
pkg/k8s/keychains_test.go:90:30: [error] `dummy` may be insensitive, use `placeholder`, `sample` instead
t.Setenv("AZURE_CLIENT_SECRET", "dummy-client-secret")

Check failure on line 91 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead

Check failure on line 91 in pkg/k8s/keychains_test.go

View workflow job for this annotation

GitHub Actions / woke

[woke] pkg/k8s/keychains_test.go#L91

[error] `dummy` may be insensitive, use `placeholder`, `sample` instead
Raw output
pkg/k8s/keychains_test.go:91:34: [error] `dummy` may be insensitive, use `placeholder`, `sample` instead
_, err := loader(ctx, registry)
if err == nil {
t.Fatal("expected error due to context cancellation, got nil")
}

if !strings.Contains(err.Error(), "context canceled") {
t.Fatalf("unexpected error: %v", err)
}

t.Logf("Successfully caught context cancellation error: %v", err)
}
Loading