Skip to content

Commit

Permalink
feat: add properties to customize universe-domain and host in Storage (
Browse files Browse the repository at this point in the history
…#3287)

* feat: add properties to customize universe-domain and host in Storage
  • Loading branch information
mpeddada1 authored Oct 14, 2024
1 parent 05ad3ef commit f5879d9
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 32 deletions.
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/storage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<spring-cloud-gcp-core,Spring Framework on Google Cloud Core Module>> | 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/`
|===


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) + "/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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() + "/";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,36 @@ 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;
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 -> {
Expand All @@ -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;

Expand All @@ -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";
}
}
}

0 comments on commit f5879d9

Please sign in to comment.