diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/AbstractGoogleServerGroupManagers.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/AbstractGoogleServerGroupManagers.java index 0e6d7ea5bc2..2fe5f76ea94 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/AbstractGoogleServerGroupManagers.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/AbstractGoogleServerGroupManagers.java @@ -46,7 +46,8 @@ abstract class AbstractGoogleServerGroupManagers implements GoogleServerGroupMan } @Override - public GoogleComputeOperationRequest abandonInstances(List instances) throws IOException { + public GoogleComputeOperationRequest> abandonInstances( + List instances) throws IOException { return wrapOperationRequest(performAbandonInstances(instances), "abandonInstances"); } @@ -54,36 +55,39 @@ abstract ComputeRequest performAbandonInstances(List instance throws IOException; @Override - public GoogleComputeOperationRequest delete() throws IOException { + public GoogleComputeOperationRequest> delete() throws IOException { return wrapOperationRequest(performDelete(), "delete"); } abstract ComputeRequest performDelete() throws IOException; @Override - public GoogleComputeRequest get() throws IOException { + public GoogleComputeRequest, InstanceGroupManager> get() + throws IOException { return wrapRequest(performGet(), "get"); } abstract ComputeRequest performGet() throws IOException; @Override - public GoogleComputeOperationRequest update(InstanceGroupManager content) throws IOException { + public GoogleComputeOperationRequest> update( + InstanceGroupManager content) throws IOException { return wrapOperationRequest(performUpdate(content), "update"); } abstract ComputeRequest performUpdate(InstanceGroupManager content) throws IOException; - private GoogleComputeRequest wrapRequest(ComputeRequest request, String api) { + private , ResponseT> + GoogleComputeRequest wrapRequest(RequestT request, String api) { return new GoogleComputeRequestImpl<>( request, registry, getMetricName(api), getRegionOrZoneTags()); } - private GoogleComputeOperationRequest wrapOperationRequest( + private GoogleComputeOperationRequest> wrapOperationRequest( ComputeRequest request, String api) { OperationWaiter waiter = getOperationWaiter(credentials, poller); - return new GoogleComputeOperationRequestImpl( + return new GoogleComputeOperationRequestImpl<>( request, registry, getMetricName(api), getRegionOrZoneTags(), waiter); } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeBatchRequest.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeBatchRequest.java new file mode 100644 index 00000000000..4c71dd3779c --- /dev/null +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeBatchRequest.java @@ -0,0 +1,215 @@ +/* + * Copyright 2019 Google, 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.compute; + +import static com.google.common.collect.Lists.partition; + +import com.google.api.client.googleapis.batch.BatchRequest; +import com.google.api.client.googleapis.batch.json.JsonBatchCallback; +import com.google.api.client.util.Throwables; +import com.google.api.services.compute.Compute; +import com.google.api.services.compute.ComputeRequest; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.AbstractFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.netflix.spectator.api.Registry; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.UncheckedIOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.apache.http.client.HttpResponseException; + +public class ComputeBatchRequest, ResponseT> { + + // Platform-specified max to not overwhelm batch backends. + @VisibleForTesting static final int MAX_BATCH_SIZE = 100; + private static final Duration CONNECT_TIMEOUT = Duration.ofMinutes(2); + private static final Duration READ_TIMEOUT = Duration.ofMinutes(2); + + private final Compute compute; + private final Registry registry; + private final String userAgent; + private final ListeningExecutorService executor; + private final List> queuedRequests; + + ComputeBatchRequest( + Compute compute, Registry registry, String userAgent, ListeningExecutorService executor) { + this.compute = compute; + this.registry = registry; + this.userAgent = userAgent; + this.executor = executor; + this.queuedRequests = new ArrayList<>(); + } + + public void queue( + GoogleComputeRequest request, JsonBatchCallback callback) { + queuedRequests.add(new QueuedRequest<>(request.getRequest(), callback)); + } + + public void execute(String batchContext) throws IOException { + if (queuedRequests.size() == 0) { + return; + } + + List>> requestPartitions = + partition(queuedRequests, MAX_BATCH_SIZE); + List queuedBatches = createBatchRequests(requestPartitions); + + String statusCode = "500"; + String success = "false"; + long start = registry.clock().monotonicTime(); + try { + executeBatches(queuedBatches); + success = "true"; + statusCode = "200"; + } catch (HttpResponseException e) { + statusCode = Integer.toString(e.getStatusCode()); + throw e; + } finally { + long nanos = registry.clock().monotonicTime() - start; + String status = statusCode.charAt(0) + "xx"; + Map tags = + ImmutableMap.of( + "context", batchContext, + "success", success, + "status", status, + "statusCode", statusCode); + registry + .timer(registry.createId("google.batchExecute", tags)) + .record(Duration.ofNanos(nanos)); + registry + .counter(registry.createId("google.batchSize", tags)) + .increment(queuedRequests.size()); + } + } + + private void executeBatches(List queuedBatches) throws IOException { + if (queuedBatches.size() == 1) { + queuedBatches.get(0).execute(); + return; + } + + List> futures = new ArrayList<>(); + for (BatchRequest batchRequest : queuedBatches) { + ListenableFuture submit = + executor.submit( + () -> { + batchRequest.execute(); + return null; + }); + futures.add(submit); + } + + try { + new FailFastFuture(futures, executor).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + Throwables.propagateIfPossible(cause, IOException.class); + throw new RuntimeException(cause); + } + } + + private List createBatchRequests( + List>> requestPartitions) throws IOException { + + List queuedBatches = new ArrayList<>(); + + try { + requestPartitions.forEach( + partition -> { + BatchRequest batch = newBatch(); + partition.forEach( + qr -> wrapIOException(() -> qr.getRequest().queue(batch, qr.getCallback()))); + queuedBatches.add(batch); + }); + return queuedBatches; + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + private BatchRequest newBatch() { + return compute.batch( + request -> { + request.getHeaders().setUserAgent(userAgent); + request.setConnectTimeout((int) CONNECT_TIMEOUT.toMillis()); + request.setReadTimeout((int) READ_TIMEOUT.toMillis()); + }); + } + + @FunctionalInterface + private interface IoExceptionRunnable { + void run() throws IOException; + } + + private static void wrapIOException(IoExceptionRunnable runnable) { + try { + runnable.run(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Value + @AllArgsConstructor + private static class QueuedRequest, ResponseT> { + private RequestT request; + private JsonBatchCallback callback; + } + + private static class FailFastFuture extends AbstractFuture { + + private final AtomicInteger remainingFutures; + + FailFastFuture(List> futures, ExecutorService executor) { + remainingFutures = new AtomicInteger(futures.size()); + for (ListenableFuture future : futures) { + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Object result) { + if (remainingFutures.decrementAndGet() == 0) { + set(null); + } + } + + @Override + public void onFailure(Throwable t) { + setException(t); + } + }, + executor); + } + } + } +} diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeConfiguration.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeConfiguration.java new file mode 100644 index 00000000000..2f9059c4105 --- /dev/null +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Google, 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.compute; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.Executors; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ComputeConfiguration { + + public static final String BATCH_REQUEST_EXECUTOR = "batchRequestExecutor"; + + @Bean + @Qualifier(BATCH_REQUEST_EXECUTOR) + public ListeningExecutorService batchRequestExecutor() { + return MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + } +} diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeApiFactory.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeApiFactory.java index 232e5d84281..7fa911ab162 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeApiFactory.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeApiFactory.java @@ -16,11 +16,14 @@ package com.netflix.spinnaker.clouddriver.google.compute; +import com.google.api.services.compute.ComputeRequest; +import com.google.common.util.concurrent.ListeningExecutorService; import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.clouddriver.google.deploy.GoogleOperationPoller; import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup; import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Service @@ -28,11 +31,20 @@ public class GoogleComputeApiFactory { private final GoogleOperationPoller operationPoller; private final Registry registry; + private String clouddriverUserAgentApplicationName; + private ListeningExecutorService batchExecutor; @Autowired - public GoogleComputeApiFactory(GoogleOperationPoller operationPoller, Registry registry) { + public GoogleComputeApiFactory( + GoogleOperationPoller operationPoller, + Registry registry, + String clouddriverUserAgentApplicationName, + @Qualifier(ComputeConfiguration.BATCH_REQUEST_EXECUTOR) + ListeningExecutorService batchExecutor) { this.operationPoller = operationPoller; this.registry = registry; + this.clouddriverUserAgentApplicationName = clouddriverUserAgentApplicationName; + this.batchExecutor = batchExecutor; } public GoogleServerGroupManagers createServerGroupManagers( @@ -44,7 +56,18 @@ public GoogleServerGroupManagers createServerGroupManagers( credentials, operationPoller, registry, serverGroup.getName(), serverGroup.getZone()); } + public Images createImages(GoogleNamedAccountCredentials credentials) { + return new Images(credentials, registry); + } + public InstanceTemplates createInstanceTemplates(GoogleNamedAccountCredentials credentials) { return new InstanceTemplates(credentials, operationPoller, registry); } + + public , ResponseT> + ComputeBatchRequest createBatchRequest( + GoogleNamedAccountCredentials credentials) { + return new ComputeBatchRequest<>( + credentials.getCompute(), registry, clouddriverUserAgentApplicationName, batchExecutor); + } } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequest.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequest.java index e0036db2fe1..5118aa8ff74 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequest.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequest.java @@ -16,11 +16,13 @@ package com.netflix.spinnaker.clouddriver.google.compute; +import com.google.api.services.compute.ComputeRequest; import com.google.api.services.compute.model.Operation; import com.netflix.spinnaker.clouddriver.data.task.Task; import java.io.IOException; -public interface GoogleComputeOperationRequest extends GoogleComputeRequest { +public interface GoogleComputeOperationRequest> + extends GoogleComputeRequest { Operation executeAndWait(Task task, String phase) throws IOException; } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequestImpl.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequestImpl.java index 448e1dc8b41..a7f67044967 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequestImpl.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeOperationRequestImpl.java @@ -23,8 +23,9 @@ import java.io.IOException; import java.util.Map; -final class GoogleComputeOperationRequestImpl extends GoogleComputeRequestImpl - implements GoogleComputeOperationRequest { +final class GoogleComputeOperationRequestImpl> + extends GoogleComputeRequestImpl + implements GoogleComputeOperationRequest { @FunctionalInterface interface OperationWaiter { @@ -34,7 +35,7 @@ interface OperationWaiter { private final OperationWaiter operationWaiter; GoogleComputeOperationRequestImpl( - ComputeRequest request, + RequestT request, Registry registry, String metricName, Map tags, diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequest.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequest.java index 31089389b25..a1539eebdea 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequest.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequest.java @@ -16,9 +16,12 @@ package com.netflix.spinnaker.clouddriver.google.compute; +import com.google.api.services.compute.ComputeRequest; import java.io.IOException; -public interface GoogleComputeRequest { +public interface GoogleComputeRequest, ResponseT> { - T execute() throws IOException; + ResponseT execute() throws IOException; + + RequestT getRequest(); } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequestImpl.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequestImpl.java index 0e579f0fff5..df36eb543e5 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequestImpl.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleComputeRequestImpl.java @@ -18,7 +18,6 @@ import static java.util.stream.Collectors.toList; -import com.google.api.client.googleapis.services.AbstractGoogleClientRequest; import com.google.api.services.compute.ComputeRequest; import com.google.common.collect.ImmutableList; import com.netflix.spectator.api.Registry; @@ -29,15 +28,16 @@ import java.util.Map; import java.util.stream.Stream; -class GoogleComputeRequestImpl implements GoogleComputeRequest { +class GoogleComputeRequestImpl, ResponseT> + implements GoogleComputeRequest { - private final ComputeRequest request; + private final RequestT request; private final Registry registry; private final String metricName; private final Map tags; GoogleComputeRequestImpl( - ComputeRequest request, Registry registry, String metricName, Map tags) { + RequestT request, Registry registry, String metricName, Map tags) { this.request = request; this.registry = registry; this.metricName = metricName; @@ -45,16 +45,16 @@ class GoogleComputeRequestImpl implements GoogleComputeRequest { } @Override - public T execute() throws IOException { + public ResponseT execute() throws IOException { return timeExecute(request); } - private T timeExecute(AbstractGoogleClientRequest request) throws IOException { + private ResponseT timeExecute(RequestT request) throws IOException { return GoogleExecutor.timeExecute( registry, request, "google.api", metricName, getTimeExecuteTags(request)); } - private String[] getTimeExecuteTags(AbstractGoogleClientRequest request) { + private String[] getTimeExecuteTags(RequestT request) { String account = AccountForClient.getAccount(request.getAbstractGoogleClient()); return ImmutableList.builder() .add("account") @@ -69,4 +69,9 @@ private List flattenTags() { .flatMap(e -> Stream.of(e.getKey(), e.getValue())) .collect(toList()); } + + @Override + public RequestT getRequest() { + return request; + } } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleServerGroupManagers.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleServerGroupManagers.java index 460ccd7d625..db465b4e9d6 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleServerGroupManagers.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/GoogleServerGroupManagers.java @@ -18,7 +18,9 @@ import com.google.api.services.compute.Compute.InstanceGroupManagers; import com.google.api.services.compute.Compute.RegionInstanceGroupManagers; +import com.google.api.services.compute.ComputeRequest; import com.google.api.services.compute.model.InstanceGroupManager; +import com.google.api.services.compute.model.Operation; import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup; import java.io.IOException; import java.util.List; @@ -29,11 +31,14 @@ */ public interface GoogleServerGroupManagers { - GoogleComputeOperationRequest abandonInstances(List instances) throws IOException; + GoogleComputeOperationRequest> abandonInstances(List instances) + throws IOException; - GoogleComputeOperationRequest delete() throws IOException; + GoogleComputeOperationRequest> delete() throws IOException; - GoogleComputeRequest get() throws IOException; + GoogleComputeRequest, InstanceGroupManager> get() + throws IOException; - GoogleComputeOperationRequest update(InstanceGroupManager content) throws IOException; + GoogleComputeOperationRequest> update(InstanceGroupManager content) + throws IOException; } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/Images.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/Images.java new file mode 100644 index 00000000000..7ea6526bddd --- /dev/null +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/Images.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google, 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.compute; + +import com.google.api.services.compute.Compute; +import com.google.api.services.compute.ComputeRequest; +import com.google.api.services.compute.model.ImageList; +import com.google.common.collect.ImmutableMap; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.clouddriver.google.GoogleExecutor; +import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials; +import java.io.IOException; + +public class Images { + + public static final ImmutableMap TAGS = + ImmutableMap.of(GoogleExecutor.getTAG_SCOPE(), GoogleExecutor.getSCOPE_GLOBAL()); + + private final GoogleNamedAccountCredentials credentials; + private final Registry registry; + + public Images(GoogleNamedAccountCredentials credentials, Registry registry) { + this.credentials = credentials; + this.registry = registry; + } + + public GoogleComputeRequest list(String project) + throws IOException { + + Compute.Images.List request = credentials.getCompute().images().list(project); + return wrapRequest(request, "list"); + } + + private , ResponseT> + GoogleComputeRequest wrapRequest(RequestT request, String api) { + return new GoogleComputeRequestImpl<>(request, registry, getMetricName(api), TAGS); + } + + private String getMetricName(String api) { + return "compute.images." + api; + } +} diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/InstanceTemplates.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/InstanceTemplates.java index 9b77db98f9c..702152cf1f3 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/InstanceTemplates.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/compute/InstanceTemplates.java @@ -16,6 +16,7 @@ package com.netflix.spinnaker.clouddriver.google.compute; +import com.google.api.services.compute.Compute; import com.google.api.services.compute.ComputeRequest; import com.google.api.services.compute.model.InstanceTemplate; import com.google.api.services.compute.model.Operation; @@ -46,31 +47,36 @@ public class InstanceTemplates { this.registry = registry; } - public GoogleComputeOperationRequest delete(String name) throws IOException { + public GoogleComputeOperationRequest delete(String name) + throws IOException { - ComputeRequest request = + Compute.InstanceTemplates.Delete request = credentials.getCompute().instanceTemplates().delete(credentials.getProject(), name); return wrapOperationRequest(request, "delete"); } - public GoogleComputeRequest get(String name) throws IOException { - ComputeRequest request = + public GoogleComputeRequest get(String name) + throws IOException { + Compute.InstanceTemplates.Get request = credentials.getCompute().instanceTemplates().get(credentials.getProject(), name); return wrapRequest(request, "get"); } - public GoogleComputeOperationRequest insert(InstanceTemplate template) throws IOException { - ComputeRequest request = + public GoogleComputeOperationRequest insert( + InstanceTemplate template) throws IOException { + Compute.InstanceTemplates.Insert request = credentials.getCompute().instanceTemplates().insert(credentials.getProject(), template); return wrapOperationRequest(request, "insert"); } - private GoogleComputeRequest wrapRequest(ComputeRequest request, String api) { - return new GoogleComputeRequestImpl<>(request, registry, getMetricName(api), TAGS); + private , ResponseT> + GoogleComputeRequest wrapRequest(RequestT request, String api) { + return new GoogleComputeRequestImpl( + request, registry, getMetricName(api), TAGS); } - private GoogleComputeOperationRequest wrapOperationRequest( - ComputeRequest request, String api) { + private > + GoogleComputeOperationRequest wrapOperationRequest(T request, String api) { OperationWaiter waiter = (operation, task, phase) -> operationPoller.waitForGlobalOperation( @@ -81,7 +87,7 @@ private GoogleComputeOperationRequest wrapOperationRequest( task, GCEUtil.getLocalName(operation.getTargetLink()), phase); - return new GoogleComputeOperationRequestImpl( + return new GoogleComputeOperationRequestImpl( request, registry, getMetricName(api), TAGS, waiter); } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverter.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverter.java index 23f3dfef89d..c57ae2f01c4 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverter.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google, 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.deploy.converters; import com.netflix.spinnaker.clouddriver.google.GoogleOperation; diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/SetStatefulDiskDescription.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/SetStatefulDiskDescription.java index ec67c9ee555..4867c3b339d 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/SetStatefulDiskDescription.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/SetStatefulDiskDescription.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google, 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.deploy.description; import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials; diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperation.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperation.java index 62a184393f4..db05aaca46f 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperation.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperation.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google, 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.deploy.ops; import com.google.api.services.compute.model.InstanceGroupManager; diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidator.java b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidator.java index 77a76108e29..4b07fe03bfc 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidator.java +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidator.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google, 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.deploy.validators; import com.netflix.spinnaker.clouddriver.deploy.DescriptionValidator; diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeBatchRequestTest.java b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeBatchRequestTest.java new file mode 100644 index 00000000000..face1741f79 --- /dev/null +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/ComputeBatchRequestTest.java @@ -0,0 +1,438 @@ +/* + * Copyright 2019 Google, 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.compute; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.catchThrowable; + +import com.google.api.client.googleapis.batch.json.JsonBatchCallback; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.services.compute.Compute; +import com.google.api.services.compute.model.Image; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import com.netflix.spectator.api.BasicTag; +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.DefaultRegistry; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.api.Tag; +import com.netflix.spectator.api.Timer; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.http.client.HttpResponseException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ComputeBatchRequestTest { + + private static final String USER_AGENT = "spinnaker-test"; + private static final String MIME_BOUNDARY = "batch_foobarbaz"; + private static final String MIME_PART_START = "--batch_foobarbaz\n"; + private static final String MIME_END = "--batch_foobarbaz--\n"; + private static final String BATCH_CONTENT_TYPE = "multipart/mixed; boundary=" + MIME_BOUNDARY; + + private Registry registry; + + @Before + public void setUp() { + registry = new DefaultRegistry(); + } + + @Test + public void exitsEarlyWithNoRequests() throws IOException { + + Compute compute = computeWithResponses(); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + batchRequest.execute("batchContext"); + } + + @Test + public void singleRequest() throws IOException { + + Compute compute = computeWithResponses(() -> successBatchResponse(1)); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + batchRequest.queue(request(compute), responses); + + batchRequest.execute("batchContext"); + + assertThat(responses.successes).hasValue(1); + assertThat(responses.failures).hasValue(0); + } + + @Test + public void singleBatch() throws IOException { + + Compute compute = + computeWithResponses(() -> successBatchResponse(ComputeBatchRequest.MAX_BATCH_SIZE)); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < ComputeBatchRequest.MAX_BATCH_SIZE; ++i) { + batchRequest.queue(request(compute), responses); + } + + batchRequest.execute("batchContext"); + + assertThat(responses.successes).hasValue(ComputeBatchRequest.MAX_BATCH_SIZE); + assertThat(responses.failures).hasValue(0); + } + + @Test + public void multipleBatches() throws IOException { + + Compute compute = + computeWithResponses( + () -> successBatchResponse(ComputeBatchRequest.MAX_BATCH_SIZE), + () -> successBatchResponse(ComputeBatchRequest.MAX_BATCH_SIZE), + () -> successBatchResponse(37)); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < ComputeBatchRequest.MAX_BATCH_SIZE * 2 + 37; ++i) { + batchRequest.queue(request(compute), responses); + } + + batchRequest.execute("batchContext"); + + assertThat(responses.successes).hasValue(ComputeBatchRequest.MAX_BATCH_SIZE * 2 + 37); + assertThat(responses.failures).hasValue(0); + } + + @Test + public void handlesErrors() throws IOException { + + StringBuilder responseContent = new StringBuilder(); + appendSuccessResponse(responseContent); + appendSuccessResponse(responseContent); + appendSuccessResponse(responseContent); + appendFailureResponse(responseContent); // FAILURE! + appendSuccessResponse(responseContent); + responseContent.append(MIME_END); + + Compute compute = computeWithResponses(() -> batchResponse(responseContent.toString())); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < 5; ++i) { + batchRequest.queue(request(compute), responses); + } + + batchRequest.execute("batchContext"); + + assertThat(responses.successes).hasValue(4); + assertThat(responses.failures).hasValue(1); + } + + @Test + public void propagatesFirstException() throws IOException { + + Compute compute = + computeWithResponses( + () -> successBatchResponse(ComputeBatchRequest.MAX_BATCH_SIZE), + () -> { + throw new IOException("first exception"); + }, + () -> { + throw new IOException("second exception"); + }, + () -> { + try { + Thread.sleep(Long.MAX_VALUE); + throw new AssertionError("slept forever"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < ComputeBatchRequest.MAX_BATCH_SIZE * 3; ++i) { + batchRequest.queue(request(compute), responses); + } + + Throwable throwable = catchThrowable(() -> batchRequest.execute("batchContext")); + + assertThat(throwable).isInstanceOf(IOException.class).hasMessage("first exception"); + } + + @Test + public void successMetrics() throws IOException { + + Compute compute = + computeWithResponses( + () -> successBatchResponse(ComputeBatchRequest.MAX_BATCH_SIZE), + () -> successBatchResponse(ComputeBatchRequest.MAX_BATCH_SIZE), + () -> successBatchResponse(37)); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < ComputeBatchRequest.MAX_BATCH_SIZE * 2 + 37; ++i) { + batchRequest.queue(request(compute), responses); + } + + batchRequest.execute("batchContext"); + + assertThat(registry.timers()).hasSize(1); + Timer timer = registry.timers().findFirst().orElseThrow(AssertionError::new); + assertThat(timer.id().name()).isEqualTo("google.batchExecute"); + assertThat(timer.id().tags()) + .contains( + tag("context", "batchContext"), + tag("success", "true"), + tag("status", "2xx"), + tag("statusCode", "200")); + + assertThat(registry.counters()).hasSize(1); + Counter counter = registry.counters().findFirst().orElseThrow(AssertionError::new); + assertThat(counter.id().name()).isEqualTo("google.batchSize"); + assertThat(counter.id().tags()) + .contains( + tag("context", "batchContext"), + tag("success", "true"), + tag("status", "2xx"), + tag("statusCode", "200")); + assertThat(counter.actualCount()).isEqualTo(ComputeBatchRequest.MAX_BATCH_SIZE * 2 + 37); + } + + @Test + public void errorMetrics() throws IOException { + + Compute compute = + computeWithResponses( + () -> { + throw new IOException("uh oh"); + }); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < 55; ++i) { + batchRequest.queue(request(compute), responses); + } + + assertThatIOException().isThrownBy(() -> batchRequest.execute("batchContext")); + + assertThat(registry.timers()).hasSize(1); + Timer timer = registry.timers().findFirst().orElseThrow(AssertionError::new); + assertThat(timer.id().name()).isEqualTo("google.batchExecute"); + assertThat(timer.id().tags()) + .contains( + tag("context", "batchContext"), + tag("success", "false"), + tag("status", "5xx"), + tag("statusCode", "500")); + + assertThat(registry.counters()).hasSize(1); + Counter counter = registry.counters().findFirst().orElseThrow(AssertionError::new); + assertThat(counter.id().name()).isEqualTo("google.batchSize"); + assertThat(counter.id().tags()) + .contains( + tag("context", "batchContext"), + tag("success", "false"), + tag("status", "5xx"), + tag("statusCode", "500")); + assertThat(counter.actualCount()).isEqualTo(55); + } + + @Test + public void httpErrorMetrics() throws IOException { + + Compute compute = + computeWithResponses( + () -> { + throw new HttpResponseException(404, "uh oh"); + }); + + ComputeBatchRequest batchRequest = + new ComputeBatchRequest<>( + compute, registry, USER_AGENT, MoreExecutors.newDirectExecutorService()); + + CountResponses responses = new CountResponses(); + for (int i = 0; i < 55; ++i) { + batchRequest.queue(request(compute), responses); + } + + assertThatIOException().isThrownBy(() -> batchRequest.execute("batchContext")); + + assertThat(registry.timers()).hasSize(1); + Timer timer = registry.timers().findFirst().orElseThrow(AssertionError::new); + assertThat(timer.id().name()).isEqualTo("google.batchExecute"); + assertThat(timer.id().tags()) + .contains( + tag("context", "batchContext"), + tag("success", "false"), + tag("status", "4xx"), + tag("statusCode", "404")); + + assertThat(registry.counters()).hasSize(1); + Counter counter = registry.counters().findFirst().orElseThrow(AssertionError::new); + assertThat(counter.id().name()).isEqualTo("google.batchSize"); + assertThat(counter.id().tags()) + .contains( + tag("context", "batchContext"), + tag("success", "false"), + tag("status", "4xx"), + tag("statusCode", "404")); + assertThat(counter.actualCount()).isEqualTo(55); + } + + private static GoogleComputeRequest request(Compute compute) + throws IOException { + return new GoogleComputeRequestImpl<>( + compute.images().get("project", "image-name"), + new DefaultRegistry(), + /* metricName= */ "google.api", + /* tags= */ ImmutableMap.of()); + } + + @FunctionalInterface + private interface ResponseSupplier { + + LowLevelHttpResponse getResponse() throws IOException; + } + + private static Compute computeWithResponses(ResponseSupplier... responses) { + return new Compute( + responses(responses), + JacksonFactory.getDefaultInstance(), + /* httpRequestInitializer= */ null); + } + + private static HttpTransport responses(ResponseSupplier... responses) { + return new HttpTransport() { + private AtomicInteger requests = new AtomicInteger(0); + + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) { + int requestNum = requests.getAndIncrement(); + ResponseSupplier response; + if (requestNum < responses.length) { + response = responses[requestNum]; + } else { + response = + () -> + new MockLowLevelHttpResponse() + .setStatusCode(500) + .setContent("Sent more requests than expected."); + } + return new LowLevelHttpRequest() { + @Override + public void addHeader(String name, String value) {} + + @Override + public LowLevelHttpResponse execute() throws IOException { + return response.getResponse(); + } + }; + } + }; + } + + private static MockLowLevelHttpResponse successBatchResponse(int responses) { + return batchResponse(successBatchResponseContent(responses)); + } + + private static MockLowLevelHttpResponse batchResponse(String content) { + return new MockLowLevelHttpResponse() + .setStatusCode(200) + .addHeader("Content-Type", BATCH_CONTENT_TYPE) + .setContent(content); + } + + private static String successBatchResponseContent(int responses) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < responses; ++i) { + appendSuccessResponse(sb); + } + return sb.append(MIME_END).toString(); + } + + private static void appendSuccessResponse(StringBuilder sb) { + sb.append(MIME_PART_START) + .append("Content-Type: application/http\n") + .append('\n') + .append("HTTP/1.1 200 OK\n") + .append("Content-Type: application/json\n") + .append("\n") + .append("{\"name\":\"foobar\"}\n\n"); + } + + private static void appendFailureResponse(StringBuilder sb) { + sb.append(MIME_PART_START) + .append("Content-Type: application/http\n") + .append('\n') + .append("HTTP/1.1 500 Really Bad Error\n") + .append("Content-Type: application/json\n") + .append("\n") + .append("{}\n\n"); + } + + private static class CountResponses extends JsonBatchCallback { + AtomicInteger successes = new AtomicInteger(); + AtomicInteger failures = new AtomicInteger(); + + @Override + public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) { + failures.incrementAndGet(); + } + + @Override + public void onSuccess(Image image, HttpHeaders responseHeaders) { + successes.incrementAndGet(); + } + } + + private static Tag tag(String key, String value) { + return new BasicTag(key, value); + } +} diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/FakeGoogleComputeOperationRequest.java b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/FakeGoogleComputeOperationRequest.java new file mode 100644 index 00000000000..5fd3588e9c8 --- /dev/null +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/FakeGoogleComputeOperationRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Google, 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.compute; + +import com.google.api.services.compute.ComputeRequest; +import com.google.api.services.compute.model.Operation; +import com.netflix.spinnaker.clouddriver.data.task.Task; +import java.io.IOException; + +public class FakeGoogleComputeOperationRequest> + extends FakeGoogleComputeRequest + implements GoogleComputeOperationRequest { + + private boolean waited = false; + + public FakeGoogleComputeOperationRequest(Operation response) { + super(response); + } + + @Override + public Operation executeAndWait(Task task, String phase) throws IOException { + waited = true; + return execute(); + } + + public boolean waitedForCompletion() { + return waited; + } +} diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/FakeGoogleComputeRequest.java b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/FakeGoogleComputeRequest.java new file mode 100644 index 00000000000..5022a18c960 --- /dev/null +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/FakeGoogleComputeRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 Google, 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.compute; + +import com.google.api.services.compute.ComputeRequest; +import java.io.IOException; + +public class FakeGoogleComputeRequest, ResponseT> + implements GoogleComputeRequest { + + private final ResponseT response; + + private boolean executed = false; + + public FakeGoogleComputeRequest(ResponseT response) { + this.response = response; + } + + @Override + public ResponseT execute() throws IOException { + executed = true; + return response; + } + + @Override + public RequestT getRequest() { + throw new UnsupportedOperationException("FakeGoogleComputeRequest#getRequest()"); + } + + public boolean executed() { + return executed; + } +} diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/ImagesTest.java b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/ImagesTest.java new file mode 100644 index 00000000000..3b391c461eb --- /dev/null +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/compute/ImagesTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2019 Google, 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.compute; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.services.compute.Compute; +import com.google.api.services.compute.model.Image; +import com.google.api.services.compute.model.ImageList; +import com.netflix.spectator.api.BasicTag; +import com.netflix.spectator.api.DefaultRegistry; +import com.netflix.spectator.api.NoopRegistry; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.api.Tag; +import com.netflix.spectator.api.Timer; +import com.netflix.spinnaker.clouddriver.google.security.FakeGoogleCredentials; +import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials; +import java.io.IOException; +import java.util.List; +import org.junit.Test; + +public class ImagesTest { + + private static final int CLOCK_STEP_TIME_MS = 1234; + private static final int CLOCK_STEP_TIME_NS = 1234 * 1000000; + + @Test + public void list_success() throws IOException { + + HttpTransport transport = + new ComputeOperationMockHttpTransport( + new MockLowLevelHttpResponse() + .setStatusCode(200) + .setContent( + "" + + "{" + + " \"items\": [" + + " { \"name\": \"centos\" }," + + " { \"name\": \"ubuntu\" }" + + " ]" + + "}")); + + Images imagesApi = createImages(transport); + + ImageList imageList = imagesApi.list("my-project").execute(); + + List images = imageList.getItems(); + assertThat(images).hasSize(2); + assertThat(images.get(0).getName()).isEqualTo("centos"); + assertThat(images.get(1).getName()).isEqualTo("ubuntu"); + } + + @Test + public void list_error() { + + HttpTransport transport = + new ComputeOperationMockHttpTransport( + new MockLowLevelHttpResponse().setStatusCode(404).setContent("{}")); + + Images imagesApi = createImages(transport); + + assertThatIOException() + .isThrownBy(() -> imagesApi.list("my-project").execute()) + .withMessageContaining("404"); + } + + @Test + public void list_successMetrics() throws IOException { + + Registry registry = new DefaultRegistry(new SteppingClock(CLOCK_STEP_TIME_MS)); + HttpTransport transport = + new ComputeOperationMockHttpTransport( + new MockLowLevelHttpResponse().setStatusCode(200).setContent("{\"items\": []}")); + + Images imagesApi = createImages(transport, registry); + + imagesApi.list("my-project").execute(); + + assertThat(registry.timers().count()).isEqualTo(1); + Timer timer = registry.timers().findFirst().orElseThrow(AssertionError::new); + assertThat(timer.id().name()).isEqualTo("google.api"); + // TODO(plumpy): Come up with something better than AccountForClient (which uses a bunch of + // global state) so that we can test for the account tags + assertThat(timer.id().tags()) + .contains( + tag("api", "compute.images.list"), + tag("scope", "global"), + tag("status", "2xx"), + tag("success", "true")); + assertThat(timer.totalTime()).isEqualTo(CLOCK_STEP_TIME_NS); + } + + @Test + public void list_errorMetrics() { + + Registry registry = new DefaultRegistry(new SteppingClock(CLOCK_STEP_TIME_MS)); + HttpTransport transport = + new ComputeOperationMockHttpTransport( + new MockLowLevelHttpResponse().setStatusCode(404).setContent("{}")); + + Images imagesApi = createImages(transport, registry); + + assertThatIOException().isThrownBy(() -> imagesApi.list("my-project").execute()); + + assertThat(registry.timers().count()).isEqualTo(1); + Timer timer = registry.timers().findFirst().orElseThrow(AssertionError::new); + assertThat(timer.id().name()).isEqualTo("google.api"); + // TODO(plumpy): Come up with something better than AccountForClient (which uses a bunch of + // global state) so that we can test for the account tags + assertThat(timer.id().tags()) + .contains( + tag("api", "compute.images.list"), + tag("scope", "global"), + tag("status", "4xx"), + tag("success", "false")); + assertThat(timer.totalTime()).isEqualTo(CLOCK_STEP_TIME_NS); + } + + private static Images createImages(HttpTransport transport) { + return createImages(transport, new NoopRegistry()); + } + + private static Images createImages(HttpTransport transport, Registry registry) { + Compute compute = + new Compute( + transport, JacksonFactory.getDefaultInstance(), /* httpRequestInitializer= */ null); + GoogleNamedAccountCredentials credentials = + new GoogleNamedAccountCredentials.Builder() + .name("plumpy") + .project("myproject") + .credentials(new FakeGoogleCredentials()) + .compute(compute) + .build(); + return new Images(credentials, registry); + } + + private static Tag tag(String key, String value) { + return new BasicTag(key, value); + } +} diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverterTest.java b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverterTest.java index 51cb5e2c90c..74e38489bd4 100644 --- a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverterTest.java +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverterTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google, 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.deploy.converters; import static org.assertj.core.api.Assertions.assertThat; diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/AbandonAndDecrementGoogleServerGroupAtomicOperationUnitSpec.groovy b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/AbandonAndDecrementGoogleServerGroupAtomicOperationUnitSpec.groovy index 57b935a2949..f74c0b8cedb 100644 --- a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/AbandonAndDecrementGoogleServerGroupAtomicOperationUnitSpec.groovy +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/AbandonAndDecrementGoogleServerGroupAtomicOperationUnitSpec.groovy @@ -18,6 +18,7 @@ package com.netflix.spinnaker.clouddriver.google.deploy.ops import com.google.api.services.compute.Compute import com.google.api.services.compute.model.InstanceGroupManagersAbandonInstancesRequest +import com.google.common.util.concurrent.MoreExecutors import com.netflix.spectator.api.DefaultRegistry import com.netflix.spinnaker.clouddriver.data.task.Task import com.netflix.spinnaker.clouddriver.data.task.TaskRepository @@ -83,7 +84,7 @@ class AbandonAndDecrementGoogleServerGroupAtomicOperationUnitSpec extends Specif @Subject def operation = new AbandonAndDecrementGoogleServerGroupAtomicOperation(description) operation.registry = registry operation.googleClusterProvider = googleClusterProviderMock - operation.computeApiFactory = new GoogleComputeApiFactory(Mock(GoogleOperationPoller), registry) + operation.computeApiFactory = new GoogleComputeApiFactory(Mock(GoogleOperationPoller), registry, "user-agent", MoreExecutors.newDirectExecutorService()) when: operation.operate([]) diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/DestroyGoogleServerGroupAtomicOperationUnitSpec.groovy b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/DestroyGoogleServerGroupAtomicOperationUnitSpec.groovy index 5f437393bf6..3e1c48654cc 100644 --- a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/DestroyGoogleServerGroupAtomicOperationUnitSpec.groovy +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/DestroyGoogleServerGroupAtomicOperationUnitSpec.groovy @@ -1,4 +1,4 @@ - /* +/* * Copyright 2014 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,17 +22,18 @@ import com.google.api.client.http.HttpHeaders import com.google.api.client.http.HttpResponseException import com.google.api.services.compute.Compute import com.google.api.services.compute.model.* +import com.google.common.util.concurrent.MoreExecutors import com.netflix.frigga.Names import com.netflix.spectator.api.DefaultRegistry import com.netflix.spinnaker.clouddriver.data.task.Task import com.netflix.spinnaker.clouddriver.data.task.TaskRepository import com.netflix.spinnaker.clouddriver.google.GoogleApiTestUtils +import com.netflix.spinnaker.clouddriver.google.compute.GoogleComputeApiFactory import com.netflix.spinnaker.clouddriver.google.config.GoogleConfigurationProperties import com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil import com.netflix.spinnaker.clouddriver.google.deploy.GoogleOperationPoller import com.netflix.spinnaker.clouddriver.google.deploy.SafeRetry import com.netflix.spinnaker.clouddriver.google.deploy.description.DestroyGoogleServerGroupDescription -import com.netflix.spinnaker.clouddriver.google.compute.GoogleComputeApiFactory import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleBackendService import com.netflix.spinnaker.clouddriver.google.model.loadbalancing.GoogleHttpLoadBalancer @@ -71,566 +72,578 @@ class DestroyGoogleServerGroupAtomicOperationUnitSpec extends Specification { void "should delete managed instance group"() { setup: - def registry = new DefaultRegistry() - def googleClusterProviderMock = Mock(GoogleClusterProvider) - def serverGroup = - new GoogleServerGroup(name: SERVER_GROUP_NAME, - region: REGION, - zone: ZONE, - launchConfig: [instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME)]).view - def computeMock = Mock(Compute) - def instanceGroupManagersMock = Mock(Compute.InstanceGroupManagers) - def zoneOperations = Mock(Compute.ZoneOperations) - def zoneOperationsGet = Mock(Compute.ZoneOperations.Get) - def instanceGroupManagersDeleteMock = Mock(Compute.InstanceGroupManagers.Delete) - def instanceGroupManagersDeleteOp = new Operation(name: INSTANCE_GROUP_OP_NAME, status: DONE, zone: ZONE, targetLink: "/${SERVER_GROUP_NAME}") - def instanceTemplatesMock = Mock(Compute.InstanceTemplates) - def instanceTemplatesDeleteMock = Mock(Compute.InstanceTemplates.Delete) - def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) - - def forwardingRules = Mock(Compute.ForwardingRules) - def forwardingRulesList = Mock(Compute.ForwardingRules.List) - def globalForwardingRules = Mock(Compute.GlobalForwardingRules) - def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) - def targetSslProxies = Mock(Compute.TargetSslProxies) - def targetSslProxiesList = Mock(Compute.TargetSslProxies.List) - def targetTcpProxies = Mock(Compute.TargetTcpProxies) - def targetTcpProxiesList = Mock(Compute.TargetTcpProxies.List) - - googleLoadBalancerProviderMock.getApplicationLoadBalancers(APPLICATION_NAME) >> [] - def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() - def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, - region: REGION, - accountName: ACCOUNT_NAME, - credentials: credentials) - @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) - operation.googleOperationPoller = - new GoogleOperationPoller( - googleConfigurationProperties: new GoogleConfigurationProperties(), - threadSleeper: threadSleeperMock, - registry: registry, - safeRetry: safeRetry - ) - operation.registry = registry - operation.safeRetry = safeRetry - operation.googleClusterProvider = googleClusterProviderMock - operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock - operation.computeApiFactory = new GoogleComputeApiFactory(operation.googleOperationPoller, registry) + def registry = new DefaultRegistry() + def googleClusterProviderMock = Mock(GoogleClusterProvider) + def serverGroup = + new GoogleServerGroup(name: SERVER_GROUP_NAME, + region: REGION, + zone: ZONE, + launchConfig: [instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME)]).view + def computeMock = Mock(Compute) + def instanceGroupManagersMock = Mock(Compute.InstanceGroupManagers) + def zoneOperations = Mock(Compute.ZoneOperations) + def zoneOperationsGet = Mock(Compute.ZoneOperations.Get) + def instanceGroupManagersDeleteMock = Mock(Compute.InstanceGroupManagers.Delete) + def instanceGroupManagersDeleteOp = new Operation(name: INSTANCE_GROUP_OP_NAME, status: DONE, zone: ZONE, targetLink: "/${SERVER_GROUP_NAME}") + def instanceTemplatesMock = Mock(Compute.InstanceTemplates) + def instanceTemplatesDeleteMock = Mock(Compute.InstanceTemplates.Delete) + def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) + + def forwardingRules = Mock(Compute.ForwardingRules) + def forwardingRulesList = Mock(Compute.ForwardingRules.List) + def globalForwardingRules = Mock(Compute.GlobalForwardingRules) + def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) + def targetSslProxies = Mock(Compute.TargetSslProxies) + def targetSslProxiesList = Mock(Compute.TargetSslProxies.List) + def targetTcpProxies = Mock(Compute.TargetTcpProxies) + def targetTcpProxiesList = Mock(Compute.TargetTcpProxies.List) + + googleLoadBalancerProviderMock.getApplicationLoadBalancers(APPLICATION_NAME) >> [] + def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() + def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, + region: REGION, + accountName: ACCOUNT_NAME, + credentials: credentials) + @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) + operation.googleOperationPoller = + new GoogleOperationPoller( + googleConfigurationProperties: new GoogleConfigurationProperties(), + threadSleeper: threadSleeperMock, + registry: registry, + safeRetry: safeRetry + ) + operation.registry = registry + operation.safeRetry = safeRetry + operation.googleClusterProvider = googleClusterProviderMock + operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock + operation.computeApiFactory = new GoogleComputeApiFactory(operation.googleOperationPoller, registry, "user-agent", MoreExecutors.newDirectExecutorService()) when: - operation.operate([]) + operation.operate([]) then: - 1 * googleClusterProviderMock.getServerGroup(ACCOUNT_NAME, REGION, SERVER_GROUP_NAME) >> serverGroup + 1 * googleClusterProviderMock.getServerGroup(ACCOUNT_NAME, REGION, SERVER_GROUP_NAME) >> serverGroup - 1 * computeMock.instanceGroupManagers() >> instanceGroupManagersMock - 1 * instanceGroupManagersMock.delete(PROJECT_NAME, ZONE, SERVER_GROUP_NAME) >> instanceGroupManagersDeleteMock - 1 * instanceGroupManagersDeleteMock.execute() >> instanceGroupManagersDeleteOp + 1 * computeMock.instanceGroupManagers() >> instanceGroupManagersMock + 1 * instanceGroupManagersMock.delete(PROJECT_NAME, ZONE, SERVER_GROUP_NAME) >> instanceGroupManagersDeleteMock + 1 * instanceGroupManagersDeleteMock.execute() >> instanceGroupManagersDeleteOp - 1 * computeMock.zoneOperations() >> zoneOperations - 1 * zoneOperations.get(PROJECT_NAME, ZONE, INSTANCE_GROUP_OP_NAME) >> zoneOperationsGet - 1 * zoneOperationsGet.execute() >> instanceGroupManagersDeleteOp + 1 * computeMock.zoneOperations() >> zoneOperations + 1 * zoneOperations.get(PROJECT_NAME, ZONE, INSTANCE_GROUP_OP_NAME) >> zoneOperationsGet + 1 * zoneOperationsGet.execute() >> instanceGroupManagersDeleteOp - 1 * computeMock.instanceTemplates() >> instanceTemplatesMock - 1 * instanceTemplatesMock.delete(PROJECT_NAME, INSTANCE_TEMPLATE_NAME) >> instanceTemplatesDeleteMock - 1 * instanceTemplatesDeleteMock.execute() + 1 * computeMock.instanceTemplates() >> instanceTemplatesMock + 1 * instanceTemplatesMock.delete(PROJECT_NAME, INSTANCE_TEMPLATE_NAME) >> instanceTemplatesDeleteMock + 1 * instanceTemplatesDeleteMock.execute() - 1 * computeMock.targetSslProxies() >> targetSslProxies - 1 * targetSslProxies.list(PROJECT_NAME) >> targetSslProxiesList - 1 * targetSslProxiesList.execute() >> new TargetSslProxyList(items: []) + 1 * computeMock.targetSslProxies() >> targetSslProxies + 1 * targetSslProxies.list(PROJECT_NAME) >> targetSslProxiesList + 1 * targetSslProxiesList.execute() >> new TargetSslProxyList(items: []) - 1 * computeMock.targetTcpProxies() >> targetTcpProxies - 1 * targetTcpProxies.list(PROJECT_NAME) >> targetTcpProxiesList - 1 * targetTcpProxiesList.execute() >> new TargetTcpProxyList(items: []) + 1 * computeMock.targetTcpProxies() >> targetTcpProxies + 1 * targetTcpProxies.list(PROJECT_NAME) >> targetTcpProxiesList + 1 * targetTcpProxiesList.execute() >> new TargetTcpProxyList(items: []) - 3 * computeMock.globalForwardingRules() >> globalForwardingRules - 3 * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList - 3 * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) + 3 * computeMock.globalForwardingRules() >> globalForwardingRules + 3 * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList + 3 * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) - 1 * computeMock.forwardingRules() >> forwardingRules - 1 * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList - 1 * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) + 1 * computeMock.forwardingRules() >> forwardingRules + 1 * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList + 1 * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) } @Unroll void "should delete managed instance group and autoscaler if defined (isRegional: #isRegional)"() { setup: - def registry = new DefaultRegistry() - def googleClusterProviderMock = Mock(GoogleClusterProvider) - def serverGroup = - new GoogleServerGroup(name: SERVER_GROUP_NAME, - region: REGION, - regional: isRegional, - zone: ZONE, - launchConfig: [instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME)], - autoscalingPolicy: [coolDownPeriodSec: 45, - minNumReplicas: 2, - maxNumReplicas: 5]).view - def computeMock = Mock(Compute) - def regionInstanceGroupManagersMock = Mock(Compute.RegionInstanceGroupManagers) - def instanceGroupManagersMock = Mock(Compute.InstanceGroupManagers) - def regionOperations = Mock(Compute.RegionOperations) - def regionOperationsGet = Mock(Compute.RegionOperations.Get) - def zoneOperations = Mock(Compute.ZoneOperations) - def zoneOperationsGet = Mock(Compute.ZoneOperations.Get) - def regionInstanceGroupManagersDeleteMock = Mock(Compute.RegionInstanceGroupManagers.Delete) - def regionalInstanceGroupTimerId = GoogleApiTestUtils.makeOkId( - registry, "compute.regionInstanceGroupManagers.delete", - [scope: "regional", region: REGION]) - def instanceGroupManagersDeleteMock = Mock(Compute.InstanceGroupManagers.Delete) - def instanceGroupManagersDeleteOp = new Operation(name: INSTANCE_GROUP_OP_NAME, status: DONE, zone: ZONE, region: REGION, targetLink: "/${SERVER_GROUP_NAME}") - def zonalInstanceGroupTimerId = GoogleApiTestUtils.makeOkId( - registry, "compute.instanceGroupManagers.delete", - [scope: "zonal", zone: ZONE]) - - def instanceTemplatesMock = Mock(Compute.InstanceTemplates) - def instanceTemplatesDeleteMock = Mock(Compute.InstanceTemplates.Delete) - def regionAutoscalersMock = Mock(Compute.RegionAutoscalers) - def regionAutoscalersDeleteMock = Mock(Compute.RegionAutoscalers.Delete) - def regionalAutoscalerTimerId = GoogleApiTestUtils.makeOkId( - registry, "compute.regionAutoscalers.delete", - [scope: "regional", region: REGION]) - def autoscalersMock = Mock(Compute.Autoscalers) - def autoscalersDeleteMock = Mock(Compute.Autoscalers.Delete) - def autoscalersDeleteOp = new Operation(name: AUTOSCALERS_OP_NAME, status: DONE) - def zonalAutoscalerTimerId = GoogleApiTestUtils.makeOkId( - registry, "compute.autoscalers.delete", - [scope: "zonal", zone: ZONE]) - - def forwardingRules = Mock(Compute.ForwardingRules) - def forwardingRulesList = Mock(Compute.ForwardingRules.List) - def globalForwardingRules = Mock(Compute.GlobalForwardingRules) - def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) - def targetSslProxies = Mock(Compute.TargetSslProxies) - def targetSslProxiesList = Mock(Compute.TargetSslProxies.List) - def targetTcpProxies = Mock(Compute.TargetTcpProxies) - def targetTcpProxiesList = Mock(Compute.TargetTcpProxies.List) - - def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() - def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, - region: REGION, - accountName: ACCOUNT_NAME, - credentials: credentials) - def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) - googleLoadBalancerProviderMock.getApplicationLoadBalancers(APPLICATION_NAME) >> [] - @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) - operation.googleOperationPoller = - new GoogleOperationPoller( - googleConfigurationProperties: new GoogleConfigurationProperties(), - threadSleeper: threadSleeperMock, - registry: registry, - safeRetry: safeRetry - ) - operation.registry = registry - operation.safeRetry = safeRetry - operation.googleClusterProvider = googleClusterProviderMock - operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock - operation.computeApiFactory = new GoogleComputeApiFactory(operation.googleOperationPoller, registry) + def registry = new DefaultRegistry() + def googleClusterProviderMock = Mock(GoogleClusterProvider) + def serverGroup = + new GoogleServerGroup(name: SERVER_GROUP_NAME, + region: REGION, + regional: isRegional, + zone: ZONE, + launchConfig: [instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME)], + autoscalingPolicy: [coolDownPeriodSec: 45, + minNumReplicas : 2, + maxNumReplicas : 5]).view + def computeMock = Mock(Compute) + def regionInstanceGroupManagersMock = Mock(Compute.RegionInstanceGroupManagers) + def instanceGroupManagersMock = Mock(Compute.InstanceGroupManagers) + def regionOperations = Mock(Compute.RegionOperations) + def regionOperationsGet = Mock(Compute.RegionOperations.Get) + def zoneOperations = Mock(Compute.ZoneOperations) + def zoneOperationsGet = Mock(Compute.ZoneOperations.Get) + def regionInstanceGroupManagersDeleteMock = Mock(Compute.RegionInstanceGroupManagers.Delete) + def regionalInstanceGroupTimerId = GoogleApiTestUtils.makeOkId( + registry, "compute.regionInstanceGroupManagers.delete", + [scope: "regional", region: REGION]) + def instanceGroupManagersDeleteMock = Mock(Compute.InstanceGroupManagers.Delete) + def instanceGroupManagersDeleteOp = new Operation(name: INSTANCE_GROUP_OP_NAME, status: DONE, zone: ZONE, region: REGION, targetLink: "/${SERVER_GROUP_NAME}") + def zonalInstanceGroupTimerId = GoogleApiTestUtils.makeOkId( + registry, "compute.instanceGroupManagers.delete", + [scope: "zonal", zone: ZONE]) + + def instanceTemplatesMock = Mock(Compute.InstanceTemplates) + def instanceTemplatesDeleteMock = Mock(Compute.InstanceTemplates.Delete) + def regionAutoscalersMock = Mock(Compute.RegionAutoscalers) + def regionAutoscalersDeleteMock = Mock(Compute.RegionAutoscalers.Delete) + def regionalAutoscalerTimerId = GoogleApiTestUtils.makeOkId( + registry, "compute.regionAutoscalers.delete", + [scope: "regional", region: REGION]) + def autoscalersMock = Mock(Compute.Autoscalers) + def autoscalersDeleteMock = Mock(Compute.Autoscalers.Delete) + def autoscalersDeleteOp = new Operation(name: AUTOSCALERS_OP_NAME, status: DONE) + def zonalAutoscalerTimerId = GoogleApiTestUtils.makeOkId( + registry, "compute.autoscalers.delete", + [scope: "zonal", zone: ZONE]) + + def forwardingRules = Mock(Compute.ForwardingRules) + def forwardingRulesList = Mock(Compute.ForwardingRules.List) + def globalForwardingRules = Mock(Compute.GlobalForwardingRules) + def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) + def targetSslProxies = Mock(Compute.TargetSslProxies) + def targetSslProxiesList = Mock(Compute.TargetSslProxies.List) + def targetTcpProxies = Mock(Compute.TargetTcpProxies) + def targetTcpProxiesList = Mock(Compute.TargetTcpProxies.List) + + def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() + def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, + region: REGION, + accountName: ACCOUNT_NAME, + credentials: credentials) + def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) + googleLoadBalancerProviderMock.getApplicationLoadBalancers(APPLICATION_NAME) >> [] + @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) + operation.googleOperationPoller = + new GoogleOperationPoller( + googleConfigurationProperties: new GoogleConfigurationProperties(), + threadSleeper: threadSleeperMock, + registry: registry, + safeRetry: safeRetry + ) + operation.registry = registry + operation.safeRetry = safeRetry + operation.googleClusterProvider = googleClusterProviderMock + operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock + operation.computeApiFactory = new GoogleComputeApiFactory(operation.googleOperationPoller, registry, "user-agent", MoreExecutors.newDirectExecutorService()) when: - operation.operate([]) + operation.operate([]) then: - 1 * googleClusterProviderMock.getServerGroup(ACCOUNT_NAME, REGION, SERVER_GROUP_NAME) >> serverGroup - - 3 * computeMock.globalForwardingRules() >> globalForwardingRules - 3 * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList - 3 * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) - - 1 * computeMock.targetSslProxies() >> targetSslProxies - 1 * targetSslProxies.list(PROJECT_NAME) >> targetSslProxiesList - 1 * targetSslProxiesList.execute() >> new TargetSslProxyList(items: []) - - 1 * computeMock.targetTcpProxies() >> targetTcpProxies - 1 * targetTcpProxies.list(PROJECT_NAME) >> targetTcpProxiesList - 1 * targetTcpProxiesList.execute() >> new TargetTcpProxyList(items: []) - - 1 * computeMock.forwardingRules() >> forwardingRules - 1 * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList - 1 * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) - - if (isRegional) { - 1 * computeMock.regionAutoscalers() >> regionAutoscalersMock - 1 * regionAutoscalersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> regionAutoscalersDeleteMock - 1 * regionAutoscalersDeleteMock.execute() >> autoscalersDeleteOp - - 1 * computeMock.regionOperations() >> regionOperations - 1 * regionOperations.get(PROJECT_NAME, location, AUTOSCALERS_OP_NAME) >> regionOperationsGet - 1 * regionOperationsGet.execute() >> autoscalersDeleteOp - } else { - 1 * computeMock.autoscalers() >> autoscalersMock - 1 * autoscalersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> autoscalersDeleteMock - 1 * autoscalersDeleteMock.execute() >> autoscalersDeleteOp - - 1 * computeMock.zoneOperations() >> zoneOperations - 1 * zoneOperations.get(PROJECT_NAME, location, AUTOSCALERS_OP_NAME) >> zoneOperationsGet - 1 * zoneOperationsGet.execute() >> autoscalersDeleteOp - } - registry.timer(regionalAutoscalerTimerId).count() == (isRegional ? 1 : 0) - registry.timer(zonalAutoscalerTimerId).count() == (isRegional ? 0 : 1) + 1 * googleClusterProviderMock.getServerGroup(ACCOUNT_NAME, REGION, SERVER_GROUP_NAME) >> serverGroup + + 3 * computeMock.globalForwardingRules() >> globalForwardingRules + 3 * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList + 3 * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) + + 1 * computeMock.targetSslProxies() >> targetSslProxies + 1 * targetSslProxies.list(PROJECT_NAME) >> targetSslProxiesList + 1 * targetSslProxiesList.execute() >> new TargetSslProxyList(items: []) + + 1 * computeMock.targetTcpProxies() >> targetTcpProxies + 1 * targetTcpProxies.list(PROJECT_NAME) >> targetTcpProxiesList + 1 * targetTcpProxiesList.execute() >> new TargetTcpProxyList(items: []) + + 1 * computeMock.forwardingRules() >> forwardingRules + 1 * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList + 1 * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) + + if (isRegional) { + 1 * computeMock.regionAutoscalers() >> regionAutoscalersMock + 1 * regionAutoscalersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> regionAutoscalersDeleteMock + 1 * regionAutoscalersDeleteMock.execute() >> autoscalersDeleteOp + + 1 * computeMock.regionOperations() >> regionOperations + 1 * regionOperations.get(PROJECT_NAME, location, AUTOSCALERS_OP_NAME) >> regionOperationsGet + 1 * regionOperationsGet.execute() >> autoscalersDeleteOp + } else { + 1 * computeMock.autoscalers() >> autoscalersMock + 1 * autoscalersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> autoscalersDeleteMock + 1 * autoscalersDeleteMock.execute() >> autoscalersDeleteOp + + 1 * computeMock.zoneOperations() >> zoneOperations + 1 * zoneOperations.get(PROJECT_NAME, location, AUTOSCALERS_OP_NAME) >> zoneOperationsGet + 1 * zoneOperationsGet.execute() >> autoscalersDeleteOp + } + registry.timer(regionalAutoscalerTimerId).count() == (isRegional ? 1 : 0) + registry.timer(zonalAutoscalerTimerId).count() == (isRegional ? 0 : 1) then: - if (isRegional) { - 1 * computeMock.regionInstanceGroupManagers() >> regionInstanceGroupManagersMock - 1 * regionInstanceGroupManagersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> regionInstanceGroupManagersDeleteMock - 1 * regionInstanceGroupManagersDeleteMock.execute() >> instanceGroupManagersDeleteOp - - 1 * computeMock.regionOperations() >> regionOperations - 1 * regionOperations.get(PROJECT_NAME, location, INSTANCE_GROUP_OP_NAME) >> regionOperationsGet - 1 * regionOperationsGet.execute() >> instanceGroupManagersDeleteOp - } else { - 1 * computeMock.instanceGroupManagers() >> instanceGroupManagersMock - 1 * instanceGroupManagersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> instanceGroupManagersDeleteMock - 1 * instanceGroupManagersDeleteMock.execute() >> instanceGroupManagersDeleteOp - - 1 * computeMock.zoneOperations() >> zoneOperations - 1 * zoneOperations.get(PROJECT_NAME, location, INSTANCE_GROUP_OP_NAME) >> zoneOperationsGet - 1 * zoneOperationsGet.execute() >> instanceGroupManagersDeleteOp - } - registry.timer(regionalInstanceGroupTimerId).count() == (isRegional ? 1 : 0) - registry.timer(zonalInstanceGroupTimerId).count() == (isRegional ? 0 : 1) + if (isRegional) { + 1 * computeMock.regionInstanceGroupManagers() >> regionInstanceGroupManagersMock + 1 * regionInstanceGroupManagersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> regionInstanceGroupManagersDeleteMock + 1 * regionInstanceGroupManagersDeleteMock.execute() >> instanceGroupManagersDeleteOp + + 1 * computeMock.regionOperations() >> regionOperations + 1 * regionOperations.get(PROJECT_NAME, location, INSTANCE_GROUP_OP_NAME) >> regionOperationsGet + 1 * regionOperationsGet.execute() >> instanceGroupManagersDeleteOp + } else { + 1 * computeMock.instanceGroupManagers() >> instanceGroupManagersMock + 1 * instanceGroupManagersMock.delete(PROJECT_NAME, location, SERVER_GROUP_NAME) >> instanceGroupManagersDeleteMock + 1 * instanceGroupManagersDeleteMock.execute() >> instanceGroupManagersDeleteOp + + 1 * computeMock.zoneOperations() >> zoneOperations + 1 * zoneOperations.get(PROJECT_NAME, location, INSTANCE_GROUP_OP_NAME) >> zoneOperationsGet + 1 * zoneOperationsGet.execute() >> instanceGroupManagersDeleteOp + } + registry.timer(regionalInstanceGroupTimerId).count() == (isRegional ? 1 : 0) + registry.timer(zonalInstanceGroupTimerId).count() == (isRegional ? 0 : 1) then: - 1 * computeMock.instanceTemplates() >> instanceTemplatesMock - 1 * instanceTemplatesMock.delete(PROJECT_NAME, INSTANCE_TEMPLATE_NAME) >> instanceTemplatesDeleteMock - 1 * instanceTemplatesDeleteMock.execute() + 1 * computeMock.instanceTemplates() >> instanceTemplatesMock + 1 * instanceTemplatesMock.delete(PROJECT_NAME, INSTANCE_TEMPLATE_NAME) >> instanceTemplatesDeleteMock + 1 * instanceTemplatesDeleteMock.execute() where: - isRegional | location - false | ZONE - true | REGION + isRegional | location + false | ZONE + true | REGION } @Unroll void "should delete http loadbalancer backend if associated"() { setup: - def registry = new DefaultRegistry() - def googleClusterProviderMock = Mock(GoogleClusterProvider) - def loadBalancerNameList = lbNames - def serverGroup = - new GoogleServerGroup( - name: SERVER_GROUP_NAME, - region: REGION, - regional: isRegional, - zone: ZONE, - asg: [ - (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES): loadBalancerNameList, - ], - launchConfig: [ - instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME, - properties: [ - 'metadata': new Metadata(items: [ - new Metadata.Items( - key: (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES), - value: 'spinnaker-http-load-balancer' - ), - new Metadata.Items( - key: (GCEUtil.BACKEND_SERVICE_NAMES), - value: 'backend-service' - ) - ]) - ]) - ]).view - def computeMock = Mock(Compute) - def backendServicesMock = Mock(Compute.BackendServices) - def backendSvcGetMock = Mock(Compute.BackendServices.Get) - def backendUpdateMock = Mock(Compute.BackendServices.Update) - - def forwardingRules = Mock(Compute.ForwardingRules) - def forwardingRulesList = Mock(Compute.ForwardingRules.List) - def globalForwardingRules = Mock(Compute.GlobalForwardingRules) - def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) - - def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) - googleLoadBalancerProviderMock.getApplicationLoadBalancers("") >> loadBalancerList - def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() - def task = Mock(Task) - def bs = isRegional ? - new BackendService(backends: lbNames.collect { new Backend(group: GCEUtil.buildZonalServerGroupUrl(PROJECT_NAME, ZONE, serverGroup.name)) }) : - new BackendService(backends: lbNames.collect { new Backend(group: GCEUtil.buildRegionalServerGroupUrl(PROJECT_NAME, REGION, serverGroup.name)) }) - - def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, - region: REGION, - accountName: ACCOUNT_NAME, - credentials: credentials) - @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) - def googleOperationPoller = Mock(GoogleOperationPoller) - operation.googleOperationPoller = googleOperationPoller - def updateOpName = 'updateOp' - - operation.registry = registry - operation.safeRetry = safeRetry - operation.googleClusterProvider = googleClusterProviderMock - operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock - operation.computeApiFactory = new GoogleComputeApiFactory(operation.googleOperationPoller, registry) + def registry = new DefaultRegistry() + def googleClusterProviderMock = Mock(GoogleClusterProvider) + def loadBalancerNameList = lbNames + def serverGroup = + new GoogleServerGroup( + name: SERVER_GROUP_NAME, + region: REGION, + regional: isRegional, + zone: ZONE, + asg: [ + (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES): loadBalancerNameList, + ], + launchConfig: [ + instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME, + properties: [ + 'metadata': new Metadata(items: [ + new Metadata.Items( + key: (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES), + value: 'spinnaker-http-load-balancer' + ), + new Metadata.Items( + key: (GCEUtil.BACKEND_SERVICE_NAMES), + value: 'backend-service' + ) + ]) + ]) + ]).view + def computeMock = Mock(Compute) + def backendServicesMock = Mock(Compute.BackendServices) + def backendSvcGetMock = Mock(Compute.BackendServices.Get) + def backendUpdateMock = Mock(Compute.BackendServices.Update) + + def forwardingRules = Mock(Compute.ForwardingRules) + def forwardingRulesList = Mock(Compute.ForwardingRules.List) + def globalForwardingRules = Mock(Compute.GlobalForwardingRules) + def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) + + def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) + googleLoadBalancerProviderMock.getApplicationLoadBalancers("") >> loadBalancerList + def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() + def task = Mock(Task) + def bs = isRegional ? + new BackendService(backends: lbNames.collect { + new Backend(group: GCEUtil.buildZonalServerGroupUrl(PROJECT_NAME, ZONE, serverGroup.name)) + }) : + new BackendService(backends: lbNames.collect { + new Backend(group: GCEUtil.buildRegionalServerGroupUrl(PROJECT_NAME, REGION, serverGroup.name)) + }) + + def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, + region: REGION, + accountName: ACCOUNT_NAME, + credentials: credentials) + @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) + def googleOperationPoller = Mock(GoogleOperationPoller) + operation.googleOperationPoller = googleOperationPoller + def updateOpName = 'updateOp' + + operation.registry = registry + operation.safeRetry = safeRetry + operation.googleClusterProvider = googleClusterProviderMock + operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock + operation.computeApiFactory = new GoogleComputeApiFactory(operation.googleOperationPoller, registry, "user-agent", MoreExecutors.newDirectExecutorService()) when: - def closure = operation.destroyHttpLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock) - closure() + def closure = operation.destroyHttpLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock) + closure() then: - _ * computeMock.backendServices() >> backendServicesMock - _ * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock - _ * backendSvcGetMock.execute() >> bs - _ * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock - _ * backendUpdateMock.execute() >> [name: updateOpName] - _ * googleOperationPoller.waitForGlobalOperation(computeMock, PROJECT_NAME, updateOpName, null, task, _, _) - - _ * computeMock.globalForwardingRules() >> globalForwardingRules - _ * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList - _ * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) - - _ * computeMock.forwardingRules() >> forwardingRules - _ * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList - _ * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) - bs.backends.size == 0 + _ * computeMock.backendServices() >> backendServicesMock + _ * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock + _ * backendSvcGetMock.execute() >> bs + _ * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock + _ * backendUpdateMock.execute() >> [name: updateOpName] + _ * googleOperationPoller.waitForGlobalOperation(computeMock, PROJECT_NAME, updateOpName, null, task, _, _) + + _ * computeMock.globalForwardingRules() >> globalForwardingRules + _ * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList + _ * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) + + _ * computeMock.forwardingRules() >> forwardingRules + _ * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList + _ * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) + bs.backends.size == 0 where: - isRegional | location | loadBalancerList | lbNames - false | ZONE | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] - true | REGION | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] - false | ZONE | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] - true | REGION | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] - false | ZONE | [] | [] - true | REGION | [] | [] + isRegional | location | loadBalancerList | lbNames + false | ZONE | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] + true | REGION | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] + false | ZONE | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] + true | REGION | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] + false | ZONE | [] | [] + true | REGION | [] | [] } @Unroll void "should delete internal loadbalancer backend if associated"() { setup: - def registry = new DefaultRegistry() - def googleClusterProviderMock = Mock(GoogleClusterProvider) - def loadBalancerNameList = lbNames - def serverGroup = - new GoogleServerGroup( - name: SERVER_GROUP_NAME, - region: REGION, - regional: isRegional, - zone: ZONE, - asg: [ - (GCEUtil.REGIONAL_LOAD_BALANCER_NAMES): loadBalancerNameList, - ], - launchConfig: [ - instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME, - properties: [ - 'metadata': new Metadata(items: [ - new Metadata.Items( - key: (GCEUtil.REGIONAL_LOAD_BALANCER_NAMES), - value: 'spinnaker-int-load-balancer' - ) - ]) - ]) - ]).view - def computeMock = Mock(Compute) - def backendServicesMock = Mock(Compute.RegionBackendServices) - def backendSvcGetMock = Mock(Compute.RegionBackendServices.Get) - def backendUpdateMock = Mock(Compute.RegionBackendServices.Update) - def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) - - def forwardingRules = Mock(Compute.ForwardingRules) - def forwardingRulesList = Mock(Compute.ForwardingRules.List) - def globalForwardingRules = Mock(Compute.GlobalForwardingRules) - def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) - - googleLoadBalancerProviderMock.getApplicationLoadBalancers("") >> loadBalancerList - def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() - def bs = isRegional ? - new BackendService(backends: lbNames.collect { new Backend(group: GCEUtil.buildZonalServerGroupUrl(PROJECT_NAME, ZONE, serverGroup.name)) }) : - new BackendService(backends: lbNames.collect { new Backend(group: GCEUtil.buildRegionalServerGroupUrl(PROJECT_NAME, REGION, serverGroup.name)) }) - - def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, + def registry = new DefaultRegistry() + def googleClusterProviderMock = Mock(GoogleClusterProvider) + def loadBalancerNameList = lbNames + def serverGroup = + new GoogleServerGroup( + name: SERVER_GROUP_NAME, region: REGION, - accountName: ACCOUNT_NAME, - credentials: credentials) - @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) - - def task = Mock(Task) - def googleOperationPoller = Mock(GoogleOperationPoller) - operation.googleOperationPoller = googleOperationPoller - def updateOpName = 'updateOp' - - operation.registry = registry - operation.safeRetry = safeRetry - operation.googleClusterProvider = googleClusterProviderMock - operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock + regional: isRegional, + zone: ZONE, + asg: [ + (GCEUtil.REGIONAL_LOAD_BALANCER_NAMES): loadBalancerNameList, + ], + launchConfig: [ + instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME, + properties: [ + 'metadata': new Metadata(items: [ + new Metadata.Items( + key: (GCEUtil.REGIONAL_LOAD_BALANCER_NAMES), + value: 'spinnaker-int-load-balancer' + ) + ]) + ]) + ]).view + def computeMock = Mock(Compute) + def backendServicesMock = Mock(Compute.RegionBackendServices) + def backendSvcGetMock = Mock(Compute.RegionBackendServices.Get) + def backendUpdateMock = Mock(Compute.RegionBackendServices.Update) + def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) + + def forwardingRules = Mock(Compute.ForwardingRules) + def forwardingRulesList = Mock(Compute.ForwardingRules.List) + def globalForwardingRules = Mock(Compute.GlobalForwardingRules) + def globalForwardingRulesList = Mock(Compute.GlobalForwardingRules.List) + + googleLoadBalancerProviderMock.getApplicationLoadBalancers("") >> loadBalancerList + def credentials = new GoogleNamedAccountCredentials.Builder().project(PROJECT_NAME).compute(computeMock).build() + def bs = isRegional ? + new BackendService(backends: lbNames.collect { + new Backend(group: GCEUtil.buildZonalServerGroupUrl(PROJECT_NAME, ZONE, serverGroup.name)) + }) : + new BackendService(backends: lbNames.collect { + new Backend(group: GCEUtil.buildRegionalServerGroupUrl(PROJECT_NAME, REGION, serverGroup.name)) + }) + + def description = new DestroyGoogleServerGroupDescription(serverGroupName: SERVER_GROUP_NAME, + region: REGION, + accountName: ACCOUNT_NAME, + credentials: credentials) + @Subject def operation = new DestroyGoogleServerGroupAtomicOperation(description) + + def task = Mock(Task) + def googleOperationPoller = Mock(GoogleOperationPoller) + operation.googleOperationPoller = googleOperationPoller + def updateOpName = 'updateOp' + + operation.registry = registry + operation.safeRetry = safeRetry + operation.googleClusterProvider = googleClusterProviderMock + operation.googleLoadBalancerProvider = googleLoadBalancerProviderMock when: - def closure = operation.destroyInternalLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock) - closure() + def closure = operation.destroyInternalLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock) + closure() then: - _ * computeMock.regionBackendServices() >> backendServicesMock - _ * backendServicesMock.get(PROJECT_NAME, REGION, 'backend-service') >> backendSvcGetMock - _ * backendSvcGetMock.execute() >> bs - _ * backendServicesMock.update(PROJECT_NAME, REGION, 'backend-service', bs) >> backendUpdateMock - _ * backendUpdateMock.execute() >> [name: updateOpName] - _ * googleOperationPoller.waitForRegionalOperation(computeMock, PROJECT_NAME, REGION, updateOpName, null, task, _, _) - - _ * computeMock.globalForwardingRules() >> globalForwardingRules - _ * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList - _ * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) - - _ * computeMock.forwardingRules() >> forwardingRules - _ * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList - _ * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) - bs.backends.size == 0 + _ * computeMock.regionBackendServices() >> backendServicesMock + _ * backendServicesMock.get(PROJECT_NAME, REGION, 'backend-service') >> backendSvcGetMock + _ * backendSvcGetMock.execute() >> bs + _ * backendServicesMock.update(PROJECT_NAME, REGION, 'backend-service', bs) >> backendUpdateMock + _ * backendUpdateMock.execute() >> [name: updateOpName] + _ * googleOperationPoller.waitForRegionalOperation(computeMock, PROJECT_NAME, REGION, updateOpName, null, task, _, _) + + _ * computeMock.globalForwardingRules() >> globalForwardingRules + _ * globalForwardingRules.list(PROJECT_NAME) >> globalForwardingRulesList + _ * globalForwardingRulesList.execute() >> new ForwardingRuleList(items: []) + + _ * computeMock.forwardingRules() >> forwardingRules + _ * forwardingRules.list(PROJECT_NAME, _) >> forwardingRulesList + _ * forwardingRulesList.execute() >> new ForwardingRuleList(items: []) + bs.backends.size == 0 where: - isRegional | location | loadBalancerList | lbNames - false | ZONE | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] - true | REGION | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] - false | ZONE | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] - true | REGION | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] - false | ZONE | [] | [] - true | REGION | [] | [] + isRegional | location | loadBalancerList | lbNames + false | ZONE | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] + true | REGION | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] + false | ZONE | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] + true | REGION | [new GoogleInternalLoadBalancer(name: 'spinnaker-int-load-balancer', backendService: new GoogleBackendService(name: 'backend-service')).view] | ['spinnaker-int-load-balancer'] + false | ZONE | [] | [] + true | REGION | [] | [] } void "should retry http backend deletion on 400, 412, socket timeout, succeed on 404"() { // Note: Implicitly tests SafeRetry.doRetry setup: - def registry = new DefaultRegistry() - def computeMock = Mock(Compute) - def backendServicesMock = Mock(Compute.BackendServices) - def backendSvcGetMock = Mock(Compute.BackendServices.Get) - def backendUpdateMock = Mock(Compute.BackendServices.Update) - def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) - googleLoadBalancerProviderMock.getApplicationLoadBalancers("") >> loadBalancerList - - def serverGroup = - new GoogleServerGroup( - name: SERVER_GROUP_NAME, - region: REGION, - regional: isRegional, - zone: ZONE, - asg: [ - (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES): lbNames, - ], - launchConfig: [ - instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME, - properties: [ - 'metadata': new Metadata(items: [ - new Metadata.Items( - key: (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES), - value: 'spinnaker-http-load-balancer' - ), - new Metadata.Items( - key: (GCEUtil.BACKEND_SERVICE_NAMES), - value: 'backend-service' - ) - ]) - ]) - ]).view - - def errorMessage = "The resource 'my-backend-service' is not ready" - def errorInfo = new GoogleJsonError.ErrorInfo( - domain: "global", - message: errorMessage, - reason: "resourceNotReady") - def details = new GoogleJsonError( - code: 400, - errors: [errorInfo], - message: errorMessage) - def httpResponseExceptionBuilder = new HttpResponseException.Builder( - 400, - "Bad Request", - new HttpHeaders()).setMessage("400 Bad Request") - def googleJsonResponseException = new GoogleJsonResponseException(httpResponseExceptionBuilder, details) - - errorMessage = "Invalid fingerprint." - errorInfo = new GoogleJsonError.ErrorInfo( - domain: "global", - message: errorMessage, - reason: "conditionNotMet") - details = new GoogleJsonError( - code: 412, - errors: [errorInfo], - message: errorMessage) - httpResponseExceptionBuilder = new HttpResponseException.Builder( - 412, - "Precondition Failed", - new HttpHeaders()).setMessage("412 Precondition Failed") - def fingerPrintException = new GoogleJsonResponseException(httpResponseExceptionBuilder, details) - - errorMessage = "Resource 'stuff' could not be located" - errorInfo = new GoogleJsonError.ErrorInfo( - domain: "global", - message: errorMessage, - reason: "stuffNotFound") - details = new GoogleJsonError( - code: 404, - errors: [errorInfo], - message: errorMessage) - httpResponseExceptionBuilder = new HttpResponseException.Builder( - 404, - "Not Found", - new HttpHeaders()).setMessage("404 Not Found") - def notFoundException = new GoogleJsonResponseException(httpResponseExceptionBuilder, details) - - def socketTimeoutException = new SocketTimeoutException("Read timed out") - - def bs = isRegional ? - new BackendService(backends: lbNames.collect { new Backend(group: GCEUtil.buildZonalServerGroupUrl(PROJECT_NAME, ZONE, serverGroup.name)) }) : - new BackendService(backends: lbNames.collect { new Backend(group: GCEUtil.buildRegionalServerGroupUrl(PROJECT_NAME, REGION, serverGroup.name)) }) - def updateOpName = 'updateOp' - def task = Mock(Task) - def googleOperationPoller = Mock(GoogleOperationPoller) + def registry = new DefaultRegistry() + def computeMock = Mock(Compute) + def backendServicesMock = Mock(Compute.BackendServices) + def backendSvcGetMock = Mock(Compute.BackendServices.Get) + def backendUpdateMock = Mock(Compute.BackendServices.Update) + def googleLoadBalancerProviderMock = Mock(GoogleLoadBalancerProvider) + googleLoadBalancerProviderMock.getApplicationLoadBalancers("") >> loadBalancerList + + def serverGroup = + new GoogleServerGroup( + name: SERVER_GROUP_NAME, + region: REGION, + regional: isRegional, + zone: ZONE, + asg: [ + (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES): lbNames, + ], + launchConfig: [ + instanceTemplate: new InstanceTemplate(name: INSTANCE_TEMPLATE_NAME, + properties: [ + 'metadata': new Metadata(items: [ + new Metadata.Items( + key: (GCEUtil.GLOBAL_LOAD_BALANCER_NAMES), + value: 'spinnaker-http-load-balancer' + ), + new Metadata.Items( + key: (GCEUtil.BACKEND_SERVICE_NAMES), + value: 'backend-service' + ) + ]) + ]) + ]).view + + def errorMessage = "The resource 'my-backend-service' is not ready" + def errorInfo = new GoogleJsonError.ErrorInfo( + domain: "global", + message: errorMessage, + reason: "resourceNotReady") + def details = new GoogleJsonError( + code: 400, + errors: [errorInfo], + message: errorMessage) + def httpResponseExceptionBuilder = new HttpResponseException.Builder( + 400, + "Bad Request", + new HttpHeaders()).setMessage("400 Bad Request") + def googleJsonResponseException = new GoogleJsonResponseException(httpResponseExceptionBuilder, details) + + errorMessage = "Invalid fingerprint." + errorInfo = new GoogleJsonError.ErrorInfo( + domain: "global", + message: errorMessage, + reason: "conditionNotMet") + details = new GoogleJsonError( + code: 412, + errors: [errorInfo], + message: errorMessage) + httpResponseExceptionBuilder = new HttpResponseException.Builder( + 412, + "Precondition Failed", + new HttpHeaders()).setMessage("412 Precondition Failed") + def fingerPrintException = new GoogleJsonResponseException(httpResponseExceptionBuilder, details) + + errorMessage = "Resource 'stuff' could not be located" + errorInfo = new GoogleJsonError.ErrorInfo( + domain: "global", + message: errorMessage, + reason: "stuffNotFound") + details = new GoogleJsonError( + code: 404, + errors: [errorInfo], + message: errorMessage) + httpResponseExceptionBuilder = new HttpResponseException.Builder( + 404, + "Not Found", + new HttpHeaders()).setMessage("404 Not Found") + def notFoundException = new GoogleJsonResponseException(httpResponseExceptionBuilder, details) + + def socketTimeoutException = new SocketTimeoutException("Read timed out") + + def bs = isRegional ? + new BackendService(backends: lbNames.collect { + new Backend(group: GCEUtil.buildZonalServerGroupUrl(PROJECT_NAME, ZONE, serverGroup.name)) + }) : + new BackendService(backends: lbNames.collect { + new Backend(group: GCEUtil.buildRegionalServerGroupUrl(PROJECT_NAME, REGION, serverGroup.name)) + }) + def updateOpName = 'updateOp' + def task = Mock(Task) + def googleOperationPoller = Mock(GoogleOperationPoller) when: - def destroy = new DestroyGoogleServerGroupAtomicOperation() + def destroy = new DestroyGoogleServerGroupAtomicOperation() - destroy.googleOperationPoller = googleOperationPoller + destroy.googleOperationPoller = googleOperationPoller - destroy.registry = registry - destroy.safeRetry = safeRetry - destroy.destroy( - destroy.destroyHttpLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock), - "Http load balancer backends", [action: 'test'] - ) + destroy.registry = registry + destroy.safeRetry = safeRetry + destroy.destroy( + destroy.destroyHttpLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock), + "Http load balancer backends", [action: 'test'] + ) then: - 1 * backendUpdateMock.execute() >> { throw googleJsonResponseException } - 2 * computeMock.backendServices() >> backendServicesMock - 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock - 1 * backendSvcGetMock.execute() >> bs - 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock + 1 * backendUpdateMock.execute() >> { throw googleJsonResponseException } + 2 * computeMock.backendServices() >> backendServicesMock + 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock + 1 * backendSvcGetMock.execute() >> bs + 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock then: - 1 * backendUpdateMock.execute() >> { throw fingerPrintException } - 2 * computeMock.backendServices() >> backendServicesMock - 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock - 1 * backendSvcGetMock.execute() >> bs - 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock + 1 * backendUpdateMock.execute() >> { throw fingerPrintException } + 2 * computeMock.backendServices() >> backendServicesMock + 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock + 1 * backendSvcGetMock.execute() >> bs + 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock then: - 1 * backendUpdateMock.execute() >> { throw socketTimeoutException } - 2 * computeMock.backendServices() >> backendServicesMock - 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock - 1 * backendSvcGetMock.execute() >> bs - 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock + 1 * backendUpdateMock.execute() >> { throw socketTimeoutException } + 2 * computeMock.backendServices() >> backendServicesMock + 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock + 1 * backendSvcGetMock.execute() >> bs + 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock then: - 1 * backendUpdateMock.execute() >> [name: updateOpName] - 2 * computeMock.backendServices() >> backendServicesMock - 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock - 1 * backendSvcGetMock.execute() >> bs - 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock - _ * googleOperationPoller.waitForGlobalOperation(computeMock, PROJECT_NAME, updateOpName, null, task, _, _) + 1 * backendUpdateMock.execute() >> [name: updateOpName] + 2 * computeMock.backendServices() >> backendServicesMock + 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock + 1 * backendSvcGetMock.execute() >> bs + 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock + _ * googleOperationPoller.waitForGlobalOperation(computeMock, PROJECT_NAME, updateOpName, null, task, _, _) when: - destroy.destroy( - destroy.destroyHttpLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock), - "Http load balancer backends", [action: 'test'] - ) + destroy.destroy( + destroy.destroyHttpLoadBalancerBackends(computeMock, PROJECT_NAME, serverGroup, googleLoadBalancerProviderMock), + "Http load balancer backends", [action: 'test'] + ) then: - 1 * backendUpdateMock.execute() >> { throw notFoundException } - 2 * computeMock.backendServices() >> backendServicesMock - 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock - 1 * backendSvcGetMock.execute() >> bs - 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock + 1 * backendUpdateMock.execute() >> { throw notFoundException } + 2 * computeMock.backendServices() >> backendServicesMock + 1 * backendServicesMock.get(PROJECT_NAME, 'backend-service') >> backendSvcGetMock + 1 * backendSvcGetMock.execute() >> bs + 1 * backendServicesMock.update(PROJECT_NAME, 'backend-service', bs) >> backendUpdateMock where: - isRegional | location | loadBalancerList | lbNames - false | ZONE | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] + isRegional | location | loadBalancerList | lbNames + false | ZONE | [new GoogleHttpLoadBalancer(name: 'spinnaker-http-load-balancer').view] | ['spinnaker-http-load-balancer'] } } diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperationUnitSpec.groovy b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperationUnitSpec.groovy index 81c028e1e27..e70cb71b637 100644 --- a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperationUnitSpec.groovy +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperationUnitSpec.groovy @@ -1,12 +1,31 @@ +/* + * Copyright 2019 Google, 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.deploy.ops + import com.google.api.services.compute.model.InstanceGroupManager +import com.google.api.services.compute.model.Operation import com.netflix.spinnaker.clouddriver.data.task.Task import com.netflix.spinnaker.clouddriver.data.task.TaskRepository -import com.netflix.spinnaker.clouddriver.google.compute.GoogleComputeOperationRequest -import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription -import com.netflix.spinnaker.clouddriver.google.compute.GoogleServerGroupManagers +import com.netflix.spinnaker.clouddriver.google.compute.FakeGoogleComputeOperationRequest +import com.netflix.spinnaker.clouddriver.google.compute.FakeGoogleComputeRequest import com.netflix.spinnaker.clouddriver.google.compute.GoogleComputeApiFactory +import com.netflix.spinnaker.clouddriver.google.compute.GoogleServerGroupManagers +import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider import com.netflix.spinnaker.clouddriver.google.security.FakeGoogleCredentials @@ -53,8 +72,8 @@ class SetStatefulDiskAtomicOperationUnitSpec extends Specification { deviceName: DEVICE_NAME, credentials: CREDENTIALS) def operation = new SetStatefulDiskAtomicOperation(clusterProvider, computeApiFactory, description) - def updateOp = Mock(GoogleComputeOperationRequest) - def getManagerRequest = { new InstanceGroupManager() } + def updateOp = new FakeGoogleComputeOperationRequest<>(new Operation()) + def getManagerRequest = new FakeGoogleComputeRequest<>(new InstanceGroupManager()) _ * serverGroupManagers.get() >> getManagerRequest when: @@ -64,6 +83,7 @@ class SetStatefulDiskAtomicOperationUnitSpec extends Specification { 1 * serverGroupManagers.update({ it.getStatefulPolicy().getPreservedState().getDisks().containsKey(DEVICE_NAME) }) >> updateOp - 1 * updateOp.executeAndWait(task, /* phase= */ _) + + assert updateOp.waitedForCompletion() } } diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidatorTest.java b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidatorTest.java index b31e42e29d3..955b6f0f444 100644 --- a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidatorTest.java +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidatorTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google, 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.deploy.validators; import static org.assertj.core.api.Java6Assertions.assertThat;