Skip to content

Commit ea17a67

Browse files
committed
Improved exception handling for GoogleCloudStorageBlobStore
1 parent a65adcd commit ea17a67

File tree

4 files changed

+112
-61
lines changed

4 files changed

+112
-61
lines changed

geowebcache/gcsblob/src/main/java/org/geowebcache/storage/blobstore/gcs/GoogleCloudStorageBlobStore.java

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import javax.annotation.Nullable;
3939
import org.geotools.util.logging.Logging;
4040
import org.geowebcache.GeoWebCacheException;
41+
import org.geowebcache.config.XMLConfiguration;
4142
import org.geowebcache.filter.parameters.ParametersUtils;
4243
import org.geowebcache.io.ByteArrayResource;
4344
import org.geowebcache.io.Resource;
@@ -52,6 +53,7 @@
5253
import org.geowebcache.storage.TileObject;
5354
import org.geowebcache.storage.TileRange;
5455
import org.geowebcache.storage.TileRangeIterator;
56+
import org.geowebcache.storage.UnsuitableStorageException;
5557
import org.geowebcache.util.TMSKeyBuilder;
5658

5759
/**
@@ -72,19 +74,35 @@ public class GoogleCloudStorageBlobStore implements BlobStore {
7274
/**
7375
* @param client a pre-configured {@link GoogleCloudStorageClient}
7476
* @param layers the tile layer dispatcher to build tile keys from
75-
* @throws org.geowebcache.storage.StorageException if the target bucket is not suitable for a cache (e.g. it's not
76-
* empty and does not contain a {@code metadata.properties} marker file.
77+
* @throws org.geowebcache.storage.UnsuitableStorageException if the target bucket is not suitable for a cache (e.g.
78+
* it's not empty and does not contain a {@code metadata.properties} marker file <strong>or</strong> the bucket
79+
* can't be accessed, for example due to bad credentials.
80+
* @implNote {@link UnsuitableStorageException} will be thrown also if the bucket can't be accessed to account fot
81+
* {@link XMLConfiguration#addBlobStore} and {@link XMLConfiguration#modifyBlobStore} checking for
82+
* {@code instanceof UnsuitableStorageException} to prevent saving a misconfigured blob store. Otherwise the
83+
* blobstore would be saved even with an invalid state and prevent application startup later on.
7784
*/
7885
public GoogleCloudStorageBlobStore(GoogleCloudStorageClient client, TileLayerDispatcher layers)
79-
throws org.geowebcache.storage.StorageException {
86+
throws org.geowebcache.storage.UnsuitableStorageException {
8087

8188
this.client = requireNonNull(client);
8289
this.layers = requireNonNull(layers);
8390

8491
String prefix = Optional.ofNullable(client.getPrefix()).orElse("");
8592
this.keyBuilder = new TMSKeyBuilder(prefix, layers);
8693

87-
ensureCacheSuitability(prefix);
94+
try {
95+
ensureCacheSuitability(prefix);
96+
} catch (UnsuitableStorageException e) {
97+
throw e;
98+
} catch (org.geowebcache.storage.StorageException somethingElse) {
99+
// throw UnsuitableStorageException instead, which is a subclass of StorageException
100+
// The GeoServer UI checks for instanceof UnsuitableStorageException when saving a blobstore that failed to
101+
// be created. Otherwise it'll save it with the invalid configuration.
102+
UnsuitableStorageException e = new UnsuitableStorageException(somethingElse.getMessage());
103+
e.addSuppressed(somethingElse);
104+
throw e;
105+
}
88106
}
89107

90108
void ensureCacheSuitability(String prefix) throws org.geowebcache.storage.StorageException {
@@ -213,10 +231,10 @@ public boolean deleteByParametersId(String layerName, String parametersId)
213231

214232
Set<String> gridsetAndFormatPrefixes = keyBuilder.forParameters(layerName, parametersId);
215233
// for each <prefix>/<layer>/<gridset>/<format>/<parametersId>/
216-
boolean prefixExists = gridsetAndFormatPrefixes.stream()
217-
.map(client::deleteDirectory)
218-
.reduce(Boolean::logicalOr)
219-
.orElse(false);
234+
boolean prefixExists = false;
235+
for (String prefix : gridsetAndFormatPrefixes) {
236+
prefixExists |= client.deleteDirectory(prefix);
237+
}
220238
if (prefixExists) {
221239
listeners.sendParametersDeleted(layerName, parametersId);
222240
}
@@ -444,18 +462,30 @@ private Properties getLayerMetadata(String layerName) {
444462
@Override
445463
public boolean layerExists(String layerName) {
446464
final String layerPrefix = keyBuilder.forLayer(layerName);
447-
return client.directoryExists(layerPrefix);
465+
try {
466+
return client.directoryExists(layerPrefix);
467+
} catch (org.geowebcache.storage.StorageException e) {
468+
throw new UncheckedIOException(e);
469+
}
448470
}
449471

472+
/**
473+
* {@inheritDoc}
474+
*
475+
* @throws UncheckedIOException if {@link GoogleCloudStorageClient#list(String)} throws an
476+
* {@link org.geowebcache.storage.StorageException}
477+
*/
450478
@Override
451479
public Map<String, Optional<Map<String, String>>> getParametersMapping(String layerName) {
452480
String parametersMetadataPrefix = keyBuilder.parametersMetadataPrefix(layerName);
453-
Stream<Blob> blobStream = client.list(parametersMetadataPrefix);
454-
455-
return blobStream
456-
.map(Blob::getName)
457-
.map(this::loadProperties)
458-
.collect(Collectors.toMap(ParametersUtils::getId, Optional::ofNullable));
481+
try (Stream<Blob> blobStream = client.list(parametersMetadataPrefix)) {
482+
return blobStream
483+
.map(Blob::getName)
484+
.map(this::loadProperties)
485+
.collect(Collectors.toMap(ParametersUtils::getId, Optional::ofNullable));
486+
} catch (org.geowebcache.storage.StorageException e) {
487+
throw new UncheckedIOException(e);
488+
}
459489
}
460490

461491
private Optional<Properties> findProperties(String key) {

geowebcache/gcsblob/src/main/java/org/geowebcache/storage/blobstore/gcs/GoogleCloudStorageBlobStoreInfo.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,15 @@ public class GoogleCloudStorageBlobStoreInfo extends BlobStoreInfo {
4444
private String prefix;
4545
private String endpointUrl; // Custom endpoint for emulators or non-standard GCS endpoints
4646
private String apiKey;
47-
private String useDefaultCredentialsChain;
47+
private boolean useDefaultCredentialsChain;
48+
49+
public GoogleCloudStorageBlobStoreInfo() {
50+
super();
51+
}
52+
53+
public GoogleCloudStorageBlobStoreInfo(String id) {
54+
super(id);
55+
}
4856

4957
@Override
5058
public GoogleCloudStorageBlobStore createInstance(TileLayerDispatcher layers, LockProvider lockProvider)
@@ -135,20 +143,20 @@ public void setApiKey(String apiKey) {
135143
}
136144

137145
/**
138-
* @return {@code "true"} if the default Google Cloud credentials chain should be used for authentication.
146+
* @return {@code true} if the default Google Cloud credentials chain should be used for authentication.
139147
* @see com.google.auth.oauth2.GoogleCredentials#getApplicationDefault()
140148
*/
141-
public String getUseDefaultCredentialsChain() {
149+
public boolean getUseDefaultCredentialsChain() {
142150
return useDefaultCredentialsChain;
143151
}
144152

145153
/**
146154
* Sets whether to use the default Google Cloud credentials chain.
147155
*
148-
* @param defaultCredentialsChain {@code "true"} to enable, {@code "false"} to disable.
156+
* @param useDefaultCredentialsChain {@code true} to enable, {@code false} to disable.
149157
*/
150-
public void setUseDefaultCredentialsChain(String defaultCredentialsChain) {
151-
this.useDefaultCredentialsChain = defaultCredentialsChain;
158+
public void setUseDefaultCredentialsChain(boolean useDefaultCredentialsChain) {
159+
this.useDefaultCredentialsChain = useDefaultCredentialsChain;
152160
}
153161

154162
@Override
@@ -162,6 +170,7 @@ public boolean equals(Object o) {
162170
GoogleCloudStorageBlobStoreInfo other = (GoogleCloudStorageBlobStoreInfo) o;
163171
return Objects.equals(projectId, other.projectId)
164172
&& Objects.equals(bucket, other.bucket)
173+
&& Objects.equals(prefix, other.prefix)
165174
&& Objects.equals(endpointUrl, other.endpointUrl)
166175
&& Objects.equals(apiKey, other.apiKey)
167176
&& Objects.equals(useDefaultCredentialsChain, other.useDefaultCredentialsChain)
@@ -173,6 +182,7 @@ public boolean equals(Object o) {
173182
@Override
174183
public int hashCode() {
175184
return 31 * super.hashCode()
176-
+ Objects.hash(projectId, bucket, endpointUrl, apiKey, useDefaultCredentialsChain, quotaProjectId);
185+
+ Objects.hash(
186+
projectId, bucket, prefix, endpointUrl, apiKey, useDefaultCredentialsChain, quotaProjectId);
177187
}
178188
}

geowebcache/gcsblob/src/main/java/org/geowebcache/storage/blobstore/gcs/GoogleCloudStorageClient.java

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,7 @@ public static GoogleCloudStorageClient.Builder builder(
126126
.resolveValueIfEnabled(config.getApiKey(), String.class)
127127
.orElse(null));
128128

129-
builder.useDefaultCredentialsChain(environment
130-
.resolveValueIfEnabled(config.getUseDefaultCredentialsChain(), Boolean.class)
131-
.orElse(false));
129+
builder.useDefaultCredentialsChain(config.getUseDefaultCredentialsChain());
132130

133131
return builder;
134132
}
@@ -187,37 +185,43 @@ public Builder useDefaultCredentialsChain(boolean useDefaultCredentialsChain) {
187185
}
188186

189187
public GoogleCloudStorageClient build() throws org.geowebcache.storage.StorageException {
190-
StorageOptions.Builder builder = StorageOptions.getDefaultInstance().toBuilder();
188+
try {
189+
StorageOptions.Builder builder = StorageOptions.getDefaultInstance().toBuilder();
191190

192-
if (projectId != null) {
193-
builder.setProjectId(projectId);
194-
}
195-
if (quotaProjectId != null) {
196-
builder.setQuotaProjectId(quotaProjectId);
197-
}
198-
if (endpointUrl != null) {
199-
// Set custom endpoint for emulators or non-standard GCS endpoints
200-
builder.setHost(endpointUrl);
201-
}
202-
Credentials credentials = null;
203-
if (apiKey != null) {
204-
credentials = ApiKeyCredentials.create(apiKey);
205-
} else if (useDefaultCredentialsChain) {
206-
try {
207-
credentials = GoogleCredentials.getApplicationDefault();
208-
} catch (IOException e) {
209-
throw new org.geowebcache.storage.StorageException("Error obtaining default credentials", e);
191+
if (projectId != null) {
192+
builder.setProjectId(projectId);
210193
}
211-
}
212-
if (credentials != null) {
213-
// credentials need to be set after projectId and quotaProjectId so its setter will
214-
// check whether projectId is null and get it from credentials if its a ServiceAccountCredentials
215-
// or quotaProjectId is null and get it from credentials if it's a QuotaProjectIdProvider
216-
builder.setCredentials(credentials);
217-
}
218-
Storage storageClient = builder.build().getService();
194+
if (quotaProjectId != null) {
195+
builder.setQuotaProjectId(quotaProjectId);
196+
}
197+
if (endpointUrl != null) {
198+
// Set custom endpoint for emulators or non-standard GCS endpoints
199+
builder.setHost(endpointUrl);
200+
}
201+
Credentials credentials = null;
202+
if (apiKey != null) {
203+
credentials = ApiKeyCredentials.create(apiKey);
204+
} else if (useDefaultCredentialsChain) {
205+
try {
206+
credentials = GoogleCredentials.getApplicationDefault();
207+
} catch (IOException e) {
208+
throw new org.geowebcache.storage.StorageException(
209+
"Error obtaining default credentials: " + e.getMessage(), e);
210+
}
211+
}
212+
if (credentials != null) {
213+
// credentials need to be set after projectId and quotaProjectId so its setter will
214+
// check whether projectId is null and get it from credentials if its a ServiceAccountCredentials
215+
// or quotaProjectId is null and get it from credentials if it's a QuotaProjectIdProvider
216+
builder.setCredentials(credentials);
217+
}
218+
Storage storageClient = builder.build().getService();
219219

220-
return new GoogleCloudStorageClient(storageClient, bucket, prefix);
220+
return new GoogleCloudStorageClient(storageClient, bucket, prefix);
221+
} catch (StorageException gcse) {
222+
throw new org.geowebcache.storage.StorageException(
223+
"Error creating GCS client: " + gcse.getMessage(), gcse);
224+
}
221225
}
222226
}
223227

@@ -265,9 +269,13 @@ public boolean blobExists(String key) throws org.geowebcache.storage.StorageExce
265269
* @param prefix The prefix to filter blobs by.
266270
* @return A stream of matching {@link Blob} objects.
267271
*/
268-
public Stream<Blob> list(String prefix) {
269-
return storage.list(bucket, BlobListOption.prefix(requireNonNull(prefix)))
270-
.streamAll();
272+
public Stream<Blob> list(String prefix) throws org.geowebcache.storage.StorageException {
273+
try {
274+
return storage.list(bucket, BlobListOption.prefix(requireNonNull(prefix)))
275+
.streamAll();
276+
} catch (StorageException gcse) {
277+
throw new org.geowebcache.storage.StorageException(gcse.getMessage(), gcse);
278+
}
271279
}
272280

273281
/**
@@ -279,10 +287,15 @@ public Stream<Blob> list(String prefix) {
279287
* @param path The directory path to check.
280288
* @return {@code true} if at least one blob exists with this path prefix, {@code false} otherwise.
281289
*/
282-
public boolean directoryExists(final String path) {
290+
public boolean directoryExists(final String path) throws org.geowebcache.storage.StorageException {
283291
requireNonNull(path);
284292
String dirPrefix = dirPrefix(path);
285-
Page<Blob> blobs = storage.list(bucket, BlobListOption.prefix(dirPrefix), BlobListOption.pageSize(1));
293+
Page<Blob> blobs;
294+
try {
295+
blobs = storage.list(bucket, BlobListOption.prefix(dirPrefix), BlobListOption.pageSize(1));
296+
} catch (StorageException gcse) {
297+
throw new org.geowebcache.storage.StorageException(gcse.getMessage(), gcse);
298+
}
286299
Iterator<Blob> iterator = blobs.getValues().iterator();
287300
boolean hasNext = iterator.hasNext();
288301
if (hasNext) {
@@ -307,7 +320,7 @@ public boolean directoryExists(final String path) {
307320
* @return {@code true} if the directory existed and the delete task was submitted, {@code false} if the directory
308321
* did not exist.
309322
*/
310-
public boolean deleteDirectory(String path) {
323+
public boolean deleteDirectory(String path) throws org.geowebcache.storage.StorageException {
311324
requireNonNull(path);
312325
if (directoryExists(path)) {
313326
String dirPrefix = dirPrefix(path);

geowebcache/gcsblob/src/test/java/org/geowebcache/storage/blobstore/gcs/GoogleCloudStorageConfigProviderTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ public void testValuesFromEnvironment() throws StorageException {
9494
assertEquals("${GCS_PREFIX}", config.getPrefix());
9595
assertEquals("${GCS_ENDPOINT}", config.getEndpointUrl());
9696
assertEquals("${GCS_APIKEY}", config.getApiKey());
97-
assertEquals("${GCS_DEFAULT_CREDENTIALS}", config.getUseDefaultCredentialsChain());
9897

9998
setProperty("GCS_PROJECT_ID", "my-project");
10099
setProperty("GCS_QUOTA_PROJECT_ID", "bills-go-here");
@@ -112,6 +111,5 @@ public void testValuesFromEnvironment() throws StorageException {
112111
assertEquals("gwc", builder.prefix);
113112
assertEquals("https://goog.compatible.storage/api", builder.endpointUrl);
114113
assertEquals("goog-fake-api-key", builder.apiKey);
115-
assertTrue(builder.useDefaultCredentialsChain);
116114
}
117115
}

0 commit comments

Comments
 (0)