Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add simple fallback to kargo controller IAM when project-specific credentials cannot be assumed for GAR. #3597

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -310,10 +310,11 @@ Google Artifact Registry repositories.
:::

:::info
Unlike in the case of EKS Pod Identity or IRSA, the Kargo controller does not
fall back on using its own IAM principal directly if it is unable to impersonate
a project-specific Google service account, although that capability is
anticipated in a future release.
Like in the case of EKS Pod Identity or IRSA if project-specific credentials could
not be assumed by the Kargo controller will fall back on on using its own
IAM principal directly. For organizations without strict tenancy requirements, this
can eliminate the need to manage a large number of project-specific IAM roles.
While useful, this approach is not strictly recommended.
:::

### Azure Container Registry (ACR)
Original file line number Diff line number Diff line change
@@ -2,11 +2,15 @@ package gar

import (
"context"
"errors"
"fmt"
"time"

"cloud.google.com/go/compute/metadata"
"github.com/patrickmn/go-cache"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
"google.golang.org/api/iamcredentials/v1"

"github.com/akuity/kargo/internal/credentials"
@@ -22,7 +26,9 @@ type WorkloadIdentityFederationProvider struct {

projectID string

getAccessTokenFn func(ctx context.Context, project string) (string, error)
getAccessTokenFn func(ctx context.Context, project string) (string, bool, error)

tokenSource oauth2.TokenSource
}

func NewWorkloadIdentityFederationProvider(ctx context.Context) credentials.Provider {
@@ -40,14 +46,20 @@ func NewWorkloadIdentityFederationProvider(ctx context.Context) credentials.Prov
return nil
}
logger.Debug("got GCP project ID", "project", projectID)
// Configure DefaultTokenSource as a fallback when project specific Service Account cannot be impersonated
tS, err := google.DefaultTokenSource(ctx, iamcredentials.CloudPlatformScope)
if err != nil {
logger.Info("Fallback to Controller Identity Default Token Source cannot be obtained")
}

p := &WorkloadIdentityFederationProvider{
tokenCache: cache.New(
// Access tokens live for one hour. We'll hang on to them for 40 minutes.
40*time.Minute, // Default ttl for each entry
time.Hour, // Cleanup interval
),
projectID: projectID,
projectID: projectID,
tokenSource: tS,
}
p.getAccessTokenFn = p.getAccessToken
return p
@@ -91,7 +103,7 @@ func (p *WorkloadIdentityFederationProvider) GetCredentials(
}

// Cache miss, get a new token
accessToken, err := p.getAccessTokenFn(ctx, project)
accessToken, cacheToken, err := p.getAccessTokenFn(ctx, project)
if err != nil {
return nil, fmt.Errorf("error getting GCP access token: %w", err)
}
@@ -101,8 +113,10 @@ func (p *WorkloadIdentityFederationProvider) GetCredentials(
return nil, nil
}

// Cache the token
p.tokenCache.Set(cacheKey, accessToken, cache.DefaultExpiration)
if cacheToken {
// Cache the token
p.tokenCache.Set(cacheKey, accessToken, cache.DefaultExpiration)
}

return &credentials.Credentials{
Username: accessTokenUsername,
@@ -115,13 +129,13 @@ func (p *WorkloadIdentityFederationProvider) GetCredentials(
func (p *WorkloadIdentityFederationProvider) getAccessToken(
ctx context.Context,
kargoProject string,
) (string, error) {
) (string, bool, error) {
logger := logging.LoggerFromContext(ctx)

iamSvc, err := iamcredentials.NewService(ctx)
if err != nil {
logger.Error(err, "error creating IAM Credentials service client")
return "", nil
return "", false, nil
}

logger = logger.WithValues("gcpProjectID", p.projectID, "kargoProject", kargoProject)
@@ -135,10 +149,29 @@ func (p *WorkloadIdentityFederationProvider) getAccessToken(
},
).Do()
if err != nil {
var googleErr *googleapi.Error
if errors.As(err, &googleErr) {
switch googleErr.Code {
// fallback to controller identity only if GCP api return 404 code
// project-specific service account do not exists
case 404:
logger.Debug("falling back to Application Default Credentials (ADC)")
token, err := p.tokenSource.Token()
if err != nil {
logger.Error(err, "Error generating access token from Application Default Credentials")
return "", false, nil
}
logger.Debug("Generated access token using Application Default Credentials")
return token.AccessToken, false, nil
default:
logger.Error(err, "error generating access token")
return "", false, nil
}
}
logger.Error(err, "error generating access token")
return "", nil
return "", false, nil
}

logger.Debug("generated Artifact Registry access token")
return resp.AccessToken, nil
return resp.AccessToken, true, nil
}
Original file line number Diff line number Diff line change
@@ -143,8 +143,8 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
provider: &WorkloadIdentityFederationProvider{
projectID: fakeProjectID,
tokenCache: cache.New(10*time.Hour, time.Hour),
getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
return fakeToken, nil
getAccessTokenFn: func(_ context.Context, _ string) (string, bool, error) {
return fakeToken, true, nil
},
},
project: fakeProject,
@@ -167,8 +167,8 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
provider: &WorkloadIdentityFederationProvider{
projectID: fakeProjectID,
tokenCache: cache.New(10*time.Hour, time.Hour),
getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
return "", fmt.Errorf("token fetch error")
getAccessTokenFn: func(_ context.Context, _ string) (string, bool, error) {
return "", true, fmt.Errorf("token fetch error")
},
},
project: fakeProject,
@@ -184,8 +184,8 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
provider: &WorkloadIdentityFederationProvider{
projectID: fakeProjectID,
tokenCache: cache.New(10*time.Hour, time.Hour),
getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
return "", nil
getAccessTokenFn: func(_ context.Context, _ string) (string, bool, error) {
return "", true, nil
},
},
project: fakeProject,