diff --git a/google/cloud/storage/doc/environment-variables.dox b/google/cloud/storage/doc/environment-variables.dox index cc28d724252d3..93f782cc8be24 100644 --- a/google/cloud/storage/doc/environment-variables.dox +++ b/google/cloud/storage/doc/environment-variables.dox @@ -37,7 +37,21 @@ protocol, but currently unused. `GOOGLE_CLOUD_CPP_STORAGE_GRPC_CONFIG=...`: this is deprecated. -[project-definition-link]: https://cloud.google.com/storage/docs/projects 'Project Definition in GCS' +`GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT=true`: attempt DirectPath +over [Cloud Interconnect][interconnect-link]. This bypasses the usual GCE +environment detection, making DirectPath usable from on-premise hosts. Set it +to `true` to enable the feature. Set it to `false` to disable the feature even +when the corresponding option is enabled in code. Only these exact values are +recognized, and the comparison is case-sensitive; any other value, such as `1` +or `TRUE`, is treated as if the variable were not set. This environment +variable takes precedence over the option. + +@see google::cloud::storage_experimental::DirectPathXdsOverInterconnectOption + +[project-definition-link]: https://cloud.google.com/storage/docs/projects +'Project Definition in GCS' [interconnect-link]: +https://cloud.google.com/network-connectivity/docs/interconnect 'Cloud +Interconnect' @section storage-env-logging Logging diff --git a/google/cloud/storage/grpc_plugin.h b/google/cloud/storage/grpc_plugin.h index 51f25f2fdc73c..ed0a40df0dce6 100644 --- a/google/cloud/storage/grpc_plugin.h +++ b/google/cloud/storage/grpc_plugin.h @@ -128,6 +128,40 @@ struct GrpcMetricsExcludedLabelsOption { using Type = std::set; }; +/** + * Option to attempt DirectPath over Interconnect. + * + * When this option is enabled, the client bypasses GCE VM environment/BIOS + * checks and configures the gRPC channel to target + * `google-c2p:///storage-direct.googleapis.com?force-xds` with standard TLS. + * This makes DirectPath usable from on-premise hosts reaching Google Cloud over + * [Cloud Interconnect], where the usual GCE environment detection would + * otherwise disable it. + * + * The default is `false`. The + * `GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT` environment variable + * overrides this option: set it to `"true"` to enable the feature, or to + * `"false"` to disable it even when this option is set. Only these exact + * values are recognized, and the comparison is case-sensitive; any other + * value, for example `"1"` or `"TRUE"`, is treated as if the variable were not + * set. This lets a deployment turn the feature on or off without rebuilding + * the application. + * + * @par Example: Enable DirectPath over Interconnect + * @code + * namespace gcs_ex = google::cloud::storage_experimental; + * auto client = google::cloud::storage::MakeGrpcClient( + * google::cloud::Options{} + * .set(true)); + * @endcode + * + * [Cloud Interconnect]: + * https://cloud.google.com/network-connectivity/docs/interconnect + */ +struct DirectPathXdsOverInterconnectOption { + using Type = bool; +}; + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_experimental } // namespace cloud diff --git a/google/cloud/storage/internal/grpc/default_options.cc b/google/cloud/storage/internal/grpc/default_options.cc index 27ac0996a369b..e8342f101ed95 100644 --- a/google/cloud/storage/internal/grpc/default_options.cc +++ b/google/cloud/storage/internal/grpc/default_options.cc @@ -36,6 +36,7 @@ namespace { auto constexpr kMinMetricsPeriod = std::chrono::seconds(5); auto constexpr kDefaultMetricsPeriod = std::chrono::seconds(60); auto constexpr kDefaultMetricsExportTimeout = std::chrono::seconds(30); +bool constexpr kDefaultDirectPathXdsOverInterconnect = false; int DefaultGrpcNumChannels(std::string const& endpoint) { // When using Direct Connectivity the gRPC library already does load balancing @@ -100,13 +101,45 @@ Options DefaultOptionsGrpc( auto const ep = google::cloud::internal::UniverseDomainEndpoint( "storage.googleapis.com", options); - // Set default to direct connectivity if we can detect we are running in GCP - // and there is not already a set endpoint or unviverse domain endpoint. - if ((!options.has() && - !options.has()) && - (gcp_detector->IsGoogleCloudBios() || - gcp_detector->IsGoogleCloudServerless())) { - options.set("google-c2p:///storage.googleapis.com"); + if (!options + .has()) { + options.set( + kDefaultDirectPathXdsOverInterconnect); + } + + // The environment variable takes precedence over the option, consistent with. + // An explicit "false" disables the feature even when the option is set, so + // deployments can opt out without rebuilding the application. Any other value + // is treated as if the variable were not set. + auto const direct_path_interconnect_env = + GetEnv("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT"); + if (direct_path_interconnect_env.has_value()) { + if (*direct_path_interconnect_env == "true") { + options.set( + true); + } else if (*direct_path_interconnect_env == "false") { + options.set( + false); + } + } + bool const direct_path_interconnect = + options.get(); + + // Unless the application configured an endpoint or universe domain, default + // to direct connectivity: the Interconnect target when that feature is + // enabled, otherwise the standard target when running in GCP. + if (!options.has() && + !options.has()) { + if (direct_path_interconnect) { + options.set( + "google-c2p:///storage-direct.googleapis.com?force-xds"); + if (!options.has()) { + options.set("storage.googleapis.com"); + } + } else if (gcp_detector->IsGoogleCloudBios() || + gcp_detector->IsGoogleCloudServerless()) { + options.set("google-c2p:///storage.googleapis.com"); + } } options = google::cloud::internal::MergeOptions( diff --git a/google/cloud/storage/internal/grpc/default_options_test.cc b/google/cloud/storage/internal/grpc/default_options_test.cc index 64600ed41b7a3..451c80d8b92af 100644 --- a/google/cloud/storage/internal/grpc/default_options_test.cc +++ b/google/cloud/storage/internal/grpc/default_options_test.cc @@ -54,6 +54,7 @@ TEST(DefaultOptionsGrpc, DefaultOptionsGrpcChannelCount) { {"storage.googleapis.com", 4, std::numeric_limits::max()}, {"google-c2p:///storage.googleapis.com", 1, 1}, {"google-c2p-experimental:///storage.googleapis.com", 1, 1}, + {"google-c2p:///storage-direct.googleapis.com?force-xds", 1, 1}, }; for (auto const& test : cases) { @@ -95,6 +96,242 @@ TEST(DefaultOptionsGrpc, DefaultEndpointsDirectPath) { EXPECT_EQ(options.get(), "storage.googleapis.com"); } +/// @test Verify the option defaults to disabled, and that the resolved default +/// is reported when neither the option nor the environment variable is set. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectDefault) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + {}); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc(Options{}, mock_detector); + EXPECT_TRUE( + options.has()); + EXPECT_FALSE( + options.get()); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_EQ(options.get(), "storage.googleapis.com"); +} + +/// @test Verify enabling the option off-GCE targets the DirectPath resolver, +/// overrides the authority so TLS/SNI matches, and uses a single channel. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectOption) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + {}); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{}.set( + true), + mock_detector); + EXPECT_EQ(options.get(), + "google-c2p:///storage-direct.googleapis.com?force-xds"); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_EQ(options.get(), 1); +} + +/// @test Verify the environment variable enables the feature when the option is +/// unset, i.e. the zero-code rollout path for operators. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectEnvVar) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + "true"); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc(Options{}, mock_detector); + EXPECT_EQ(options.get(), + "google-c2p:///storage-direct.googleapis.com?force-xds"); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_EQ(options.get(), 1); +} + +/// @test Verify explicitly disabling the option off-GCE keeps the client on +/// CloudPath, including the CloudPath multi-channel defaults. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectDisabled) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + {}); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{}.set( + false), + mock_detector); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_GE(options.get(), 4); +} + +/// @test Verify a caller-supplied endpoint wins over the DirectPath target, so +/// testbench and private-endpoint setups keep working. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectUserEndpointOverride) { + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{} + .set(true) + .set("custom-endpoint") + .set("custom-authority"), + mock_detector); + EXPECT_EQ(options.get(), "custom-endpoint"); + EXPECT_EQ(options.get(), "custom-authority"); +} + +/// @test Verify a caller-supplied universe domain wins over the DirectPath +/// target, as DirectPath is only available in the default universe. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectUniverseDomainOverride) { + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{} + .set(true) + .set("my-ud.net"), + mock_detector); + EXPECT_EQ(options.get(), "storage.my-ud.net"); + EXPECT_EQ(options.get(), "storage.my-ud.net"); +} + +/// @test Verify `GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT=true` +/// overrides the option, consistent with other environment variables. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectEnvVarOverridesOption) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + "true"); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{}.set( + false), + mock_detector); + EXPECT_TRUE( + options.get()); + EXPECT_EQ(options.get(), + "google-c2p:///storage-direct.googleapis.com?force-xds"); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_EQ(options.get(), 1); +} + +/// @test Verify `GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT=false` +/// disables the feature even when the option is set, providing an opt-out that +/// does not require rebuilding the application. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectEnvVarFalseOverridesOption) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + "false"); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{}.set( + true), + mock_detector); + EXPECT_FALSE( + options.get()); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_EQ(options.get(), "storage.googleapis.com"); + EXPECT_GE(options.get(), 4); +} + +/// @test Verify unrecognized environment variable values are ignored and leave +/// the option as the application configured it. Each value is tried against +/// both an enabled and a disabled option, so that a case-insensitive match +/// (for example treating `"TRUE"` as `"true"`) would be detected. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectEnvVarInvalidValueIgnored) { + for (auto const* value : {"1", "0", "TRUE", "False", "yes", ""}) { + for (auto const configured : {false, true}) { + SCOPED_TRACE("Testing with value " + std::string(value) + + ", option configured as " + (configured ? "true" : "false")); + ScopedEnvironment env( + "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", value); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{} + .set( + configured), + mock_detector); + EXPECT_EQ( + options + .get(), + configured); + EXPECT_EQ(options.get(), + configured + ? "google-c2p:///storage-direct.googleapis.com?force-xds" + : "storage.googleapis.com"); + } + } +} + +/// @test Verify a caller-supplied authority is preserved when the DirectPath +/// target is selected; `storage.googleapis.com` is only the default. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectPreservesCustomAuthority) { + ScopedEnvironment env("GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS_OVER_INTERCONNECT", + {}); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{} + .set(true) + .set("custom-authority"), + mock_detector); + EXPECT_EQ(options.get(), + "google-c2p:///storage-direct.googleapis.com?force-xds"); + EXPECT_EQ(options.get(), "custom-authority"); + EXPECT_EQ(options.get(), 1); +} + +/// @test Verify universe domain precedence also holds when the domain comes +/// from `GOOGLE_CLOUD_UNIVERSE_DOMAIN` rather than an option. +TEST(DefaultOptionsGrpc, DirectPathOverInterconnectUniverseDomainEnvVar) { + ScopedEnvironment ud("GOOGLE_CLOUD_UNIVERSE_DOMAIN", "my-ud.net"); + auto mock_detector = std::make_shared(); + EXPECT_CALL(*mock_detector, IsGoogleCloudBios()) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*mock_detector, IsGoogleCloudServerless()) + .WillRepeatedly(Return(false)); + + auto options = DefaultOptionsGrpc( + Options{}.set( + true), + mock_detector); + EXPECT_EQ(options.get(), "storage.my-ud.net"); + EXPECT_EQ(options.get(), "storage.my-ud.net"); +} + TEST(DefaultOptionsGrpc, EndpointOptionsOverrideDefaults) { ScopedEnvironment ud("GOOGLE_CLOUD_UNIVERSE_DOMAIN", "ud-env-var.net");