diff --git a/docs/src/main/asciidoc/storage.adoc b/docs/src/main/asciidoc/storage.adoc index 7dd2cf9bb8..002e68f285 100644 --- a/docs/src/main/asciidoc/storage.adoc +++ b/docs/src/main/asciidoc/storage.adoc @@ -119,6 +119,8 @@ The Spring Boot Starter for Google Cloud Storage provides the following configur Base64-encoded contents of OAuth2 account private key for authenticating with the Google Cloud Storage API, if different from the ones in the <> | No | | `spring.cloud.gcp.storage.credentials.scopes` | https://developers.google.com/identity/protocols/googlescopes[OAuth2 scope] for Spring Framework on Google Cloud Storage credentials | No | https://www.googleapis.com/auth/devstorage.read_write +| `spring.cloud.gcp.storage.universe-domain` | Universe domain of the Storage service. The universe domain is a part of the host that is formatted as `https://${service}.${universeDomain}/` | No | Relies on client library’s default universe domain which is `googleapis.com` +| `spring.cloud.gcp.storage.host` | Host of the Storage service which expects `https://${service}.${universeDomain}/` as the format. | No | Relies on client library’s default host which is `https://storage.googleapis.com/` |=== diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryAutoConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryAutoConfiguration.java index 2bab6fb587..7b2ce44422 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryAutoConfiguration.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryAutoConfiguration.java @@ -160,7 +160,7 @@ public BigQueryTemplate bigQueryTemplate( bigQuery, bigQueryWriteClient, bqInitSettings, bigQueryThreadPoolTaskScheduler); } - private String resolveToHost(String endpoint) throws URISyntaxException { + private String resolveToHost(String endpoint) { int portIndex = endpoint.indexOf(":"); if (portIndex != -1) { return "https://" + endpoint.substring(0, portIndex) + "/"; diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryProperties.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryProperties.java index f555c4ae2c..4d515006e2 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryProperties.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/bigquery/GcpBigQueryProperties.java @@ -45,10 +45,17 @@ public class GcpBigQueryProperties implements CredentialsSupplier { /** The size of thread pool of ThreadPoolTaskScheduler used by GcpBigQueryAutoConfiguration */ private int threadPoolSize; + /** + * Universe domain of the BigQuery and BigQueryWriteClient which is part of the endpoint that is + * formatted as `{service}.{universeDomain}:${port}`. + */ private String universeDomain; /** - * Endpoint (formatted as `{service}.{universeDomain}:${port}`) + * Endpoint of the BigQuery and BigQueryWriteClient. Formatted as + * `{service}.{universeDomain}:${port}`. Note that endpoint will be reformatted in {@link + * GcpBigQueryAutoConfiguration} to follow the `https://${service}.${universeDomain}/` pattern + * before being applied to the BigQuery client. */ private String endpoint; diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/kms/GcpKmsProperties.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/kms/GcpKmsProperties.java index df7d90c459..796d44240f 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/kms/GcpKmsProperties.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/kms/GcpKmsProperties.java @@ -31,8 +31,14 @@ public class GcpKmsProperties implements CredentialsSupplier { /** Overrides the GCP Project ID specified in the Core module. */ private String projectId; + + /** + * Universe domain of the client which is part of the endpoint that is formatted as + * `${service}.${universeDomain}:${port}` + */ private String universeDomain; + /** Endpoint of the KMS client which is formatted as`${service}.${universeDomain}:${port}` */ private String endpoint; @Override diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfiguration.java index 7bd6f36fda..2ad85f3dac 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfiguration.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfiguration.java @@ -26,6 +26,8 @@ import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -52,6 +54,10 @@ public class GcpStorageAutoConfiguration { // NOSONAR squid:S1610 must be a clas private final CredentialsProvider credentialsProvider; + private final String universeDomain; + + private final String host; + public GcpStorageAutoConfiguration( GcpProjectIdProvider coreProjectIdProvider, CredentialsProvider credentialsProvider, @@ -67,16 +73,46 @@ public GcpStorageAutoConfiguration( gcpStorageProperties.getCredentials().hasKey() ? new DefaultCredentialsProvider(gcpStorageProperties) : credentialsProvider; + + this.universeDomain = gcpStorageProperties.getUniverseDomain(); + this.host = gcpStorageProperties.getHost(); } @Bean @ConditionalOnMissingBean public Storage storage() throws IOException { - return StorageOptions.newBuilder() - .setHeaderProvider(new UserAgentHeaderProvider(GcpStorageAutoConfiguration.class)) - .setProjectId(this.gcpProjectIdProvider.getProjectId()) - .setCredentials(this.credentialsProvider.getCredentials()) - .build() - .getService(); + StorageOptions.Builder storageOptionsBuilder = + StorageOptions.newBuilder() + .setHeaderProvider(new UserAgentHeaderProvider(GcpStorageAutoConfiguration.class)) + .setProjectId(this.gcpProjectIdProvider.getProjectId()) + .setCredentials(this.credentialsProvider.getCredentials()); + + if (this.universeDomain != null) { + storageOptionsBuilder.setUniverseDomain(this.universeDomain); + } + if (this.host != null) { + storageOptionsBuilder.setHost(verifyAndFetchHost(this.host)); + } + return storageOptionsBuilder.build().getService(); + } + + /** + * Verifies and returns host in the `https://${service}.${universeDomain}/` format, following + * convention in com.google.cloud.ServiceOptions#getResolvedApiaryHost(). + * + * @param host host provided through `spring.cloud.gcp.storage.host` property + * @return host formatted as `https://${service}.${universeDomain}/` + */ + private String verifyAndFetchHost(String host) { + URL url; + try { + url = new URL(host); + } catch (MalformedURLException e) { + throw new IllegalArgumentException( + "Invalid host format: " + + host + + ". Please verify that the specified host follows the 'https://${service}.${universeDomain}/' format"); + } + return url.getProtocol() + "://" + url.getHost() + "/"; } } diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageProperties.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageProperties.java index 2b86449c62..112c626237 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageProperties.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageProperties.java @@ -38,6 +38,15 @@ public Credentials getCredentials() { private String projectId; + /** + * Universe domain of the client which is part of the host that is formatted as + * `https://${service}.${universeDomain}/`. + */ + private String universeDomain; + + /** Host of the Storage client that is formatted as `https://${service}.${universeDomain}/`. */ + private String host; + public String getProjectId() { return projectId; } @@ -45,4 +54,20 @@ public String getProjectId() { public void setProjectId(String projectId) { this.projectId = projectId; } + + public String getUniverseDomain() { + return universeDomain; + } + + public void setUniverseDomain(String universeDomain) { + this.universeDomain = universeDomain; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } } diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfigurationTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfigurationTests.java index fa36ccec9c..e9485dfde6 100644 --- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfigurationTests.java +++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/storage/GcpStorageAutoConfigurationTests.java @@ -17,6 +17,7 @@ package com.google.cloud.spring.autoconfigure.storage; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -48,27 +49,31 @@ class GcpStorageAutoConfigurationTests { .withUserConfiguration(TestConfiguration.class); @Test - void testValidObject() throws Exception { - this.contextRunner.run( - context -> { - Resource resource = context.getBean("mockResource", Resource.class); - assertThat(resource.contentLength()).isEqualTo(4096); - }); + void testValidObject() { + this.contextRunner + .withUserConfiguration(TestStorageConfiguration.class) + .run( + context -> { + Resource resource = context.getBean("mockResource", Resource.class); + assertThat(resource.contentLength()).isEqualTo(4096); + }); } @Test - void testAutoCreateFilesTrueByDefault() throws IOException { - this.contextRunner.run( - context -> { - Resource resource = context.getBean("mockResource", Resource.class); - assertThat(((GoogleStorageResource) resource).isAutoCreateFiles()).isTrue(); - }); + void testAutoCreateFilesTrueByDefault() { + this.contextRunner + .withUserConfiguration(TestStorageConfiguration.class) + .run( + context -> { + Resource resource = context.getBean("mockResource", Resource.class); + assertThat(((GoogleStorageResource) resource).isAutoCreateFiles()).isTrue(); + }); } @Test - void testAutoCreateFilesRespectsProperty() throws IOException { - + void testAutoCreateFilesRespectsProperty() { this.contextRunner + .withUserConfiguration(TestStorageConfiguration.class) .withPropertyValues("spring.cloud.gcp.storage.auto-create-files=false") .run( context -> { @@ -77,9 +82,85 @@ void testAutoCreateFilesRespectsProperty() throws IOException { }); } + @Test + void testUniverseDomain() { + this.contextRunner + .withPropertyValues("spring.cloud.gcp.storage.universe-domain=example.com") + .run( + context -> { + Storage storage = context.getBean("storage", Storage.class); + assertThat(storage.getOptions().getUniverseDomain()).isEqualTo("example.com"); + assertThat(storage.getOptions().getHost()).isEqualTo("https://storage.example.com/"); + }); + } + + @Test + void testHost() { + this.contextRunner + .withPropertyValues("spring.cloud.gcp.storage.host=https://storage.example.com/") + .run( + context -> { + Storage storage = context.getBean("storage", Storage.class); + assertThat(storage.getOptions().getHost()).isEqualTo("https://storage.example.com/"); + }); + } + + @Test + void testUniverseDomainAndHostSet() { + this.contextRunner + .withPropertyValues( + "spring.cloud.gcp.storage.universe-domain=example.com", + "spring.cloud.gcp.storage.host=https://storage.example.com") + .run( + context -> { + Storage storage = context.getBean("storage", Storage.class); + assertThat(storage.getOptions().getUniverseDomain()).isEqualTo("example.com"); + assertThat(storage.getOptions().getHost()).isEqualTo("https://storage.example.com/"); + }); + } + + @Test + void testNoUniverseDomainOrHostSet_useDefaults() { + this.contextRunner.run( + context -> { + Storage storage = context.getBean("storage", Storage.class); + assertThat(storage.getOptions().getUniverseDomain()).isNull(); + assertThat(storage.getOptions().getHost()).isEqualTo("https://storage.googleapis.com/"); + }); + } + + @Test + void testInvalidHost_throwsException() { + this.contextRunner + .withPropertyValues("spring.cloud.gcp.storage.host=storage.example.com") + .run( + context -> { + Exception exception = + assertThrows(Exception.class, () -> context.getBean("storage", Storage.class)); + assertThat(exception).hasRootCauseInstanceOf(IllegalArgumentException.class); + assertThat(exception) + .hasRootCauseMessage( + "Invalid host format: storage.example.com. Please verify that the specified host follows the 'https://${service}.${universeDomain}/' format"); + }); + } + @Configuration static class TestConfiguration { + @Bean + public static CredentialsProvider googleCredentials() { + return () -> mock(Credentials.class); + } + + @Bean + public static GcpProjectIdProvider gcpProjectIdProvider() { + return () -> "default-project"; + } + } + + @Configuration + static class TestStorageConfiguration { + @Value("gs://test-spring/images/spring.png") private Resource remoteResource; @@ -98,15 +179,5 @@ public static Storage mockStorage() throws Exception { when(storage.get(validBlob)).thenReturn(mockedBlob); return storage; } - - @Bean - public static CredentialsProvider googleCredentials() { - return () -> mock(Credentials.class); - } - - @Bean - public static GcpProjectIdProvider gcpProjectIdProvider() { - return () -> "default-project"; - } } }