This is an automated email from the ASF dual-hosted git repository.
derrickaw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 509c333ce24 [Go] Avoid importing gcpopts in gcsx and dataflowlib to
prevent flag collisions (#40729)
509c333ce24 is described below
commit 509c333ce24f4e9de783293c2b4eb90c47b991f9
Author: Derrick Williams <[email protected]>
AuthorDate: Fri Oct 23 15:11:24 2026 -0400
[Go] Avoid importing gcpopts in gcsx and dataflowlib to prevent flag
collisions (#40729)
* remove functions from gcpopts and move to gcsx
* switch to a separate package design
* address reviewers' comments
* forgot one change
---
sdks/go/pkg/beam/options/gcpopts/options.go | 46 ++------------
.../pkg/beam/runners/dataflow/dataflowlib/job.go | 4 +-
sdks/go/pkg/beam/util/gcpx/gcp.go | 73 ++++++++++++++++++++++
.../universe_test.go => util/gcpx/gcp_test.go} | 34 +++-------
sdks/go/pkg/beam/util/gcsx/client_test.go | 35 +++++++++--
sdks/go/pkg/beam/util/gcsx/gcs.go | 23 ++++++-
6 files changed, 139 insertions(+), 76 deletions(-)
diff --git a/sdks/go/pkg/beam/options/gcpopts/options.go
b/sdks/go/pkg/beam/options/gcpopts/options.go
index b5df80e2de0..d75585047e1 100644
--- a/sdks/go/pkg/beam/options/gcpopts/options.go
+++ b/sdks/go/pkg/beam/options/gcpopts/options.go
@@ -22,11 +22,11 @@ import (
"os"
"os/exec"
"strings"
- "sync"
"cloud.google.com/go/storage"
- "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime"
"github.com/apache/beam/sdks/v2/go/pkg/beam/log"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/util/gcpx"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/util/gcsx"
"golang.org/x/oauth2/google"
)
@@ -49,24 +49,7 @@ var (
// option, which is how the value reaches remote workers, where flags are not
// parsed. Returns an empty string if no override is configured.
func GetGcsEndpoint() string {
- if *GcsEndpoint != "" {
- return *GcsEndpoint
- }
- return runtime.GlobalOptions.Get("gcs_endpoint")
-}
-
-const universeDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
-
-// findDefaultCredentials is a variable to allow overriding in tests.
-var findDefaultCredentials = google.FindDefaultCredentials
-
-// cachedUniverseDomain caches the universe domain resolved from the default
-// credentials, since resolving it may require a metadata server round trip
-// and clients are created frequently.
-var cachedUniverseDomain struct {
- mu sync.Mutex
- resolved bool
- value string
+ return gcsx.GetGcsEndpoint()
}
// GetUniverseDomain returns the Google Cloud universe domain, first from the
@@ -79,28 +62,7 @@ var cachedUniverseDomain struct {
// option.WithUniverseDomain to support non-default universes such as Trusted
// Partner Cloud (TPC).
func GetUniverseDomain(ctx context.Context) string {
- if ud := os.Getenv(universeDomainEnvVar); ud != "" {
- return ud
- }
-
- cachedUniverseDomain.mu.Lock()
- defer cachedUniverseDomain.mu.Unlock()
- if cachedUniverseDomain.resolved {
- return cachedUniverseDomain.value
- }
- creds, err := findDefaultCredentials(ctx)
- if err != nil {
- // Don't cache: credentials may become available later, and
client
- // construction surfaces the credential error itself.
- return ""
- }
- ud, err := creds.GetUniverseDomain()
- if err != nil {
- return ""
- }
- cachedUniverseDomain.resolved = true
- cachedUniverseDomain.value = ud
- return ud
+ return gcpx.GetUniverseDomain(ctx)
}
// GetProject returns the project, if non empty and exits otherwise.
diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go
b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go
index 841e1749876..ee1dd1b8cc0 100644
--- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go
+++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go
@@ -27,7 +27,7 @@ import (
"github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors"
"github.com/apache/beam/sdks/v2/go/pkg/beam/log"
pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1"
- "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/util/gcpx"
df "google.golang.org/api/dataflow/v1b3"
"google.golang.org/api/option"
"google.golang.org/protobuf/proto"
@@ -325,7 +325,7 @@ func NewClient(ctx context.Context, endpoint string)
(*df.Service, error) {
}
// The client ignores the credentials' universe domain, and requests
fail
// if it differs from the configured one (googleapis.com by default).
- if ud := gcpopts.GetUniverseDomain(ctx); ud != "" {
+ if ud := gcpx.GetUniverseDomain(ctx); ud != "" {
opts = append(opts, option.WithUniverseDomain(ud))
}
return df.NewService(ctx, opts...)
diff --git a/sdks/go/pkg/beam/util/gcpx/gcp.go
b/sdks/go/pkg/beam/util/gcpx/gcp.go
new file mode 100644
index 00000000000..e8b5af04ae0
--- /dev/null
+++ b/sdks/go/pkg/beam/util/gcpx/gcp.go
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package gcpx contains utilities for working with Google Cloud Platform
(GCP).
+package gcpx
+
+import (
+ "context"
+ "os"
+ "sync"
+
+ "golang.org/x/oauth2/google"
+)
+
+const universeDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
+
+// findDefaultCredentials is a variable to allow overriding in tests.
+var findDefaultCredentials = google.FindDefaultCredentials
+
+// cachedUniverseDomain caches the universe domain resolved from the default
+// credentials, since resolving it may require a metadata server round trip
+// and clients are created frequently.
+var cachedUniverseDomain struct {
+ mu sync.Mutex
+ resolved bool
+ value string
+}
+
+// GetUniverseDomain returns the Google Cloud universe domain, first from the
+// GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variable, then falling back to the
+// universe domain of the default credentials. Returns an empty string if it
+// cannot be determined.
+//
+// Google API clients don't infer the universe domain from the credentials,
+// and requests fail if the two differ, so clients should pass this value via
+// option.WithUniverseDomain to support non-default universes such as Trusted
+// Partner Cloud (TPC).
+func GetUniverseDomain(ctx context.Context) string {
+ if ud := os.Getenv(universeDomainEnvVar); ud != "" {
+ return ud
+ }
+
+ cachedUniverseDomain.mu.Lock()
+ defer cachedUniverseDomain.mu.Unlock()
+ if cachedUniverseDomain.resolved {
+ return cachedUniverseDomain.value
+ }
+ creds, err := findDefaultCredentials(ctx)
+ if err != nil {
+ // Don't cache: credentials may become available later, and
client
+ // construction surfaces the credential error itself.
+ return ""
+ }
+ ud, err := creds.GetUniverseDomain()
+ if err != nil {
+ return ""
+ }
+ cachedUniverseDomain.resolved = true
+ cachedUniverseDomain.value = ud
+ return ud
+}
diff --git a/sdks/go/pkg/beam/options/gcpopts/universe_test.go
b/sdks/go/pkg/beam/util/gcpx/gcp_test.go
similarity index 77%
rename from sdks/go/pkg/beam/options/gcpopts/universe_test.go
rename to sdks/go/pkg/beam/util/gcpx/gcp_test.go
index 35050d1010e..e8ba73085cd 100644
--- a/sdks/go/pkg/beam/options/gcpopts/universe_test.go
+++ b/sdks/go/pkg/beam/util/gcpx/gcp_test.go
@@ -13,14 +13,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package gcpopts
+package gcpx
import (
"context"
"errors"
"testing"
- "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime"
"golang.org/x/oauth2/google"
)
@@ -30,6 +29,7 @@ func TestGetUniverseDomain(t *testing.T) {
env string
credsUD string
credsErr error
+ udErr error
want string
wantCalls int // credential lookups across two calls
}{
@@ -51,6 +51,12 @@ func TestGetUniverseDomain(t *testing.T) {
want: "",
wantCalls: 2,
},
+ {
+ name: "universe domain provider errors are not
cached",
+ udErr: errors.New("metadata server error"),
+ want: "",
+ wantCalls: 2,
+ },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -64,7 +70,7 @@ func TestGetUniverseDomain(t *testing.T) {
return nil, test.credsErr
}
return &google.Credentials{
- UniverseDomainProvider: func() (string,
error) { return test.credsUD, nil },
+ UniverseDomainProvider: func() (string,
error) { return test.credsUD, test.udErr },
}, nil
}
t.Cleanup(func() { findDefaultCredentials = orig })
@@ -96,25 +102,3 @@ func resetUniverseDomainCache(t *testing.T) {
reset()
t.Cleanup(reset)
}
-
-func TestGetGcsEndpoint(t *testing.T) {
- const flagValue = "https://flag.example.com/storage/v1/"
- const optionValue = "https://option.example.com/storage/v1/"
-
- origFlag := *GcsEndpoint
- t.Cleanup(func() {
- *GcsEndpoint = origFlag
- runtime.GlobalOptions.Set("gcs_endpoint", "")
- })
- // On workers, the value arrives as a pipeline option rather than a
flag.
- runtime.GlobalOptions.Set("gcs_endpoint", optionValue)
-
- *GcsEndpoint = ""
- if got := GetGcsEndpoint(); got != optionValue {
- t.Errorf("GetGcsEndpoint() with flag unset = %q, want pipeline
option %q", got, optionValue)
- }
- *GcsEndpoint = flagValue
- if got := GetGcsEndpoint(); got != flagValue {
- t.Errorf("GetGcsEndpoint() with flag set = %q, want flag %q",
got, flagValue)
- }
-}
diff --git a/sdks/go/pkg/beam/util/gcsx/client_test.go
b/sdks/go/pkg/beam/util/gcsx/client_test.go
index 6b31646d660..3ec62a3a734 100644
--- a/sdks/go/pkg/beam/util/gcsx/client_test.go
+++ b/sdks/go/pkg/beam/util/gcsx/client_test.go
@@ -16,14 +16,42 @@
package gcsx
import (
+ "flag"
"net/http"
"net/http/httptest"
"strings"
"testing"
- "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime"
)
+func TestGetGcsEndpoint(t *testing.T) {
+ const flagValue = "https://flag.example.com/storage/v1/"
+ const optionValue = "https://option.example.com/storage/v1/"
+
+ origCommandLine := flag.CommandLine
+ flag.CommandLine = flag.NewFlagSet(t.Name(), flag.ContinueOnError)
+ t.Cleanup(func() {
+ flag.CommandLine = origCommandLine
+ runtime.GlobalOptions.Set("gcs_endpoint", "")
+ })
+ // On workers, the value arrives as a pipeline option rather than a
flag.
+ runtime.GlobalOptions.Set("gcs_endpoint", optionValue)
+
+ if got := GetGcsEndpoint(); got != optionValue {
+ t.Errorf("GetGcsEndpoint() with flag unregistered = %q, want
pipeline option %q", got, optionValue)
+ }
+
+ gcsEndpoint := flag.String("gcs_endpoint", "", "")
+ if got := GetGcsEndpoint(); got != optionValue {
+ t.Errorf("GetGcsEndpoint() with flag unset = %q, want pipeline
option %q", got, optionValue)
+ }
+ *gcsEndpoint = flagValue
+ if got := GetGcsEndpoint(); got != flagValue {
+ t.Errorf("GetGcsEndpoint() with flag set = %q, want flag %q",
got, flagValue)
+ }
+}
+
func TestNewUnauthenticatedClient_UsesGcsEndpoint(t *testing.T) {
var gotPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request) {
@@ -33,9 +61,8 @@ func TestNewUnauthenticatedClient_UsesGcsEndpoint(t
*testing.T) {
}))
defer srv.Close()
- orig := *gcpopts.GcsEndpoint
- *gcpopts.GcsEndpoint = srv.URL + "/storage/v1/"
- t.Cleanup(func() { *gcpopts.GcsEndpoint = orig })
+ runtime.GlobalOptions.Set("gcs_endpoint", srv.URL+"/storage/v1/")
+ t.Cleanup(func() { runtime.GlobalOptions.Set("gcs_endpoint", "") })
ctx := t.Context()
client, err := NewUnauthenticatedClient(ctx)
diff --git a/sdks/go/pkg/beam/util/gcsx/gcs.go
b/sdks/go/pkg/beam/util/gcsx/gcs.go
index 73300fcc6dc..9bbc14567e6 100644
--- a/sdks/go/pkg/beam/util/gcsx/gcs.go
+++ b/sdks/go/pkg/beam/util/gcsx/gcs.go
@@ -18,14 +18,16 @@ package gcsx
import (
"context"
+ "flag"
"fmt"
"io"
"net/url"
"path"
"cloud.google.com/go/storage"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime"
"github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors"
- "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/util/gcpx"
"google.golang.org/api/option"
)
@@ -43,7 +45,7 @@ func NewClient(ctx context.Context, scope string)
(*storage.Client, error) {
opts = append(opts, endpointOptions()...)
// storage.NewClient ignores the credentials' universe domain, and
requests
// fail if it differs from the configured one (googleapis.com by
default).
- if ud := gcpopts.GetUniverseDomain(ctx); ud != "" {
+ if ud := gcpx.GetUniverseDomain(ctx); ud != "" {
opts = append(opts, option.WithUniverseDomain(ud))
}
return storage.NewClient(ctx, opts...)
@@ -60,12 +62,27 @@ func NewUnauthenticatedClient(ctx context.Context)
(*storage.Client, error) {
// endpointOptions returns the client options for the --gcs_endpoint override,
// if any.
func endpointOptions() []option.ClientOption {
- if ep := gcpopts.GetGcsEndpoint(); ep != "" {
+ if ep := GetGcsEndpoint(); ep != "" {
return []option.ClientOption{option.WithEndpoint(ep)}
}
return nil
}
+// GetGcsEndpoint returns the GCS endpoint override from the --gcs_endpoint
+// flag. If the flag is unset or not registered, it falls back to the
+// "gcs_endpoint" pipeline option, which is how the value reaches remote
+// workers, where flags are not parsed. Returns an empty string if no override
+// is configured.
+//
+// Looking up the flag by name avoids importing gcpopts, which registers global
+// flags such as --region and --project that can conflict with user binaries.
+func GetGcsEndpoint() string {
+ if f := flag.Lookup("gcs_endpoint"); f != nil && f.Value.String() != ""
{
+ return f.Value.String()
+ }
+ return runtime.GlobalOptions.Get("gcs_endpoint")
+}
+
// Upload writes the given content to GCS. If the specified bucket does not
// exist, it is created first. Returns the full path of the object.
func Upload(ctx context.Context, client *storage.Client, project, bucket,
object string, r io.Reader) (string, error) {