diff --git a/api/v1/bucket_types.go b/api/v1/bucket_types.go index 2c733a6cc..96d06c360 100644 --- a/api/v1/bucket_types.go +++ b/api/v1/bucket_types.go @@ -51,6 +51,8 @@ const ( // +kubebuilder:validation:XValidation:rule="self.provider != 'generic' || !has(self.sts) || self.sts.provider == 'ldap'", message="'ldap' is the only supported STS provider for the 'generic' Bucket provider" // +kubebuilder:validation:XValidation:rule="!has(self.sts) || self.sts.provider != 'aws' || !has(self.sts.secretRef)", message="spec.sts.secretRef is not required for the 'aws' STS provider" // +kubebuilder:validation:XValidation:rule="!has(self.sts) || self.sts.provider != 'aws' || !has(self.sts.certSecretRef)", message="spec.sts.certSecretRef is not required for the 'aws' STS provider" +// +kubebuilder:validation:XValidation:rule="self.provider == 'gcp' || !has(self.serviceAccountName)", message="ServiceAccountName is only supported for the 'gcp' Bucket provider" +// +kubebuilder:validation:XValidation:rule="!has(self.secretRef) || !has(self.serviceAccountName)", message="cannot set both .spec.secretRef and .spec.serviceAccountName" type BucketSpec struct { // Provider of the object storage bucket. // Defaults to 'generic', which expects an S3 (API) compatible object @@ -93,6 +95,12 @@ type BucketSpec struct { // +optional SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` + // ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate + // the bucket. For more information about workload identity: + // https://fluxcd.io/flux/components/source/buckets/#workload-identity + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + // CertSecretRef can be given the name of a Secret containing // either or both of // diff --git a/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml b/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml index 74342dcdd..3b1ec05bc 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml @@ -142,6 +142,12 @@ spec: required: - name type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate + the bucket. For more information about workload identity: + https://fluxcd.io/flux/components/source/buckets/#workload-identity + type: string sts: description: |- STS specifies the required configuration to use a Security Token @@ -232,6 +238,10 @@ spec: rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)' - message: spec.sts.certSecretRef is not required for the 'aws' STS provider rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.certSecretRef)' + - message: ServiceAccountName is only supported for the 'gcp' Bucket provider + rule: self.provider == 'gcp' || !has(self.serviceAccountName) + - message: cannot set both .spec.secretRef and .spec.serviceAccountName + rule: '!has(self.secretRef) || !has(self.serviceAccountName)' status: default: observedGeneration: -1 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index be1010e97..d2cd9e7cb 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -15,6 +15,7 @@ rules: - "" resources: - secrets + - serviceaccounts verbs: - get - list diff --git a/docs/api/v1/source.md b/docs/api/v1/source.md index 0e9c7cc8f..dccda7191 100644 --- a/docs/api/v1/source.md +++ b/docs/api/v1/source.md @@ -182,6 +182,20 @@ for the Bucket.

+serviceAccountName
+ +string + + + +(Optional) +

ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate +the bucket. For more information about workload identity: +https://fluxcd.io/flux/components/source/buckets/#workload-identity

+ + + + certSecretRef
@@ -1624,6 +1638,20 @@ for the Bucket.

+serviceAccountName
+ +string + + + +(Optional) +

ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate +the bucket. For more information about workload identity: +https://fluxcd.io/flux/components/source/buckets/#workload-identity

+ + + + certSecretRef
diff --git a/docs/spec/v1/buckets.md b/docs/spec/v1/buckets.md index b00c9bf92..d1b60b8d8 100644 --- a/docs/spec/v1/buckets.md +++ b/docs/spec/v1/buckets.md @@ -647,29 +647,38 @@ Refer to the [Azure documentation](https://learn.microsoft.com/en-us/rest/api/st #### GCP -When a Bucket's `.spec.provider` is set to `gcp`, the source-controller will -attempt to communicate with the specified [Endpoint](#endpoint) using the -[Google Client SDK](https://github.com/googleapis/google-api-go-client). +For detailed setup instructions, see: https://fluxcd.io/flux/integrations/gcp/#for-google-cloud-storage -Without a [Secret reference](#secret-reference), authorization using a -workload identity is attempted by default. The workload identity is obtained -using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable, falling back -to the Google Application Credential file in the config directory. -When a reference is specified, it expects a Secret with a `.data.serviceaccount` -value with a GCP service account JSON file. +##### GCP Controller-Level Workload Identity example -The Provider allows for specifying the -[Bucket location](https://cloud.google.com/storage/docs/locations) using the -[`.spec.region` field](#region). +```yaml +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: Bucket +metadata: + name: gcp-controller-level-workload-identity + namespace: default +spec: + interval: 5m0s + provider: gcp + bucketName: podinfo + endpoint: storage.googleapis.com + region: us-east-1 + timeout: 30s +``` + +##### GCP Object-Level Workload Identity example -##### GCP example +**Note:** To use Object-Level Workload Identity (`.spec.serviceAccountName` with +cloud providers), the controller feature gate `ObjectLevelWorkloadIdentity` must +be enabled. ```yaml --- apiVersion: source.toolkit.fluxcd.io/v1 kind: Bucket metadata: - name: gcp-workload-identity + name: gcp-object-level-workload-identity namespace: default spec: interval: 5m0s @@ -677,7 +686,16 @@ spec: bucketName: podinfo endpoint: storage.googleapis.com region: us-east-1 + serviceAccountName: gcp-workload-identity-sa timeout: 30s +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gcp-workload-identity-sa + namespace: default + annotations: + iam.gke.io/gcp-service-account: ``` ##### GCP static auth example @@ -959,6 +977,29 @@ credentials for the object storage. For some `.spec.provider` implementations the presence of the field is required, see [Provider](#provider) for more details and examples. +### Service Account reference + +`.spec.serviceAccountName` is an optional field to specify a Service Account +in the same namespace as Bucket with purpose depending on the value of +the `.spec.provider` field: + +- When `.spec.provider` is set to `generic`, the controller will fetch the image + pull secrets attached to the Service Account and use them for authentication. +- When `.spec.provider` is set to `aws`, `azure`, or `gcp`, the Service Account + will be used for Workload Identity authentication. In this case, the controller + feature gate `ObjectLevelWorkloadIdentity` must be enabled, otherwise the + controller will error out. + +**Note:** that for a publicly accessible object storage, you don't need to +provide a `secretRef` nor `serviceAccountName`. + +**Important:** `.spec.secretRef` and `.spec.serviceAccountName` are mutually +exclusive and cannot be set at the same time. This constraint is enforced +at the CRD level. + +For a complete guide on how to set up authentication for cloud providers, +see the integration [docs](/flux/integrations/). + ### Prefix `.spec.prefix` is an optional field to enable server-side filtering diff --git a/internal/controller/bucket_controller.go b/internal/controller/bucket_controller.go index 7852d196c..f11078935 100644 --- a/internal/controller/bucket_controller.go +++ b/internal/controller/bucket_controller.go @@ -44,6 +44,8 @@ import ( eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/auth" + "github.com/fluxcd/pkg/cache" "github.com/fluxcd/pkg/runtime/conditions" helper "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/jitter" @@ -116,6 +118,8 @@ var bucketFailConditions = []string{ // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status,verbs=get;update;patch // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/finalizers,verbs=get;create;update;patch;delete // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create // BucketReconciler reconciles a v1.Bucket object. type BucketReconciler struct { @@ -125,6 +129,7 @@ type BucketReconciler struct { Storage *Storage ControllerName string + TokenCache *cache.TokenCache patchOptions []patch.Option } @@ -430,6 +435,18 @@ func (r *BucketReconciler) reconcileStorage(ctx context.Context, sp *patch.Seria // the provider. If this fails, it records v1.FetchFailedCondition=True on // the object and returns early. func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher, obj *sourcev1.Bucket, index *index.Digester, dir string) (sreconcile.Result, error) { + usesObjectLevelWorkloadIdentity := obj.Spec.Provider != "" && obj.Spec.Provider != sourcev1.BucketProviderGeneric && obj.Spec.ServiceAccountName != "" + if usesObjectLevelWorkloadIdentity { + if !auth.IsObjectLevelWorkloadIdentityEnabled() { + const gate = auth.FeatureGateObjectLevelWorkloadIdentity + const msgFmt = "to use spec.serviceAccountName for provider authentication please enable the %s feature gate in the controller" + err := fmt.Errorf(msgFmt, gate) + e := serror.NewStalling(err, meta.FeatureGateDisabledReason) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) + return sreconcile.ResultEmpty, e + } + } + creds, err := r.setupCredentials(ctx, obj) if err != nil { e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason) @@ -590,6 +607,10 @@ func (r *BucketReconciler) reconcileDelete(ctx context.Context, obj *sourcev1.Bu // Remove our finalizer from the list controllerutil.RemoveFinalizer(obj, sourcev1.SourceFinalizer) + // Cleanup caches. + r.TokenCache.DeleteEventsForObject(sourcev1.BucketKind, + obj.GetName(), obj.GetNamespace(), cache.OperationReconcile) + // Stop reconciliation as the object is being deleted return sreconcile.ResultEmpty, nil } @@ -838,19 +859,47 @@ func (r *BucketReconciler) setupCredentials(ctx context.Context, obj *sourcev1.B // createBucketProvider creates a provider-specific bucket client using the given credentials and configuration. // It handles different bucket providers (AWS, GCP, Azure, generic) and returns the appropriate client. func (r *BucketReconciler) createBucketProvider(ctx context.Context, obj *sourcev1.Bucket, creds *bucketCredentials) (BucketProvider, error) { + var authOpts []auth.Option + + if obj.Spec.ServiceAccountName != "" { + serviceAccount := client.ObjectKey{ + Name: obj.Spec.ServiceAccountName, + Namespace: obj.GetNamespace(), + } + authOpts = append(authOpts, auth.WithServiceAccount(serviceAccount, r.Client)) + } + + if r.TokenCache != nil { + involvedObject := cache.InvolvedObject{ + Kind: sourcev1.BucketKind, + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + Operation: cache.OperationReconcile, + } + authOpts = append(authOpts, auth.WithCache(*r.TokenCache, involvedObject)) + } + + if creds.proxyURL != nil { + authOpts = append(authOpts, auth.WithProxyURL(*creds.proxyURL)) + } + switch obj.Spec.Provider { case sourcev1.BucketProviderGoogle: - if err := gcp.ValidateSecret(creds.secret); err != nil { - return nil, err - } var opts []gcp.Option - if creds.secret != nil { - opts = append(opts, gcp.WithSecret(creds.secret)) - } if creds.proxyURL != nil { opts = append(opts, gcp.WithProxyURL(creds.proxyURL)) } - return gcp.NewClient(ctx, opts...) + + if creds.secret != nil { + if err := gcp.ValidateSecret(creds.secret); err != nil { + return nil, err + } + opts = append(opts, gcp.WithSecret(creds.secret)) + } else { + opts = append(opts, gcp.WithAuth(authOpts...)) + } + + return gcp.NewClient(ctx, obj, opts...) case sourcev1.BucketProviderAzure: if err := azure.ValidateSecret(creds.secret); err != nil { diff --git a/internal/controller/bucket_controller_test.go b/internal/controller/bucket_controller_test.go index 4114050e8..dc4698a89 100644 --- a/internal/controller/bucket_controller_test.go +++ b/internal/controller/bucket_controller_test.go @@ -437,6 +437,7 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) { bucketObjects []*s3mock.Object middleware http.Handler secret *corev1.Secret + serviceAccount *corev1.ServiceAccount beforeFunc func(obj *sourcev1.Bucket) want sreconcile.Result wantErr bool @@ -910,6 +911,10 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) { clientBuilder.WithObjects(tt.secret) } + if tt.serviceAccount != nil { + clientBuilder.WithObjects(tt.serviceAccount) + } + r := &BucketReconciler{ EventRecorder: record.NewFakeRecorder(32), Client: clientBuilder.Build(), @@ -972,15 +977,17 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) { func TestBucketReconciler_reconcileSource_gcs(t *testing.T) { tests := []struct { - name string - bucketName string - bucketObjects []*gcsmock.Object - secret *corev1.Secret - beforeFunc func(obj *sourcev1.Bucket) - want sreconcile.Result - wantErr bool - assertIndex *index.Digester - assertConditions []metav1.Condition + name string + bucketName string + bucketObjects []*gcsmock.Object + secret *corev1.Secret + serviceAccount *corev1.ServiceAccount + beforeFunc func(obj *sourcev1.Bucket) + want sreconcile.Result + wantErr bool + assertIndex *index.Digester + assertConditions []metav1.Condition + disableObjectLevelWorkloadIdentity bool }{ { name: "Reconciles GCS source", @@ -1283,6 +1290,80 @@ func TestBucketReconciler_reconcileSource_gcs(t *testing.T) { *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'sha256:b4c2a60ce44b67f5b659a95ce4e4cc9e2a86baf13afb72bd397c5384cbc0e479'"), }, }, + { + name: "GCS Object-Level Workload Identity (no secret)", + bucketName: "dummy", + bucketObjects: []*gcsmock.Object{ + { + Key: "test.txt", + ContentType: "text/plain", + Content: []byte("test"), + Generation: 3, + }, + }, + serviceAccount: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + }, + }, + beforeFunc: func(obj *sourcev1.Bucket) { + obj.Spec.ServiceAccountName = "test-sa" + }, + want: sreconcile.ResultSuccess, + assertIndex: index.NewDigester(index.WithIndex(map[string]string{ + "test.txt": "098f6bcd4621d373cade4e832627b4f6", + })), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'sha256:b4c2a60ce44b67f5b659a95ce4e4cc9e2a86baf13afb72bd397c5384cbc0e479'"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'sha256:b4c2a60ce44b67f5b659a95ce4e4cc9e2a86baf13afb72bd397c5384cbc0e479'"), + }, + }, + { + name: "GCS Controller-Level Workload Identity (no secret, no SA)", + bucketName: "dummy", + bucketObjects: []*gcsmock.Object{ + { + Key: "test.txt", + ContentType: "text/plain", + Content: []byte("test"), + Generation: 3, + }, + }, + beforeFunc: func(obj *sourcev1.Bucket) { + // ServiceAccountName は設定しない (Controller-Level) + }, + want: sreconcile.ResultSuccess, + assertIndex: index.NewDigester(index.WithIndex(map[string]string{ + "test.txt": "098f6bcd4621d373cade4e832627b4f6", + })), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'sha256:b4c2a60ce44b67f5b659a95ce4e4cc9e2a86baf13afb72bd397c5384cbc0e479'"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'sha256:b4c2a60ce44b67f5b659a95ce4e4cc9e2a86baf13afb72bd397c5384cbc0e479'"), + }, + }, + { + name: "GCS Object-Level fails when feature gate disabled", + bucketName: "dummy", + serviceAccount: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + }, + }, + beforeFunc: func(obj *sourcev1.Bucket) { + obj.Spec.ServiceAccountName = "test-sa" + conditions.MarkReconciling(obj, meta.ProgressingReason, "foo") + conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar") + }, + want: sreconcile.ResultEmpty, + wantErr: true, + assertIndex: index.NewDigester(), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.FetchFailedCondition, meta.FeatureGateDisabledReason, "to use spec.serviceAccountName for provider authentication please enable the ObjectLevelWorkloadIdentity feature gate in the controller"), + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"), + *conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"), + }, + disableObjectLevelWorkloadIdentity: true, + }, // TODO: Middleware for mock server to test authentication using secret. } for _, tt := range tests { @@ -1297,12 +1378,24 @@ func TestBucketReconciler_reconcileSource_gcs(t *testing.T) { clientBuilder.WithObjects(tt.secret) } + if tt.serviceAccount != nil { + clientBuilder.WithObjects(tt.serviceAccount) + } + r := &BucketReconciler{ EventRecorder: record.NewFakeRecorder(32), Client: clientBuilder.Build(), Storage: testStorage, patchOptions: getPatchOptions(bucketReadyCondition.Owned, "sc"), } + + // Handle ObjectLevelWorkloadIdentity feature gate environment variable + if tt.disableObjectLevelWorkloadIdentity { + t.Setenv("ENABLE_OBJECT_LEVEL_WORKLOAD_IDENTITY", "false") + } else if tt.serviceAccount != nil { + t.Setenv("ENABLE_OBJECT_LEVEL_WORKLOAD_IDENTITY", "true") + } + tmpDir := t.TempDir() // Test bucket object. diff --git a/main.go b/main.go index 2ed231ce8..ca5e20e90 100644 --- a/main.go +++ b/main.go @@ -272,6 +272,7 @@ func main() { Metrics: metrics, Storage: storage, ControllerName: controllerName, + TokenCache: tokenCache, }).SetupWithManagerAndOptions(mgr, controller.BucketReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), }); err != nil { diff --git a/pkg/gcp/gcp.go b/pkg/gcp/gcp.go index e33e7be34..70afe9fcd 100644 --- a/pkg/gcp/gcp.go +++ b/pkg/gcp/gcp.go @@ -34,6 +34,11 @@ import ( htransport "google.golang.org/api/transport/http" corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" + + "github.com/fluxcd/pkg/auth" + gcpauth "github.com/fluxcd/pkg/auth/gcp" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var ( @@ -69,13 +74,21 @@ func WithProxyURL(proxyURL *url.URL) Option { } } +// WithAuth sets the auth options for workload identity authentication. +func WithAuth(authOpts ...auth.Option) Option { + return func(o *options) { + o.authOpts = authOpts + } +} + type options struct { secret *corev1.Secret proxyURL *url.URL + authOpts []auth.Option // newCustomHTTPClient should create a new HTTP client for interacting with the GCS API. // This is a test-only option required for mocking the real logic, which requires either - // a valid Google Service Account Key or ADC. Both are not available in tests. + // a valid Google Service Account Key or Controller-Level Workload Identity. Both are not available in tests. // The real logic is implemented in the newHTTPClient function, which is used when // constructing the default options object. newCustomHTTPClient func(context.Context, *options) (*http.Client, error) @@ -89,7 +102,7 @@ func newOptions() *options { // NewClient creates a new GCP storage client. The Client will automatically look for the Google Application // Credential environment variable or look for the Google Application Credential file. -func NewClient(ctx context.Context, opts ...Option) (*GCSClient, error) { +func NewClient(ctx context.Context, bucket *sourcev1.Bucket, opts ...Option) (*GCSClient, error) { o := newOptions() for _, opt := range opts { opt(o) @@ -100,7 +113,10 @@ func NewClient(ctx context.Context, opts ...Option) (*GCSClient, error) { switch { case o.secret != nil && o.proxyURL == nil: clientOpts = append(clientOpts, option.WithCredentialsJSON(o.secret.Data["serviceaccount"])) - case o.proxyURL != nil: + case o.secret == nil && o.proxyURL == nil: + tokenSource := gcpauth.NewTokenSource(ctx, o.authOpts...) + clientOpts = append(clientOpts, option.WithTokenSource(tokenSource)) + default: // o.proxyURL != nil: httpClient, err := o.newCustomHTTPClient(ctx, o) if err != nil { return nil, err @@ -135,6 +151,9 @@ func newHTTPClient(ctx context.Context, o *options) (*http.Client, error) { return nil, fmt.Errorf("failed to create Google credentials from secret: %w", err) } opts = append(opts, option.WithCredentials(creds)) + } else { // Workload Identity. + tokenSource := gcpauth.NewTokenSource(ctx, o.authOpts...) + opts = append(opts, option.WithTokenSource(tokenSource)) } transport, err := htransport.NewTransport(ctx, baseTransport, opts...) diff --git a/pkg/gcp/gcp_test.go b/pkg/gcp/gcp_test.go index 84003151d..fadb1e756 100644 --- a/pkg/gcp/gcp_test.go +++ b/pkg/gcp/gcp_test.go @@ -42,6 +42,7 @@ import ( corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" testproxy "github.com/fluxcd/source-controller/tests/proxy" ) @@ -82,6 +83,22 @@ var ( } ) +// createTestBucket creates a test bucket for testing purposes +func createTestBucket() *sourcev1.Bucket { + return &sourcev1.Bucket{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-bucket", + Namespace: "default", + }, + Spec: sourcev1.BucketSpec{ + BucketName: bucketName, + Endpoint: "storage.googleapis.com", + Provider: sourcev1.BucketProviderGoogle, + Interval: v1.Duration{Duration: time.Minute * 5}, + }, + } +} + func TestMain(m *testing.M) { hc, host, close = newTestServer(func(w http.ResponseWriter, r *http.Request) { io.Copy(io.Discard, r.Body) @@ -147,7 +164,8 @@ func TestMain(m *testing.M) { } func TestNewClientWithSecretErr(t *testing.T) { - gcpClient, err := NewClient(context.Background(), WithSecret(secret.DeepCopy())) + bucket := createTestBucket() + gcpClient, err := NewClient(context.Background(), bucket, WithSecret(secret.DeepCopy())) t.Log(err) assert.Error(t, err, "dialing: invalid character 'e' looking for beginning of value") assert.Assert(t, gcpClient == nil) @@ -158,31 +176,29 @@ func TestNewClientWithProxyErr(t *testing.T) { assert.Assert(t, !envADCIsSet) assert.Assert(t, !metadata.OnGCE()) - tests := []struct { - name string - opts []Option - err string - }{ - { - name: "invalid secret", - opts: []Option{WithSecret(secret.DeepCopy())}, - err: "failed to create Google credentials from secret: invalid character 'e' looking for beginning of value", - }, - { - name: "attempts default credentials", - err: "failed to create Google HTTP transport: google: could not find default credentials. See https://cloud.google.com/docs/authentication/external/set-up-adc for more information", - }, - } + t.Run("with secret", func(t *testing.T) { + g := NewWithT(t) + bucket := createTestBucket() + gcpClient, err := NewClient(context.Background(), bucket, + WithProxyURL(&url.URL{}), + WithSecret(secret.DeepCopy())) + g.Expect(err).To(HaveOccurred()) + g.Expect(gcpClient).To(BeNil()) + g.Expect(err.Error()).To(Equal("failed to create Google credentials from secret: invalid character 'e' looking for beginning of value")) + }) - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - opts := append([]Option{WithProxyURL(&url.URL{})}, tt.opts...) - gcpClient, err := NewClient(context.Background(), opts...) - assert.Error(t, err, tt.err) - assert.Assert(t, gcpClient == nil) - }) - } + t.Run("without secret", func(t *testing.T) { + g := NewWithT(t) + bucket := createTestBucket() + gcpClient, err := NewClient(context.Background(), bucket, + WithProxyURL(&url.URL{})) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(gcpClient).NotTo(BeNil()) + bucketAttrs, err := gcpClient.Client.Bucket("some-bucket").Attrs(context.Background()) + g.Expect(err).To(HaveOccurred()) + g.Expect(bucketAttrs).To(BeNil()) + g.Expect(err.Error()).To(ContainSubstring("failed to create provider access token")) + }) } func TestProxy(t *testing.T) { @@ -224,7 +240,8 @@ func TestProxy(t *testing.T) { return &http.Client{Transport: transport}, nil } }) - gcpClient, err := NewClient(context.Background(), opts...) + bucket := createTestBucket() + gcpClient, err := NewClient(context.Background(), bucket, opts...) assert.NilError(t, err) assert.Assert(t, gcpClient != nil) gcpClient.Client.SetRetry(gcpstorage.WithMaxAttempts(1))