Skip to content

Commit 384d7c0

Browse files
authored
Merge pull request #8836 from mainred/ksa-token-credential-provider
feat(credential-provider): support k8s service account token
2 parents 0ad85b3 + cc77e45 commit 384d7c0

File tree

6 files changed

+397
-53
lines changed

6 files changed

+397
-53
lines changed

cmd/acr-credential-provider/main.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131
"k8s.io/component-base/logs"
3232
"k8s.io/klog/v2"
3333

34-
"sigs.k8s.io/cloud-provider-azure/pkg/credentialprovider"
3534
"sigs.k8s.io/cloud-provider-azure/pkg/version"
3635
)
3736

@@ -54,14 +53,8 @@ func main() {
5453
return nil
5554
},
5655
Version: version.Get().GitVersion,
57-
RunE: func(cmd *cobra.Command, args []string) error {
58-
acrProvider, err := credentialprovider.NewAcrProviderFromConfig(args[0], RegistryMirrorStr)
59-
if err != nil {
60-
klog.Errorf("Failed to initialize ACR provider: %v", err)
61-
return err
62-
}
63-
64-
if err := NewCredentialProvider(acrProvider).Run(cmd.Context()); err != nil {
56+
RunE: func(_ *cobra.Command, args []string) error {
57+
if err := NewCredentialProvider(args[0], RegistryMirrorStr).Run(context.TODO()); err != nil {
6558
klog.Errorf("Error running acr credential provider: %v", err)
6659
return err
6760
}

cmd/acr-credential-provider/plugin.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ func init() {
4444

4545
// ExecPlugin implements the exec-based plugin for fetching credentials that is invoked by the kubelet.
4646
type ExecPlugin struct {
47-
plugin credentialprovider.CredentialProvider
47+
configFile string
48+
RegistryMirrorStr string
49+
plugin credentialprovider.CredentialProvider
4850
}
4951

5052
// NewCredentialProvider returns an instance of execPlugin that fetches
5153
// credentials based on the provided plugin implementing the CredentialProvider interface.
52-
func NewCredentialProvider(plugin credentialprovider.CredentialProvider) *ExecPlugin {
53-
return &ExecPlugin{plugin}
54+
func NewCredentialProvider(configFile string, registryMirrorStr string) *ExecPlugin {
55+
return &ExecPlugin{
56+
configFile: configFile,
57+
RegistryMirrorStr: registryMirrorStr,
58+
}
5459
}
5560

5661
// Run executes the credential provider plugin. Required information for the plugin request (in
@@ -85,6 +90,14 @@ func (e *ExecPlugin) runPlugin(ctx context.Context, r io.Reader, w io.Writer, ar
8590
return errors.New("image in plugin request was empty")
8691
}
8792

93+
if e.plugin == nil {
94+
// acr provider plugin are decided at runtime by the request information.
95+
e.plugin, err = credentialprovider.NewAcrProvider(request, e.RegistryMirrorStr, e.configFile)
96+
if err != nil {
97+
return err
98+
}
99+
}
100+
88101
response, err := e.plugin.GetCredentials(ctx, request.Image, args)
89102
if err != nil {
90103
return err

cmd/acr-credential-provider/plugin_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"bytes"
2121
"context"
22+
"os"
2223
"reflect"
2324
"testing"
2425
"time"
@@ -79,10 +80,25 @@ func Test_runPlugin(t *testing.T) {
7980

8081
for _, testcase := range testcases {
8182
t.Run(testcase.name, func(t *testing.T) {
82-
p := NewCredentialProvider(&fakePlugin{})
83+
configFile, err := os.CreateTemp(".", "config.json")
84+
if err != nil {
85+
t.Fatalf("Unexpected error when creating temp file: %v", err)
86+
}
87+
defer os.Remove(configFile.Name())
8388

89+
_, err = configFile.WriteString(`
90+
{
91+
"aadClientId": "foo",
92+
"aadClientSecret": "bar"
93+
}`)
94+
if err != nil {
95+
t.Fatalf("Unexpected error when writing to temp file: %v", err)
96+
}
97+
p := NewCredentialProvider(configFile.Name(), "mcr.microsoft.com:fakeacrname.azurecr.io")
98+
p.plugin = &fakePlugin{}
8499
out := &bytes.Buffer{}
85-
err := p.runPlugin(context.TODO(), testcase.in, out, nil)
100+
101+
err = p.runPlugin(context.TODO(), testcase.in, out, []string{configFile.Name(), "--registry-mirror=mcr.microsoft.com:fakeacrname.azurecr.io"})
86102
if err != nil && !testcase.expectErr {
87103
t.Fatal(err)
88104
}

pkg/credentialprovider/azure_credentials.go

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package credentialprovider
1818

1919
import (
2020
"context"
21-
"errors"
2221
"fmt"
2322
"regexp"
2423
"strings"
@@ -67,29 +66,56 @@ type acrProvider struct {
6766
registryMirror map[string]string // Registry mirror relation: source registry -> target registry
6867
}
6968

70-
func NewAcrProvider(config *providerconfig.AzureClientConfig, environment *azclient.Environment, credential azcore.TokenCredential) CredentialProvider {
71-
return &acrProvider{
72-
config: config,
73-
credential: credential,
74-
environment: environment,
75-
}
76-
}
69+
type getTokenCredentialFunc func(req *v1.CredentialProviderRequest, config *providerconfig.AzureClientConfig) (azcore.TokenCredential, error)
7770

78-
// NewAcrProvider creates a new instance of the ACR provider.
79-
func NewAcrProviderFromConfig(configFile string, registryMirrorStr string) (CredentialProvider, error) {
80-
if len(configFile) == 0 {
81-
return nil, errors.New("no azure credential file is provided")
82-
}
71+
func NewAcrProvider(req *v1.CredentialProviderRequest, registryMirrorStr string, configFile string) (CredentialProvider, error) {
8372
config, err := configloader.Load[providerconfig.AzureClientConfig](context.Background(), nil, &configloader.FileLoaderConfig{FilePath: configFile})
8473
if err != nil {
8574
return nil, fmt.Errorf("failed to load config: %w", err)
8675
}
8776

77+
_, env, err := azclient.GetAzCoreClientOption(&config.ARMClientConfig)
78+
if err != nil {
79+
return nil, err
80+
}
81+
clientOption, _, err := azclient.GetAzCoreClientOption(&config.ARMClientConfig)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
var getTokenCredential getTokenCredentialFunc
87+
88+
// kubelet is responsible for checking the service account token emptiness when service account token is enabled, and only when service account token provide is enabled,
89+
// service account token is set in the request, so we can safely check the service account token emptiness to decide which credential to use.
90+
if len(req.ServiceAccountToken) != 0 {
91+
klog.V(2).Infof("Using service account token to authenticate ACR for image %s", req.Image)
92+
getTokenCredential = getServiceAccountTokenCredential
93+
} else {
94+
klog.V(2).Infof("Using managed identity to authenticate ACR for image %s", req.Image)
95+
getTokenCredential = getManagedIdentityCredential
96+
}
97+
credential, err := getTokenCredential(req, config)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to get token credential for image %s: %w", req.Image, err)
100+
}
101+
102+
return &acrProvider{
103+
config: config,
104+
credential: credential,
105+
environment: env,
106+
cloudConfig: clientOption.Cloud,
107+
registryMirror: parseRegistryMirror(registryMirrorStr),
108+
}, nil
109+
}
110+
111+
// getManagedIdentityCredential creates a new instance of the ACR provider.
112+
func getManagedIdentityCredential(_ *v1.CredentialProviderRequest, config *providerconfig.AzureClientConfig) (azcore.TokenCredential, error) {
113+
88114
var managedIdentityCredential azcore.TokenCredential
89115

90-
clientOption, env, err := azclient.GetAzCoreClientOption(&config.ARMClientConfig)
116+
clientOption, _, err := azclient.GetAzCoreClientOption(&config.ARMClientConfig)
91117
if err != nil {
92-
return nil, err
118+
return nil, fmt.Errorf("Failed to get Azure client options: %w", err)
93119
}
94120
if config.UseManagedIdentityExtension {
95121
credOptions := &azidentity.ManagedIdentityCredentialOptions{
@@ -108,13 +134,45 @@ func NewAcrProviderFromConfig(configFile string, registryMirrorStr string) (Cred
108134
}
109135
}
110136

111-
return &acrProvider{
112-
config: config,
113-
credential: managedIdentityCredential,
114-
environment: env,
115-
cloudConfig: clientOption.Cloud,
116-
registryMirror: parseRegistryMirror(registryMirrorStr),
117-
}, nil
137+
return managedIdentityCredential, nil
138+
}
139+
140+
func getServiceAccountTokenCredential(req *v1.CredentialProviderRequest, config *providerconfig.AzureClientConfig) (azcore.TokenCredential, error) {
141+
if len(req.ServiceAccountToken) == 0 {
142+
return nil, fmt.Errorf("kubernetes Service account token is not provided for image %s", req.Image)
143+
}
144+
klog.V(2).Infof("Kubernetes Service account token is provided for image %s", req.Image)
145+
146+
clientOption, _, err := azclient.GetAzCoreClientOption(&config.ARMClientConfig)
147+
if err != nil {
148+
return nil, fmt.Errorf("Failed to get Azure client options for image %s: %w", req.Image, err)
149+
}
150+
151+
// check required annotations
152+
clientID, ok := req.ServiceAccountAnnotations[clientIDAnnotation]
153+
if !ok || len(clientID) == 0 {
154+
return nil, fmt.Errorf("client id annotation %s is not found or the value is empty", clientIDAnnotation)
155+
}
156+
157+
// check required annotations
158+
tenantID, ok := req.ServiceAccountAnnotations[tenantIDAnnotation]
159+
if !ok || len(tenantID) == 0 {
160+
return nil, fmt.Errorf("tenant id annotation %s is not found or the value is empty", tenantIDAnnotation)
161+
}
162+
163+
// Create getAssertion callback that returns the service account token
164+
getAssertion := func(_ context.Context) (string, error) {
165+
return req.ServiceAccountToken, nil
166+
}
167+
168+
clientAssertCredential, err := azidentity.NewClientAssertionCredential(tenantID, clientID, getAssertion, &azidentity.ClientAssertionCredentialOptions{
169+
ClientOptions: *clientOption,
170+
})
171+
172+
if err != nil {
173+
return nil, fmt.Errorf("failed to initialize client assertion credential: %w", err)
174+
}
175+
return clientAssertCredential, nil
118176
}
119177

120178
func (a *acrProvider) GetCredentials(ctx context.Context, image string, _ []string) (*v1.CredentialProviderResponse, error) {

0 commit comments

Comments
 (0)