
com.hazelcast.gcp.GcpClient Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2008-2024, Hazelcast, Inc. All Rights Reserved.
*
* Licensed 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 com.hazelcast.gcp;
import com.hazelcast.logging.ILogger;
import com.hazelcast.logging.Logger;
import com.hazelcast.spi.exception.RestClientException;
import com.hazelcast.spi.utils.RetryUtils;
import java.util.ArrayList;
import java.util.List;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
/**
* Responsible for fetching the discovery information from GCP APIs.
*/
class GcpClient {
private static final ILogger LOGGER = Logger.getLogger(GcpDiscoveryStrategy.class);
private static final int RETRIES = 3;
private static final List NON_RETRYABLE_KEYWORDS = asList("Private key json file not found",
"Request had insufficient authentication scopes", "Required 'compute.instances.list' permission",
"Service account not enabled on this instance");
private boolean isKnownExceptionAlreadyLogged;
private final GcpMetadataApi gcpMetadataApi;
private final GcpComputeApi gcpComputeApi;
private final GcpAuthenticator gcpAuthenticator;
private final String privateKeyPath;
private final List projects;
private final List zones;
private final Label label;
GcpClient(GcpMetadataApi gcpMetadataApi, GcpComputeApi gcpComputeApi, GcpAuthenticator gcpAuthenticator,
GcpConfig gcpConfig) {
this.gcpMetadataApi = gcpMetadataApi;
this.gcpComputeApi = gcpComputeApi;
this.gcpAuthenticator = gcpAuthenticator;
this.privateKeyPath = gcpConfig.getPrivateKeyPath();
this.projects = projectFromConfigOrMetadataApi(gcpConfig);
this.zones = zonesFromConfigOrComputeApi(gcpConfig);
this.label = gcpConfig.getLabel();
}
private List projectFromConfigOrMetadataApi(final GcpConfig gcpConfig) {
if (!gcpConfig.getProjects().isEmpty()) {
return gcpConfig.getProjects();
}
LOGGER.finest("Property 'projects' not configured, fetching the current GCP project");
return singletonList(RetryUtils.retry(gcpMetadataApi::currentProject, RETRIES, NON_RETRYABLE_KEYWORDS));
}
private List zonesFromConfigOrComputeApi(final GcpConfig gcpConfig) {
try {
if (gcpConfig.getRegion() != null) {
LOGGER.finest("Property 'region' configured, fetching GCP zones of the specified GCP region");
return RetryUtils.retry(() -> fetchZones(gcpConfig.getRegion()), RETRIES, NON_RETRYABLE_KEYWORDS);
}
if (!gcpConfig.getZones().isEmpty()) {
return gcpConfig.getZones();
}
LOGGER.finest("Property 'zones' not configured, fetching GCP zones of the current GCP region");
return RetryUtils.retry(() -> {
String region = gcpMetadataApi.currentRegion();
return fetchZones(region);
}, RETRIES, NON_RETRYABLE_KEYWORDS);
} catch (RestClientException e) {
handleKnownException(e);
return emptyList();
}
}
List getAddresses() {
try {
return RetryUtils.retry(this::fetchGcpAddresses, RETRIES, NON_RETRYABLE_KEYWORDS);
} catch (RestClientException e) {
handleKnownException(e);
return emptyList();
}
}
private List fetchZones(String region) {
List zones = new ArrayList<>();
String accessToken = fetchAccessToken();
for (String project : projects) {
zones.addAll(gcpComputeApi.zones(project, region, accessToken));
}
return zones;
}
private List fetchGcpAddresses() {
LOGGER.finest("Fetching OAuth Access Token");
final String accessToken = fetchAccessToken();
List result = new ArrayList<>();
for (final String project : projects) {
for (final String zone : zones) {
LOGGER.finest(String.format("Fetching instances for project '%s' and zone '%s'", project, zone));
List addresses = gcpComputeApi.instances(project, zone, label, accessToken);
LOGGER.finest(String.format("Found the following instances for project '%s' and zone '%s': %s", project, zone,
addresses));
result.addAll(addresses);
}
}
return result;
}
private String fetchAccessToken() {
if (privateKeyPath != null) {
return gcpAuthenticator.refreshAccessToken(privateKeyPath);
}
return gcpMetadataApi.accessToken();
}
String getAvailabilityZone() {
return gcpMetadataApi.currentZone();
}
private void handleKnownException(RestClientException e) {
if (e.getHttpErrorCode() == HTTP_UNAUTHORIZED) {
if (!isKnownExceptionAlreadyLogged) {
LOGGER.warning("Google Cloud API Authorization failed! Check your credentials. Starting standalone.");
isKnownExceptionAlreadyLogged = true;
}
} else if (e.getHttpErrorCode() == HTTP_FORBIDDEN) {
if (!isKnownExceptionAlreadyLogged) {
LOGGER.warning("Google Cloud API access is forbidden! Starting standalone. To use Hazelcast GCP discovery, "
+ "make sure that your service account has at minimum \"Read Only\" Access Scope to Compute Engine API.");
isKnownExceptionAlreadyLogged = true;
}
} else if (e.getHttpErrorCode() == HTTP_NOT_FOUND) {
if (!isKnownExceptionAlreadyLogged) {
LOGGER.warning("Google Cloud API Not Found! Starting standalone. Please check that you have a service account "
+ "assigned to your VM instance or `private-key-path` property defined.");
isKnownExceptionAlreadyLogged = true;
}
} else {
throw e;
}
LOGGER.finest(e);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy