diff --git a/api/v1beta2/bucket_types.go b/api/v1beta2/bucket_types.go index 928a61373..d30417fba 100644 --- a/api/v1beta2/bucket_types.go +++ b/api/v1beta2/bucket_types.go @@ -103,7 +103,7 @@ type BucketSpec struct { // ProxySecretRef specifies the Secret containing the proxy configuration // to use while communicating with the Bucket server. // - // Only supported for the generic provider. + // Only supported for the `generic` and `gcp` providers. // +optional ProxySecretRef *meta.LocalObjectReference `json:"proxySecretRef,omitempty"` diff --git a/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml b/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml index 5411f06b0..cc3358890 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml @@ -397,7 +397,7 @@ spec: to use while communicating with the Bucket server. - Only supported for the generic provider. + Only supported for the `generic` and `gcp` providers. properties: name: description: Name of the referent. diff --git a/docs/api/v1beta2/source.md b/docs/api/v1beta2/source.md index 451d83611..dd02a3992 100644 --- a/docs/api/v1beta2/source.md +++ b/docs/api/v1beta2/source.md @@ -202,7 +202,7 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference (Optional)

ProxySecretRef specifies the Secret containing the proxy configuration to use while communicating with the Bucket server.

-

Only supported for the generic provider.

+

Only supported for the generic and gcp providers.

@@ -1568,7 +1568,7 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference (Optional)

ProxySecretRef specifies the Secret containing the proxy configuration to use while communicating with the Bucket server.

-

Only supported for the generic provider.

+

Only supported for the generic and gcp providers.

diff --git a/docs/spec/v1beta2/buckets.md b/docs/spec/v1beta2/buckets.md index 630f9f5e5..da51a56e3 100644 --- a/docs/spec/v1beta2/buckets.md +++ b/docs/spec/v1beta2/buckets.md @@ -837,7 +837,7 @@ The Secret can contain three keys: - `password`, to specify the password to use if the proxy server is protected by basic authentication. This is an optional key. -This API is only supported for the `generic` [provider](#provider). +This API is only supported for the `generic` and `gcp` [providers](#provider). Example: diff --git a/go.mod b/go.mod index b8330eb4a..a9de17dd8 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 golang.org/x/crypto v0.22.0 + golang.org/x/oauth2 v0.19.0 golang.org/x/sync v0.7.0 google.golang.org/api v0.177.0 gotest.tools v2.2.0+incompatible @@ -360,7 +361,6 @@ require ( golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.24.0 // indirect - golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/internal/controller/bucket_controller.go b/internal/controller/bucket_controller.go index 656e5d704..9934a7a11 100644 --- a/internal/controller/bucket_controller.go +++ b/internal/controller/bucket_controller.go @@ -431,6 +431,12 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial // Return error as the world as observed may change return sreconcile.ResultEmpty, e } + proxyURL, err := r.getProxyURL(ctx, obj) + if err != nil { + e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error()) + return sreconcile.ResultEmpty, e + } // Construct provider client var provider BucketProvider @@ -441,7 +447,14 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) return sreconcile.ResultEmpty, e } - if provider, err = gcp.NewClient(ctx, secret); err != nil { + var opts []gcp.Option + if secret != nil { + opts = append(opts, gcp.WithSecret(secret)) + } + if proxyURL != nil { + opts = append(opts, gcp.WithProxyURL(proxyURL)) + } + if provider, err = gcp.NewClient(ctx, opts...); err != nil { e := serror.NewGeneric(err, "ClientError") conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) return sreconcile.ResultEmpty, e @@ -469,12 +482,6 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e) return sreconcile.ResultEmpty, e } - proxyURL, err := r.getProxyURL(ctx, obj) - if err != nil { - e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error()) - return sreconcile.ResultEmpty, e - } var opts []minio.Option if secret != nil { opts = append(opts, minio.WithSecret(secret)) diff --git a/pkg/gcp/gcp.go b/pkg/gcp/gcp.go index 77011fada..1f4d495b3 100644 --- a/pkg/gcp/gcp.go +++ b/pkg/gcp/gcp.go @@ -21,13 +21,18 @@ import ( "errors" "fmt" "io" + "net/http" + "net/url" "os" "path/filepath" gcpstorage "cloud.google.com/go/storage" "github.com/go-logr/logr" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" "google.golang.org/api/iterator" "google.golang.org/api/option" + htransport "google.golang.org/api/transport/http" corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" ) @@ -48,24 +53,71 @@ type GCSClient struct { *gcpstorage.Client } -// NewClient creates a new GCP storage client. The Client will automatically look for the Google Application +// Option is a functional option for configuring the GCS client. +type Option func(*options) + +// WithSecret sets the secret to use for authenticating with GCP. +func WithSecret(secret *corev1.Secret) Option { + return func(o *options) { + o.secret = secret + } +} + +// WithProxyURL sets the proxy URL to use for the GCS client. +func WithProxyURL(proxyURL *url.URL) Option { + return func(o *options) { + o.proxyURL = proxyURL + } +} + +type options struct { + secret *corev1.Secret + proxyURL *url.URL +} + +// 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, secret *corev1.Secret) (*GCSClient, error) { - c := &GCSClient{} +func NewClient(ctx context.Context, opts ...Option) (*GCSClient, error) { + var o options + for _, opt := range opts { + opt(&o) + } + secret := o.secret + proxyURL := o.proxyURL + + var creds *google.Credentials + var err error if secret != nil { - client, err := gcpstorage.NewClient(ctx, option.WithCredentialsJSON(secret.Data["serviceaccount"])) - if err != nil { - return nil, err - } - c.Client = client + creds, err = google.CredentialsFromJSON(ctx, secret.Data["serviceaccount"], gcpstorage.ScopeReadOnly) } else { - client, err := gcpstorage.NewClient(ctx) - if err != nil { - return nil, err - } - c.Client = client + creds, err = google.FindDefaultCredentials(ctx, gcpstorage.ScopeReadOnly) + } + if err != nil { + return nil, fmt.Errorf("failed to get Google credentials: %w", err) + } + + baseTransport := http.DefaultTransport.(*http.Transport).Clone() + if proxyURL != nil { + baseTransport.Proxy = http.ProxyURL(proxyURL) + } + googleTransport, err := htransport.NewTransport(ctx, baseTransport) + if err != nil { + return nil, fmt.Errorf("failed to create Google HTTP transport: %w", err) } - return c, nil + + hc := &http.Client{ + Transport: &oauth2.Transport{ + Source: creds.TokenSource, + Base: googleTransport, + }, + } + + client, err := gcpstorage.NewClient(ctx, option.WithHTTPClient(hc)) + if err != nil { + return nil, err + } + + return &GCSClient{Client: client}, nil } // ValidateSecret validates the credential secret. The provided Secret may diff --git a/pkg/gcp/gcp_test.go b/pkg/gcp/gcp_test.go index 9ccf0c645..eb8de7a28 100644 --- a/pkg/gcp/gcp_test.go +++ b/pkg/gcp/gcp_test.go @@ -140,9 +140,9 @@ func TestMain(m *testing.M) { } func TestNewClientWithSecretErr(t *testing.T) { - gcpClient, err := NewClient(context.Background(), secret.DeepCopy()) + gcpClient, err := NewClient(context.Background(), WithSecret(secret.DeepCopy())) t.Log(err) - assert.Error(t, err, "dialing: invalid character 'e' looking for beginning of value") + assert.Error(t, err, "failed to get Google credentials: invalid character 'e' looking for beginning of value") assert.Assert(t, gcpClient == nil) }