diff --git a/oci/auth/gcp/auth.go b/oci/auth/gcp/auth.go index 4e017a53..29cc8bbc 100644 --- a/oci/auth/gcp/auth.go +++ b/oci/auth/gcp/auth.go @@ -28,6 +28,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/fluxcd/pkg/oci" + "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) @@ -48,8 +49,8 @@ func ValidHost(host string) bool { // Client is a GCP GCR client which can log into the registry and return // authorization information. type Client struct { - tokenURL string - proxyURL *url.URL + proxyURL *url.URL + tokenSource oauth2.TokenSource } // Option is a functional option for configuring the client. @@ -62,21 +63,21 @@ func WithProxyURL(proxyURL *url.URL) Option { } } +// WithTokenSource sets a custom token source for the client. +func (c *Client) WithTokenSource(ts oauth2.TokenSource) *Client { + c.tokenSource = ts + return c +} + // NewClient creates a new GCR client with default configurations. func NewClient(opts ...Option) *Client { - client := &Client{tokenURL: GCP_TOKEN_URL} + client := &Client{} for _, opt := range opts { opt(client) } return client } -// WithTokenURL sets the token URL used by the GCR client. -func (c *Client) WithTokenURL(url string) *Client { - c.tokenURL = url - return c -} - // getLoginAuth obtains authentication using the default GCP credential chain. // This supports various authentication methods including service account JSON, // external account JSON, user credentials, and GCE metadata service. @@ -86,10 +87,18 @@ func (c *Client) getLoginAuth(ctx context.Context) (authn.AuthConfig, time.Time, // Define the required scopes for accessing GCR. scopes := []string{"https://www.googleapis.com/auth/cloud-platform"} - // Obtain the default token source. - tokenSource, err := google.DefaultTokenSource(ctx, scopes...) - if err != nil { - return authConfig, time.Time{}, fmt.Errorf("failed to get default token source: %w", err) + var tokenSource oauth2.TokenSource + var err error + + // Use the injected token source if available; otherwise, use the default. + if c.tokenSource != nil { + tokenSource = c.tokenSource + } else { + // Obtain the default token source. + tokenSource, err = google.DefaultTokenSource(ctx, scopes...) + if err != nil { + return authConfig, time.Time{}, fmt.Errorf("failed to get default token source: %w", err) + } } // Retrieve the token. diff --git a/oci/auth/gcp/auth_test.go b/oci/auth/gcp/auth_test.go index 185c34f6..a49b587f 100644 --- a/oci/auth/gcp/auth_test.go +++ b/oci/auth/gcp/auth_test.go @@ -18,49 +18,51 @@ package gcp import ( "context" - "net/http" - "net/http/httptest" + "fmt" "testing" "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" . "github.com/onsi/gomega" + "golang.org/x/oauth2" ) const testValidGCRImage = "gcr.io/foo/bar:v1" +type fakeTokenSource struct { + token *oauth2.Token + err error +} + +func (f *fakeTokenSource) Token() (*oauth2.Token, error) { + return f.token, f.err +} + func TestGetLoginAuth(t *testing.T) { tests := []struct { name string - responseBody string - statusCode int + token *oauth2.Token + tokenErr error wantErr bool wantAuthConfig authn.AuthConfig }{ { name: "success", - responseBody: `{ - "access_token": "some-token", - "expires_in": 10, - "token_type": "foo" -}`, - statusCode: http.StatusOK, + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, wantAuthConfig: authn.AuthConfig{ Username: "oauth2accesstoken", Password: "some-token", }, }, { - name: "fail", - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - { - name: "invalid response", - responseBody: "foo", - statusCode: http.StatusOK, - wantErr: true, + name: "fail", + tokenErr: fmt.Errorf("token error"), + wantErr: true, }, } @@ -68,22 +70,17 @@ func TestGetLoginAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: tt.token, + err: tt.tokenErr, } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - gc := NewClient().WithTokenURL(srv.URL) + gc := NewClient().WithTokenSource(fakeTS) a, expiresAt, err := gc.getLoginAuth(context.TODO()) g.Expect(err != nil).To(Equal(tt.wantErr)) if !tt.wantErr { - g.Expect(expiresAt).To(BeTemporally("~", time.Now().Add(10*time.Second), time.Second)) - } - if tt.statusCode == http.StatusOK { + g.Expect(expiresAt).To(BeTemporally("~", tt.token.Expiry, time.Second)) g.Expect(a).To(Equal(tt.wantAuthConfig)) } }) @@ -111,40 +108,45 @@ func TestValidHost(t *testing.T) { func TestLogin(t *testing.T) { tests := []struct { - name string - autoLogin bool - image string - statusCode int - testOIDC bool - wantErr bool + name string + autoLogin bool + image string + token *oauth2.Token + tokenErr error + wantErr bool }{ { - name: "no auto login", - autoLogin: false, - image: testValidGCRImage, - statusCode: http.StatusOK, - wantErr: true, + name: "no auto login", + autoLogin: false, + image: testValidGCRImage, + wantErr: true, }, { - name: "with auto login", - autoLogin: true, - image: testValidGCRImage, - testOIDC: true, - statusCode: http.StatusOK, + name: "with auto login", + autoLogin: true, + image: testValidGCRImage, + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, }, { - name: "login failure", - autoLogin: true, - image: testValidGCRImage, - statusCode: http.StatusInternalServerError, - testOIDC: true, - wantErr: true, + name: "login failure", + autoLogin: true, + image: testValidGCRImage, + tokenErr: fmt.Errorf("token error"), + wantErr: true, }, { - name: "non GCR image", - autoLogin: true, - image: "foo/bar:v1", - statusCode: http.StatusOK, + name: "non GCR image", + autoLogin: true, + image: "foo/bar:v1", + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, }, } @@ -152,27 +154,19 @@ func TestLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(`{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - ref, err := name.ParseReference(tt.image) g.Expect(err).ToNot(HaveOccurred()) - gc := NewClient().WithTokenURL(srv.URL) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: tt.token, + err: tt.tokenErr, + } + + gc := NewClient().WithTokenSource(fakeTS) _, err = gc.Login(context.TODO(), tt.autoLogin, tt.image, ref) g.Expect(err != nil).To(Equal(tt.wantErr)) - - if tt.testOIDC { - _, err = gc.OIDCLogin(context.TODO()) - g.Expect(err != nil).To(Equal(tt.wantErr)) - } }) } } diff --git a/oci/auth/login/login.go b/oci/auth/login/login.go index 9ffaef94..a038eec3 100644 --- a/oci/auth/login/login.go +++ b/oci/auth/login/login.go @@ -148,6 +148,7 @@ func (m *Manager) Login(ctx context.Context, url string, ref name.Reference, opt ) if opts.Cache != nil { key, err = m.keyFromURL(url, provider) + fmt.Println("key", key) if err != nil { logr.FromContextOrDiscard(ctx).Error(err, "failed to get cache key") } else { diff --git a/oci/auth/login/login_test.go b/oci/auth/login/login_test.go index 2aebdfab..67a8544f 100644 --- a/oci/auth/login/login_test.go +++ b/oci/auth/login/login_test.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" . "github.com/onsi/gomega" + "golang.org/x/oauth2" "github.com/fluxcd/pkg/cache" "github.com/fluxcd/pkg/oci" @@ -38,6 +39,15 @@ import ( "github.com/fluxcd/pkg/oci/auth/gcp" ) +type fakeTokenSource struct { + token *oauth2.Token + err error +} + +func (f *fakeTokenSource) Token() (*oauth2.Token, error) { + return f.token, f.err +} + func TestImageRegistryProvider(t *testing.T) { tests := []struct { name string @@ -100,11 +110,18 @@ func TestLogin(t *testing.T) { }, { name: "gcr", - responseBody: `{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`, providerOpts: ProviderOptions{GcpAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { - // Create GCR client and configure the manager. - gcrClient := gcp.NewClient().WithTokenURL(serverURL) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, + } + // Create GCR client with fake token source + gcrClient := gcp.NewClient().WithTokenSource(fakeTS) mgr.WithGCRClient(gcrClient) *image = "gcr.io/foo/bar:v1" @@ -122,9 +139,8 @@ func TestLogin(t *testing.T) { }, // NOTE: This fails because the azure exchanger uses the image host // to exchange token which can't be modified here without - // interfering image name based categorization of the login - // provider, that's actually being tested here. This is tested in - // detail in the azure package. + // interfering with image name based categorization of the login + // provider. This is tested in detail in the azure package. wantErr: true, }, { @@ -140,21 +156,28 @@ func TestLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - // Create test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(tt.responseBody)) + // Create test server if responseBody is set. + var srv *httptest.Server + if tt.responseBody != "" { + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tt.responseBody)) + } + srv = httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) mgr := NewManager() var image string if tt.beforeFunc != nil { - tt.beforeFunc(srv.URL, mgr, &image) + serverURL := "" + if srv != nil { + serverURL = srv.URL + } + tt.beforeFunc(serverURL, mgr, &image) } ref, err := name.ParseReference(image) @@ -167,7 +190,7 @@ func TestLogin(t *testing.T) { } func TestLogin_WithCache(t *testing.T) { - timestamp := time.Now().Add(10 * time.Second).Unix() + timestamp := time.Now().Add(10 * time.Second) tests := []struct { name string responseBody string @@ -178,7 +201,7 @@ func TestLogin_WithCache(t *testing.T) { }{ { name: "ecr", - responseBody: fmt.Sprintf(`{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ=","expiresAt": %d}]}`, timestamp), + responseBody: fmt.Sprintf(`{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ=","expiresAt": "%s"}]}`, timestamp.Format(time.RFC3339)), providerOpts: ProviderOptions{AwsAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { // Create ECR client and configure the manager. @@ -198,11 +221,18 @@ func TestLogin_WithCache(t *testing.T) { }, { name: "gcr", - responseBody: `{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`, providerOpts: ProviderOptions{GcpAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { - // Create GCR client and configure the manager. - gcrClient := gcp.NewClient().WithTokenURL(serverURL) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: timestamp, + }, + } + // Create GCR client with fake token source + gcrClient := gcp.NewClient().WithTokenSource(fakeTS) mgr.WithGCRClient(gcrClient) *image = "gcr.io/foo/bar:v1" @@ -213,16 +243,13 @@ func TestLogin_WithCache(t *testing.T) { responseBody: `{"refresh_token": "bbbbb"}`, providerOpts: ProviderOptions{AzureAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { + // Create ACR client and configure the manager. acrClient := azure.NewClient().WithTokenCredential(&azure.FakeTokenCredential{Token: "foo"}).WithScheme("http") mgr.WithACRClient(acrClient) *image = "foo.azurecr.io/bar:v1" }, - // NOTE: This fails because the azure exchanger uses the image host - // to exchange token which can't be modified here without - // interfering image name based categorization of the login - // provider, that's actually being tested here. This is tested in - // detail in the azure package. + // NOTE: This test may still fail because of the way the Azure exchanger uses the image host. wantErr: true, }, } @@ -231,21 +258,28 @@ func TestLogin_WithCache(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - // Create test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(tt.responseBody)) + // Create test server if responseBody is set. + var srv *httptest.Server + if tt.responseBody != "" { + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tt.responseBody)) + } + srv = httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) mgr := NewManager() var image string if tt.beforeFunc != nil { - tt.beforeFunc(srv.URL, mgr, &image) + serverURL := "" + if srv != nil { + serverURL = srv.URL + } + tt.beforeFunc(serverURL, mgr, &image) } ref, err := name.ParseReference(image) @@ -262,6 +296,7 @@ func TestLogin_WithCache(t *testing.T) { g.Expect(err).To(HaveOccurred()) } else { key, err := mgr.keyFromURL(image, ImageRegistryProvider(image, ref)) + fmt.Println("key", key) g.Expect(err).ToNot(HaveOccurred()) auth, exists, err := getObjectFromCache(cache, key) g.Expect(err).ToNot(HaveOccurred()) @@ -272,7 +307,7 @@ func TestLogin_WithCache(t *testing.T) { expiration, err := cache.GetExpiration(obj) g.Expect(err).ToNot(HaveOccurred()) g.Expect(expiration).ToNot(BeZero()) - g.Expect(expiration).To(BeTemporally("~", time.Unix(timestamp, 0), 1*time.Second)) + g.Expect(expiration).To(BeTemporally("~", timestamp, 1*time.Second)) } }) }