You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by at...@apache.org on 2026/08/07 15:55:05 UTC

(iceberg-go) branch main updated: fix(io/gcs): propagate application-default credential errors (#1584)

This is an automated email from the ASF dual-hosted git repository.

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new c98c32d4 fix(io/gcs): propagate application-default credential errors (#1584)
c98c32d4 is described below

commit c98c32d44e82c2c14c277ed11af684a20a99b868
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Fri Aug 7 17:54:59 2026 +0200

    fix(io/gcs): propagate application-default credential errors (#1584)
    
    * fix(io/gcs): propagate application-default credential errors
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
    
    * fix(io/gcs): preserve implicit anonymous fallback
    
    * fix(io/gcs): propagate all ADC discovery errors
    
    * test(io/gcs): use explicit anonymous emulator access
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
    
    * fix(io/gcs): clarify missing credential failures
    
    * test(io/gcs): cover no-auth credential bypass
    
    * fix(io/gcs): satisfy credential test lint
    
    ---------
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
---
 io/gocloud/gcs.go                  | 16 ++++++++++++-
 io/gocloud/gcs_integration_test.go |  4 +++-
 io/gocloud/gcs_test.go             | 49 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/io/gocloud/gcs.go b/io/gocloud/gcs.go
index dc68343f..1b01550b 100644
--- a/io/gocloud/gcs.go
+++ b/io/gocloud/gcs.go
@@ -86,6 +86,14 @@ func ParseGCSConfig(props map[string]string) *gcsblob.Options {
 const gcsScope = "https://www.googleapis.com/auth/devstorage.read_write"
 
 func gcsCredentials(ctx context.Context, props map[string]string) (*google.Credentials, error) {
+	return gcsCredentialsWithDefault(ctx, props, gcp.DefaultCredentials)
+}
+
+func gcsCredentialsWithDefault(
+	ctx context.Context,
+	props map[string]string,
+	defaultCredentials func(context.Context) (*google.Credentials, error),
+) (*google.Credentials, error) {
 	// Explicit no-auth wins over everything: use an anonymous client even when
 	// ADC is available.
 	if noAuth, err := strconv.ParseBool(props[io.GCSNoAuth]); err == nil && noAuth {
@@ -131,7 +139,13 @@ func gcsCredentials(ctx context.Context, props map[string]string) (*google.Crede
 		return parse(data, fmt.Sprintf("%s %q", io.GCSKeyPath, path))
 	}
 
-	creds, _ := gcp.DefaultCredentials(ctx)
+	creds, err := defaultCredentials(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("gcs: no credentials found; set %s=true for anonymous/public bucket access: %w", io.GCSNoAuth, err)
+	}
+	if creds == nil {
+		return nil, fmt.Errorf("gcs: no credentials found; set %s=true for anonymous/public bucket access", io.GCSNoAuth)
+	}
 
 	return creds, nil
 }
diff --git a/io/gocloud/gcs_integration_test.go b/io/gocloud/gcs_integration_test.go
index 0b86628d..ebaba81f 100644
--- a/io/gocloud/gcs_integration_test.go
+++ b/io/gocloud/gcs_integration_test.go
@@ -115,7 +115,9 @@ func (s *GCSIOTestSuite) TestGCSWarehouse() {
 		"type":            "sql",
 		"warehouse":       fmt.Sprintf("gs://%s/iceberg/", gcsBucketName),
 		io.GCSEndpoint:    fmt.Sprintf("http://%s/", gcsEndpoint),
-		io.GCSUseJSONAPI:  "true",
+		// The local fake-gcs-server intentionally has no application credentials.
+		io.GCSNoAuth:     "true",
+		io.GCSUseJSONAPI: "true",
 	}
 
 	cat, err := catalog.Load(context.Background(), "default", properties)
diff --git a/io/gocloud/gcs_test.go b/io/gocloud/gcs_test.go
index a004ec5f..6b3e7a66 100644
--- a/io/gocloud/gcs_test.go
+++ b/io/gocloud/gcs_test.go
@@ -19,6 +19,7 @@ package gocloud
 
 import (
 	"context"
+	"errors"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -28,6 +29,7 @@ import (
 	"github.com/apache/iceberg-go/io"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"golang.org/x/oauth2/google"
 )
 
 // Both parse without a real RSA key (the private key is only read lazily when a
@@ -142,6 +144,53 @@ func TestGCSCredentialsNoAuth(t *testing.T) {
 	assert.Nil(t, creds)
 }
 
+func TestGCSCredentialsPropagatesADCError(t *testing.T) {
+	t.Run("missing configured file", func(t *testing.T) {
+		t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", filepath.Join(t.TempDir(), "missing.json"))
+
+		creds, err := gcsCredentials(context.Background(), nil)
+		require.ErrorContains(t, err, "gcs: no credentials found")
+		require.ErrorContains(t, err, io.GCSNoAuth)
+		assert.Nil(t, creds)
+	})
+
+	t.Run("no configured credentials", func(t *testing.T) {
+		t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
+
+		creds, err := gcsCredentialsWithDefault(context.Background(), nil,
+			func(context.Context) (*google.Credentials, error) {
+				return nil, errors.New("application default credentials unavailable")
+			})
+		require.ErrorContains(t, err, "gcs: no credentials found")
+		require.ErrorContains(t, err, io.GCSNoAuth)
+		assert.Nil(t, creds)
+	})
+
+	t.Run("nil credentials", func(t *testing.T) {
+		creds, err := gcsCredentialsWithDefault(context.Background(), nil,
+			func(context.Context) (*google.Credentials, error) { return nil, nil })
+		require.ErrorContains(t, err, "gcs: no credentials found")
+		assert.Nil(t, creds)
+	})
+
+	t.Run("no-auth skips default credentials", func(t *testing.T) {
+		called := false
+		creds, err := gcsCredentialsWithDefault(context.Background(), map[string]string{io.GCSNoAuth: "true"},
+			func(context.Context) (*google.Credentials, error) {
+				called = true
+
+				return nil, errors.New("default credentials should not be requested")
+			})
+		require.NoError(t, err)
+		assert.Nil(t, creds)
+		assert.False(t, called)
+	})
+
+	creds, err := gcsCredentials(context.Background(), map[string]string{io.GCSNoAuth: "true"})
+	require.NoError(t, err)
+	assert.Nil(t, creds)
+}
+
 // A vended gcs.oauth2.token is turned into a static token source, not dropped.
 func TestGCSCredentialsFromOAuthToken(t *testing.T) {
 	exp := time.Now().Add(time.Hour).UnixMilli()