Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gcp): provide a configurable option to bypass gcp account health check. (backport #6093) #6096

Merged
merged 2 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,25 @@ package com.netflix.spinnaker.clouddriver.google.config

import com.netflix.spinnaker.clouddriver.consul.config.ConsulConfig
import com.netflix.spinnaker.clouddriver.googlecommon.config.GoogleCommonManagedAccount
import groovy.transform.Canonical
import groovy.transform.ToString
import org.springframework.boot.context.properties.NestedConfigurationProperty

class GoogleConfigurationProperties {
public static final int ASYNC_OPERATION_TIMEOUT_SECONDS_DEFAULT = 300
public static final int ASYNC_OPERATION_MAX_POLLING_INTERVAL_SECONDS = 8

/**
* health check related config settings
*/
@Canonical
static class HealthConfig {
/**
* flag to toggle verifying account health check. by default, account health check is enabled.
*/
boolean verifyAccountHealth = true
}

@ToString(includeNames = true)
static class ManagedAccount extends GoogleCommonManagedAccount {
boolean alphaListed
Expand All @@ -45,4 +58,7 @@ class GoogleConfigurationProperties {
// Takes a list of regions you want indexed. Will default to indexing all regions if left
// unspecified. An empty list will index no regions.
List<String> defaultRegions

@NestedConfigurationProperty
final HealthConfig health = new HealthConfig()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.netflix.spinnaker.config


import com.netflix.spinnaker.clouddriver.google.config.GoogleConfigurationProperties
import com.netflix.spinnaker.clouddriver.google.config.GoogleCredentialsConfiguration

Expand Down Expand Up @@ -49,6 +48,7 @@ class GoogleConfiguration {
}

@Bean
@ConditionalOnProperty("google.health.verifyAccountHealth")
GoogleHealthIndicator googleHealthIndicator() {
new GoogleHealthIndicator()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2023 Armory, Inc.
*
* 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.netflix.spinnaker.clouddriver.google.health


import com.google.api.services.compute.model.Project
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.netflix.spectator.api.NoopRegistry
import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.clouddriver.google.provider.agent.StubComputeFactory
import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials
import com.netflix.spinnaker.credentials.CredentialsRepository
import com.netflix.spinnaker.credentials.CredentialsTypeBaseConfiguration
import org.springframework.boot.actuate.health.Status
import org.springframework.context.ApplicationContext
import spock.lang.Specification
import spock.lang.Unroll

class GoogleHealthIndicatorSpec extends Specification {

private static final String ACCOUNT_NAME = "partypups"
private static final String PROJECT = "myproject"
private static final String REGION = "myregion"
private static final String ZONE = REGION + "-myzone"
private static final Registry REGISTRY = new NoopRegistry()

@Unroll
def "health succeeds when google is reachable"() {
setup:
def applicationContext = Mock(ApplicationContext)
def project = new Project()
project.setName(PROJECT)

def compute = new StubComputeFactory()
.setProjects(project)
.create()

def googleNamedAccountCredentials =
new GoogleNamedAccountCredentials.Builder()
.project(PROJECT)
.name(ACCOUNT_NAME)
.compute(compute)
.regionToZonesMap(ImmutableMap.of(REGION, ImmutableList.of(ZONE)))
.build()

def credentials = [googleNamedAccountCredentials]
def credentialsRepository = Stub(CredentialsRepository) {
getAll() >> credentials
}

def credentialsTypeBaseConfiguration = new CredentialsTypeBaseConfiguration(applicationContext, null)
credentialsTypeBaseConfiguration.credentialsRepository = credentialsRepository

def indicator = new GoogleHealthIndicator()
indicator.registry = REGISTRY
indicator.credentialsTypeBaseConfiguration = credentialsTypeBaseConfiguration

when:
indicator.checkHealth()
def health = indicator.health()

then:
health.status == Status.UP
health.details.isEmpty()
}

@Unroll
def "health throws exception when google appears unreachable"() {
setup:
def applicationContext = Mock(ApplicationContext)
def project = new Project()
project.setName(PROJECT)

def compute = new StubComputeFactory()
.setProjects(project)
.setProjectException(new IOException("Read timed out"))
.create()

def googleNamedAccountCredentials =
new GoogleNamedAccountCredentials.Builder()
.project(PROJECT)
.name(ACCOUNT_NAME)
.compute(compute)
.regionToZonesMap(ImmutableMap.of(REGION, ImmutableList.of(ZONE)))
.build()

def credentials = [googleNamedAccountCredentials]
def credentialsRepository = Stub(CredentialsRepository) {
getAll() >> credentials
}

def credentialsTypeBaseConfiguration = new CredentialsTypeBaseConfiguration(applicationContext, null)
credentialsTypeBaseConfiguration.credentialsRepository = credentialsRepository

def indicator = new GoogleHealthIndicator()
indicator.registry = REGISTRY
indicator.credentialsTypeBaseConfiguration = credentialsTypeBaseConfiguration

when:
indicator.checkHealth()
def health = indicator.health()

then:
thrown(GoogleHealthIndicator.GoogleIOException)

health == null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,7 @@
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.services.compute.Compute;
import com.google.api.services.compute.model.Autoscaler;
import com.google.api.services.compute.model.AutoscalerAggregatedList;
import com.google.api.services.compute.model.AutoscalerList;
import com.google.api.services.compute.model.AutoscalersScopedList;
import com.google.api.services.compute.model.Instance;
import com.google.api.services.compute.model.InstanceAggregatedList;
import com.google.api.services.compute.model.InstanceGroupManager;
import com.google.api.services.compute.model.InstanceGroupManagerList;
import com.google.api.services.compute.model.InstanceList;
import com.google.api.services.compute.model.InstanceTemplate;
import com.google.api.services.compute.model.InstanceTemplateList;
import com.google.api.services.compute.model.InstancesScopedList;
import com.google.api.services.compute.model.RegionInstanceGroupManagerList;
import com.google.api.services.compute.model.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -72,8 +60,10 @@ final class StubComputeFactory {

private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

private static final String COMPUTE_PATH_PREFIX = "/compute/[-.a-zA-Z0-9]+";

private static final String COMPUTE_PROJECT_PATH_PREFIX =
"/compute/[-.a-zA-Z0-9]+/projects/[-.a-zA-Z0-9]+";
COMPUTE_PATH_PREFIX + "/projects/[-.a-zA-Z0-9]+";

private static final Pattern BATCH_COMPUTE_PATTERN =
Pattern.compile("/batch/compute/[-.a-zA-Z0-9]+");
Expand Down Expand Up @@ -114,11 +104,15 @@ final class StubComputeFactory {
Pattern.compile(COMPUTE_PROJECT_PATH_PREFIX + "/regions/([-a-z0-9]+)/autoscalers");
private static final Pattern AGGREGATED_AUTOSCALERS_PATTERN =
Pattern.compile(COMPUTE_PROJECT_PATH_PREFIX + "/aggregated/autoscalers");
private static final Pattern GET_PROJECT_PATTERN =
Pattern.compile(COMPUTE_PATH_PREFIX + "/projects/([-.a-zA-Z0-9]+)");

private List<InstanceGroupManager> instanceGroupManagers = new ArrayList<>();
private List<InstanceTemplate> instanceTemplates = new ArrayList<>();
private List<Instance> instances = new ArrayList<>();
private List<Autoscaler> autoscalers = new ArrayList<>();
private List<Project> projects = new ArrayList<>();
private Exception projectException;

StubComputeFactory setInstanceGroupManagers(InstanceGroupManager... instanceGroupManagers) {
this.instanceGroupManagers = ImmutableList.copyOf(instanceGroupManagers);
Expand All @@ -140,6 +134,16 @@ StubComputeFactory setAutoscalers(Autoscaler... autoscalers) {
return this;
}

StubComputeFactory setProjects(Project... projects) {
this.projects = ImmutableList.copyOf(projects);
return this;
}

StubComputeFactory setProjectException(Exception projectException) {
this.projectException = projectException;
return this;
}

Compute create() {
HttpTransport httpTransport =
new StubHttpTransport()
Expand All @@ -166,7 +170,8 @@ Compute create() {
.addGetResponse(
LIST_REGIONAL_AUTOSCALERS_PATTERN,
new PathBasedJsonResponseGenerator(this::regionalAutoscalerList))
.addGetResponse(AGGREGATED_AUTOSCALERS_PATTERN, this::autoscalerAggregatedList);
.addGetResponse(AGGREGATED_AUTOSCALERS_PATTERN, this::autoscalerAggregatedList)
.addGetResponse(GET_PROJECT_PATTERN, this::project);
return new Compute(
httpTransport, JacksonFactory.getDefaultInstance(), /* httpRequestInitializer= */ null);
}
Expand Down Expand Up @@ -322,6 +327,21 @@ private MockLowLevelHttpResponse autoscalerAggregatedList(LowLevelHttpRequest re
return jsonResponse(new AutoscalerAggregatedList().setItems(autoscalers));
}

private MockLowLevelHttpResponse project(MockLowLevelHttpRequest request) {
if (projectException != null) {
return errorResponse(500, projectException);
}

Matcher matcher = GET_PROJECT_PATTERN.matcher(getPath(request));
checkState(matcher.matches());
String name = matcher.group(1);
return projects.stream()
.filter(project -> name.equals(project.getName()))
.findFirst()
.map(StubComputeFactory::jsonResponse)
.orElse(errorResponse(404));
}

private static <T> ImmutableListMultimap<String, T> aggregate(
Collection<T> items, Function<T, String> zoneFunction, Function<T, String> regionFunction) {
return items.stream()
Expand All @@ -345,9 +365,18 @@ private static <T> String getAggregateKey(
}

private static MockLowLevelHttpResponse errorResponse(int statusCode) {
return errorResponse(statusCode, null);
}

private static MockLowLevelHttpResponse errorResponse(int statusCode, Exception exception) {
GoogleJsonErrorContainer errorContainer = new GoogleJsonErrorContainer();
GoogleJsonError error = new GoogleJsonError();
error.setCode(statusCode);

if (exception != null) {
error.setMessage(exception.getMessage());
}

errorContainer.setError(error);
return jsonResponse(statusCode, errorContainer);
}
Expand Down