Skip to content

Commit

Permalink
migrate credential provider to track2 client
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinForReal authored and k8s-infra-cherrypick-robot committed Sep 11, 2024
1 parent 34d2f7d commit 3edf794
Show file tree
Hide file tree
Showing 31 changed files with 4,731 additions and 55 deletions.
2 changes: 1 addition & 1 deletion cmd/acr-credential-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func main() {
os.Exit(1)
}

acrProvider, err := credentialprovider.NewAcrProvider(args[0])
acrProvider, err := credentialprovider.NewAcrProviderFromConfig(args[0])
if err != nil {
klog.Errorf("Failed to initialize ACR provider: %v", err)
os.Exit(1)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkc
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.1 h1:Rj6ScDn/5amy1qlQwodwbh+eqXjlopD0LpS6TuN8qcU=
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.1/go.mod h1:QRmL+qp2wYVnAlyVlik0w/vBo3OXf6SP0/WY2IZmYVs=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw=
Expand Down
100 changes: 70 additions & 30 deletions pkg/credentialprovider/azure_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,23 @@ package credentialprovider

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

"sigs.k8s.io/cloud-provider-azure/pkg/azclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/armauth"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader"
providerconfig "sigs.k8s.io/cloud-provider-azure/pkg/provider/config"

"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
v1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
Expand All @@ -55,45 +60,73 @@ type CredentialProvider interface {

// acrProvider implements the credential provider interface for Azure Container Registry.
type acrProvider struct {
config *providerconfig.AzureAuthConfig
environment *azure.Environment
servicePrincipalToken *adal.ServicePrincipalToken
config *providerconfig.AzureAuthConfig
environment *azclient.Environment
credential azcore.TokenCredential
}

func NewAcrProvider(config *providerconfig.AzureAuthConfig, environment *azclient.Environment, credential azcore.TokenCredential) CredentialProvider {
return &acrProvider{
config: config,
credential: credential,
environment: environment,
}
}

// NewAcrProvider creates a new instance of the ACR provider.
func NewAcrProvider(configFile string) (CredentialProvider, error) {
func NewAcrProviderFromConfig(configFile string) (CredentialProvider, error) {
if len(configFile) == 0 {
return nil, errors.New("no azure credential file is provided")
}

f, err := os.Open(configFile)
config, err := configloader.Load[providerconfig.AzureAuthConfig](context.Background(), nil, &configloader.FileLoaderConfig{FilePath: configFile})
if err != nil {
return nil, fmt.Errorf("failed to load config from file %s: %w", configFile, err)
return nil, fmt.Errorf("failed to load config: %w", err)
}
defer f.Close()

return newAcrProviderFromConfigReader(f)
}

func newAcrProviderFromConfigReader(configReader io.Reader) (*acrProvider, error) {
config, env, err := providerconfig.ParseAzureAuthConfig(configReader)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
var envConfig azclient.Environment
envFilePath, ok := os.LookupEnv(azclient.EnvironmentFilepathName)
if ok {
content, err := os.ReadFile(envFilePath)
if err != nil {
return nil, err
}
if err = json.Unmarshal(content, &envConfig); err != nil {
return nil, err
}
}

servicePrincipalToken, err := providerconfig.GetServicePrincipalToken(config, env, env.ServiceManagementEndpoint)
var managedIdentityCredential azcore.TokenCredential

clientOption, err := azclient.GetAzCoreClientOption(&config.ARMClientConfig)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %w", err)
return nil, err
}
if config.UseManagedIdentityExtension {
credOptions := &azidentity.ManagedIdentityCredentialOptions{
ClientOptions: *clientOption,
}
if len(config.UserAssignedIdentityID) > 0 {
if strings.Contains(strings.ToUpper(config.UserAssignedIdentityID), "/SUBSCRIPTIONS/") {
credOptions.ID = azidentity.ResourceID(config.UserAssignedIdentityID)
} else {
credOptions.ID = azidentity.ClientID(config.UserAssignedIdentityID)
}
}
managedIdentityCredential, err = azidentity.NewManagedIdentityCredential(credOptions)
if err != nil {
return nil, err
}
managedIdentityCredential = armauth.NewExpireEarlyTokenWrapper(managedIdentityCredential)
}

return &acrProvider{
config: config,
environment: env,
servicePrincipalToken: servicePrincipalToken,
config: config,
credential: managedIdentityCredential,
environment: &envConfig,
}, nil
}

func (a *acrProvider) GetCredentials(_ context.Context, image string, _ []string) (*v1.CredentialProviderResponse, error) {
func (a *acrProvider) GetCredentials(ctx context.Context, image string, _ []string) (*v1.CredentialProviderResponse, error) {
loginServer := a.parseACRLoginServerFromImage(image)
if loginServer == "" {
klog.V(2).Infof("image(%s) is not from ACR, return empty authentication", image)
Expand All @@ -117,7 +150,7 @@ func (a *acrProvider) GetCredentials(_ context.Context, image string, _ []string
}

if a.config.UseManagedIdentityExtension {
username, password, err := a.getFromACR(loginServer)
username, password, err := a.getFromACR(ctx, loginServer)
if err != nil {
klog.Errorf("error getting credentials from ACR for %s: %s", loginServer, err)
return nil, err
Expand Down Expand Up @@ -163,13 +196,20 @@ func (a *acrProvider) GetCredentials(_ context.Context, image string, _ []string
}

// getFromACR gets credentials from ACR.
func (a *acrProvider) getFromACR(loginServer string) (string, string, error) {
// Run EnsureFresh to make sure the token is valid and does not expire
if err := a.servicePrincipalToken.EnsureFresh(); err != nil {
func (a *acrProvider) getFromACR(ctx context.Context, loginServer string) (string, string, error) {
config, err := azclient.GetAzureCloudConfig(&a.config.ARMClientConfig)
if err != nil {
return "", "", err
}
var armAccessToken azcore.AccessToken
if armAccessToken, err = a.credential.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{
strings.TrimRight(config.Services[azcontainerregistry.ServiceName].Audience, "/") + "/.default",
},
}); err != nil {
klog.Errorf("Failed to ensure fresh service principal token: %v", err)
return "", "", err
}
armAccessToken := a.servicePrincipalToken.OAuthToken()

klog.V(4).Infof("discovering auth redirects for: %s", loginServer)
directive, err := receiveChallengeFromLoginServer(loginServer, "https")
Expand All @@ -180,7 +220,7 @@ func (a *acrProvider) getFromACR(loginServer string) (string, string, error) {

klog.V(4).Infof("exchanging an acr refresh_token")
registryRefreshToken, err := performTokenExchange(
loginServer, directive, a.config.TenantID, armAccessToken)
loginServer, directive, a.config.TenantID, armAccessToken.Token)
if err != nil {
klog.Errorf("failed to perform token exchange: %s", err)
return "", "", err
Expand Down
64 changes: 40 additions & 24 deletions pkg/credentialprovider/azure_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ limitations under the License.
package credentialprovider

import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/Azure/go-autorest/autorest/azure"
"github.com/stretchr/testify/assert"

"sigs.k8s.io/cloud-provider-azure/pkg/azclient"
"sigs.k8s.io/cloud-provider-azure/pkg/provider/config"
)

const (
Expand All @@ -36,22 +37,19 @@ const (
)

func TestGetCredentials(t *testing.T) {
configStr := `
{
"aadClientId": "foo",
"aadClientSecret": "bar"
}`
result := []string{
"*.azurecr.io",
"*.azurecr.cn",
"*.azurecr.de",
"*.azurecr.us",
}

provider, err := newAcrProviderFromConfigReader(bytes.NewBufferString(configStr))
if err != nil {
t.Fatalf("Unexpected error when creating new acr provider: %v", err)
}
provider := NewAcrProvider(&config.AzureAuthConfig{
AzureAuthConfig: azclient.AzureAuthConfig{
AADClientID: "foo",
AADClientSecret: "bar",
},
}, nil, nil)

credResponse, err := provider.GetCredentials(context.TODO(), "foo.azurecr.io/nginx:v1", nil)
if err != nil {
Expand All @@ -75,7 +73,6 @@ func TestGetCredentials(t *testing.T) {
}
}
}

func TestGetCredentialsConfig(t *testing.T) {
// msiEndpointEnv and msiSecretEnv are required because autorest/adal requires IMDS endpoint to be available.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -128,15 +125,35 @@ func TestGetCredentialsConfig(t *testing.T) {
}

for i, test := range testCases {
provider, err := newAcrProviderFromConfigReader(bytes.NewBufferString(test.configStr))
configFile, err := os.CreateTemp(".", "config.json")
if err != nil {
t.Fatalf("Unexpected error when creating temp file: %v", err)
}
_, err = configFile.WriteString(test.configStr)
if err != nil {
t.Fatalf("Unexpected error when writing to temp file: %v", err)
}
err = configFile.Close()
if err != nil {
t.Fatalf("Unexpected error when closing temp file: %v", err)
}
provider, err := NewAcrProviderFromConfig(configFile.Name())
if err != nil && !test.expectError {
t.Fatalf("Unexpected error when creating new acr provider: %v", err)
}
if err != nil && test.expectError {
err = os.Remove(configFile.Name())
if err != nil {
t.Fatalf("Unexpected error when writing to temp file: %v", err)
}
continue
}
err = os.Remove(configFile.Name())
if err != nil {
t.Fatalf("Unexpected error when writing to temp file: %v", err)
}

credResponse, err := provider.GetCredentials(context.TODO(), test.image, nil)
credResponse, err := provider.GetCredentials(context.Background(), test.image, nil)
if err != nil {
t.Fatalf("Unexpected error when fetching acr credentials: %v", err)
}
Expand All @@ -147,18 +164,17 @@ func TestGetCredentialsConfig(t *testing.T) {
}

func TestParseACRLoginServerFromImage(t *testing.T) {
configStr := `
{
"aadClientId": "foo",
"aadClientSecret": "bar"
}`

provider, err := newAcrProviderFromConfigReader(bytes.NewBufferString(configStr))
if err != nil {
t.Fatalf("Unexpected error when creating new acr provider: %v", err)
}
providerInterface := NewAcrProvider(&config.AzureAuthConfig{
AzureAuthConfig: azclient.AzureAuthConfig{
AADClientID: "foo",
AADClientSecret: "bar",
},
}, nil, nil)

provider := providerInterface.(*acrProvider)

provider.environment = &azure.Environment{
provider.environment = &azclient.Environment{
ContainerRegistryDNSSuffix: ".azurecr.my.cloud",
}
tests := []struct {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 3edf794

Please sign in to comment.