feature(google): Add a SetStatefulDisk operation.
MODIFY
clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/orchestration/AtomicOperations.java
clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/orchestration/AtomicOperations.java
@@ -37,6 +37,7 @@ public final class AtomicOperations {
public static final String MIGRATE_CLUSTER_CONFIGURATIONS = "migrateClusterConfigurations";
public static final String START_SERVER_GROUP = "startServerGroup";
public static final String STOP_SERVER_GROUP = "stopServerGroup";
+ public static final String SET_STATEFUL_DISK = "setStatefulDisk";
// Instance operations
public static final String REBOOT_INSTANCES = "rebootInstances";

MODIFY
clouddriver-google/clouddriver-google.gradle
clouddriver-google/clouddriver-google.gradle
@@ -4,6 +4,7 @@ dependencies {
compile project(":clouddriver-consul")
compile project(":clouddriver-google-common")
+ compile spinnaker.dependency('assertj')
compile spinnaker.dependency('bootActuator')
compile spinnaker.dependency('bootWeb')
compile spinnaker.dependency('frigga')

MODIFY
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/GoogleAtomicOperationConverterHelper.groovy
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/GoogleAtomicOperationConverterHelper.groovy
package com.netflix.spinnaker.clouddriver.google.deploy.converters
import com.fasterxml.jackson.databind.DeserializationFeature
-import com.netflix.spinnaker.clouddriver.google.deploy.description.AbstractGoogleCredentialsDescription
import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials
import com.netflix.spinnaker.clouddriver.security.AbstractAtomicOperationsCredentialsSupport
+import com.netflix.spinnaker.clouddriver.security.resources.CredentialsNameable
class GoogleAtomicOperationConverterHelper {
- static <T extends AbstractGoogleCredentialsDescription> T convertDescription(Map input,
+ static <T extends CredentialsNameable> T convertDescription(Map input,
AbstractAtomicOperationsCredentialsSupport credentialsSupport,
Class<T> targetDescriptionType) {
if (!input.accountName) {

ADD
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverter.java
+package com.netflix.spinnaker.clouddriver.google.deploy.converters;
+
+import com.netflix.spinnaker.clouddriver.google.GoogleOperation;
+import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription;
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupManagersFactory;
+import com.netflix.spinnaker.clouddriver.google.deploy.ops.SetStatefulDiskAtomicOperation;
+import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider;
+import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations;
+import com.netflix.spinnaker.clouddriver.security.AbstractAtomicOperationsCredentialsSupport;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@GoogleOperation(AtomicOperations.SET_STATEFUL_DISK)
+@Component
+public class SetStatefulDiskAtomicOperationConverter
+ extends AbstractAtomicOperationsCredentialsSupport {
+
+ private final GoogleClusterProvider clusterProvider;
+ private final GoogleServerGroupManagersFactory serverGroupManagersFactory;
+
+ @Autowired
+ public SetStatefulDiskAtomicOperationConverter(
+ GoogleClusterProvider clusterProvider,
+ GoogleServerGroupManagersFactory serverGroupManagersFactory) {
+ this.clusterProvider = clusterProvider;
+ this.serverGroupManagersFactory = serverGroupManagersFactory;
+ }
+
+ @Override
+ public SetStatefulDiskAtomicOperation convertOperation(Map input) {
+ return new SetStatefulDiskAtomicOperation(
+ clusterProvider, serverGroupManagersFactory, convertDescription(input));
+ }
+
+ @Override
+ public SetStatefulDiskDescription convertDescription(Map input) {
+ return GoogleAtomicOperationConverterHelper.convertDescription(
+ input, this, SetStatefulDiskDescription.class);
+ }
+}

ADD
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/description/SetStatefulDiskDescription.java
+package com.netflix.spinnaker.clouddriver.google.deploy.description;
+
+import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials;
+import com.netflix.spinnaker.clouddriver.security.resources.CredentialsNameable;
+import com.netflix.spinnaker.clouddriver.security.resources.ServerGroupNameable;
+import lombok.Data;
+
+@Data
+public class SetStatefulDiskDescription implements CredentialsNameable, ServerGroupNameable {
+
+ private GoogleNamedAccountCredentials credentials;
+ private String serverGroupName;
+ private String region;
+ private String deviceName;
+}

MODIFY
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/AbstractGoogleServerGroupManagers.java
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/AbstractGoogleServerGroupManagers.java
@@ -2,6 +2,7 @@ package com.netflix.spinnaker.clouddriver.google.deploy.instancegroups;
import com.google.api.client.googleapis.services.AbstractGoogleClientRequest;
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.google.common.collect.ImmutableList;
import com.netflix.spectator.api.Registry;
@@ -39,6 +40,20 @@ public abstract class AbstractGoogleServerGroupManagers implements GoogleServerG
abstract ComputeRequest<Operation> performDelete() throws IOException;
+ @Override
+ public InstanceGroupManager get() throws IOException {
+ return timeExecute(performGet(), "get");
+ }
+
+ abstract ComputeRequest<InstanceGroupManager> performGet() throws IOException;
+
+ @Override
+ public Operation update(InstanceGroupManager content) throws IOException {
+ return timeExecute(performUpdate(content), "update");
+ }
+
+ abstract ComputeRequest<Operation> performUpdate(InstanceGroupManager content) throws IOException;
+
private <T> T timeExecute(AbstractGoogleClientRequest<T> request, String api) throws IOException {
return GoogleExecutor.timeExecute(
registry,

MODIFY
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/GoogleServerGroupManagers.java
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/GoogleServerGroupManagers.java
@@ -2,6 +2,7 @@ package com.netflix.spinnaker.clouddriver.google.deploy.instancegroups;
import com.google.api.services.compute.Compute.InstanceGroupManagers;
import com.google.api.services.compute.Compute.RegionInstanceGroupManagers;
+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;
@@ -17,5 +18,9 @@ public interface GoogleServerGroupManagers {
Operation delete() throws IOException;
+ InstanceGroupManager get() throws IOException;
+
+ Operation update(InstanceGroupManager content) throws IOException;
+
GoogleServerGroupOperationPoller getOperationPoller();
}

MODIFY
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/RegionGoogleServerGroupManagers.java
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/RegionGoogleServerGroupManagers.java
@@ -2,6 +2,7 @@ package com.netflix.spinnaker.clouddriver.google.deploy.instancegroups;
import com.google.api.services.compute.Compute;
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.google.api.services.compute.model.RegionInstanceGroupManagersAbandonInstancesRequest;
import com.google.common.collect.ImmutableList;
@@ -45,6 +46,16 @@ class RegionGoogleServerGroupManagers extends AbstractGoogleServerGroupManagers
return managers.delete(getProject(), region, getInstanceGroupName());
}
+ @Override
+ ComputeRequest<InstanceGroupManager> performGet() throws IOException {
+ return managers.get(getProject(), region, getInstanceGroupName());
+ }
+
+ @Override
+ ComputeRequest<Operation> performUpdate(InstanceGroupManager content) throws IOException {
+ return managers.update(getProject(), region, getInstanceGroupName(), content);
+ }
+
@Override
public GoogleServerGroupOperationPoller getOperationPoller() {
return operationPoller;

MODIFY
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/ZoneGoogleServerGroupManagers.java
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/instancegroups/ZoneGoogleServerGroupManagers.java
@@ -2,6 +2,7 @@ package com.netflix.spinnaker.clouddriver.google.deploy.instancegroups;
import com.google.api.services.compute.Compute;
import com.google.api.services.compute.ComputeRequest;
+import com.google.api.services.compute.model.InstanceGroupManager;
import com.google.api.services.compute.model.InstanceGroupManagersAbandonInstancesRequest;
import com.google.api.services.compute.model.Operation;
import com.google.common.collect.ImmutableList;
@@ -45,6 +46,16 @@ class ZoneGoogleServerGroupManagers extends AbstractGoogleServerGroupManagers {
return managers.delete(getProject(), zone, getInstanceGroupName());
}
+ @Override
+ ComputeRequest<InstanceGroupManager> performGet() throws IOException {
+ return managers.get(getProject(), zone, getInstanceGroupName());
+ }
+
+ @Override
+ ComputeRequest<Operation> performUpdate(InstanceGroupManager content) throws IOException {
+ return managers.update(getProject(), zone, getInstanceGroupName(), content);
+ }
+
@Override
public GoogleServerGroupOperationPoller getOperationPoller() {
return operationPoller;

ADD
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperation.java
+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.google.api.services.compute.model.StatefulPolicy;
+import com.google.api.services.compute.model.StatefulPolicyPreservedState;
+import com.google.api.services.compute.model.StatefulPolicyPreservedStateDiskDevice;
+import com.google.common.annotations.VisibleForTesting;
+import com.netflix.spinnaker.clouddriver.data.task.Task;
+import com.netflix.spinnaker.clouddriver.data.task.TaskRepository;
+import com.netflix.spinnaker.clouddriver.google.deploy.GCEUtil;
+import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription;
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupManagers;
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupManagersFactory;
+import com.netflix.spinnaker.clouddriver.google.model.GoogleServerGroup;
+import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.HashMap;
+import java.util.List;
+
+public class SetStatefulDiskAtomicOperation extends GoogleAtomicOperation<Void> {
+
+ private static final String BASE_PHASE = "SET_STATEFUL_DISK";
+
+ private final GoogleClusterProvider clusterProvider;
+ private final GoogleServerGroupManagersFactory serverGroupManagersFactory;
+ private final SetStatefulDiskDescription description;
+
+ public SetStatefulDiskAtomicOperation(
+ GoogleClusterProvider clusterProvider,
+ GoogleServerGroupManagersFactory serverGroupManagersFactory,
+ SetStatefulDiskDescription description) {
+ this.clusterProvider = clusterProvider;
+ this.serverGroupManagersFactory = serverGroupManagersFactory;
+ this.description = description;
+ }
+
+ /*
+ curl -X POST -H "Content-Type: application/json" -d '
+ [ { "setStatefulDisk": {
+ "serverGroupName": "myapp-dev-v000",
+ "region": "us-east1",
+ "device-name": "myapp-dev-v000-1",
+ "credentials": "my-account-name"
+ } } ]' localhost:7002/gce/ops
+ */
+ @Override
+ public Void operate(List priorOutputs) {
+
+ Task task = TaskRepository.threadLocalTask.get();
+
+ task.updateStatus(
+ BASE_PHASE,
+ String.format(
+ "Initializing set stateful disk of instance group %s in region %s",
+ description.getServerGroupName(), description.getRegion()));
+
+ GoogleServerGroup.View serverGroup =
+ GCEUtil.queryServerGroup(
+ clusterProvider,
+ description.getAccount(),
+ description.getRegion(),
+ description.getServerGroupName());
+
+ try {
+ GoogleServerGroupManagers managers =
+ serverGroupManagersFactory.getManagers(description.getCredentials(), serverGroup);
+
+ task.updateStatus(BASE_PHASE, "Retrieving current instance group definition");
+
+ InstanceGroupManager instanceGroupManager = managers.get();
+
+ task.updateStatus(BASE_PHASE, "Retrieving associated instance template definition");
+
+ setStatefulPolicy(instanceGroupManager);
+
+ task.updateStatus(BASE_PHASE, "Storing updated instance group definition");
+
+ Operation operation = managers.update(instanceGroupManager);
+
+ task.updateStatus(BASE_PHASE, "Waiting for update to complete");
+
+ managers
+ .getOperationPoller()
+ .waitForOperation(
+ operation, /* timeout= */ null, TaskRepository.threadLocalTask.get(), BASE_PHASE);
+
+ return null;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private void setStatefulPolicy(InstanceGroupManager instanceGroupManager) {
+
+ if (instanceGroupManager.getStatefulPolicy() == null) {
+ instanceGroupManager.setStatefulPolicy(new StatefulPolicy());
+ }
+ StatefulPolicy statefulPolicy = instanceGroupManager.getStatefulPolicy();
+ if (statefulPolicy.getPreservedState() == null) {
+ statefulPolicy.setPreservedState(new StatefulPolicyPreservedState());
+ }
+ StatefulPolicyPreservedState preservedState = statefulPolicy.getPreservedState();
+ if (preservedState.getDisks() == null) {
+ preservedState.setDisks(new HashMap<>());
+ }
+ preservedState
+ .getDisks()
+ .put(description.getDeviceName(), new StatefulPolicyPreservedStateDiskDevice());
+ }
+
+ @VisibleForTesting
+ public SetStatefulDiskDescription getDescription() {
+ return description;
+ }
+}

ADD
clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidator.java
+package com.netflix.spinnaker.clouddriver.google.deploy.validators;
+
+import com.netflix.spinnaker.clouddriver.deploy.DescriptionValidator;
+import com.netflix.spinnaker.clouddriver.google.GoogleOperation;
+import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription;
+import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations;
+import java.util.List;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.Errors;
+
+@GoogleOperation(AtomicOperations.SET_STATEFUL_DISK)
+@Component
+public class SetStatefulDiskDescriptionValidator
+ extends DescriptionValidator<SetStatefulDiskDescription> {
+
+ @Override
+ public void validate(
+ List priorDescriptions, SetStatefulDiskDescription description, Errors errors) {
+ StandardGceAttributeValidator helper =
+ new StandardGceAttributeValidator("setStatefulDiskDescription", errors);
+ helper.validateRegion(description.getRegion(), description.getCredentials());
+ helper.validateServerGroupName(description.getServerGroupName());
+ helper.validateName(description.getDeviceName(), "deviceName");
+ }
+}

ADD
clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/converters/SetStatefulDiskAtomicOperationConverterTest.java
+package com.netflix.spinnaker.clouddriver.google.deploy.converters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription;
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupManagersFactory;
+import com.netflix.spinnaker.clouddriver.google.deploy.ops.SetStatefulDiskAtomicOperation;
+import com.netflix.spinnaker.clouddriver.google.provider.view.GoogleClusterProvider;
+import com.netflix.spinnaker.clouddriver.google.security.FakeGoogleCredentials;
+import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials;
+import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SetStatefulDiskAtomicOperationConverterTest {
+
+ private static final String ACCOUNT_NAME = "spinnaker-account";
+ private static final String SERVER_GROUP_NAME = "spinnaker-test-v000";
+ private static final String REGION = "us-central1";
+ private static final String DEVICE_NAME = "spinnaker-test-v000-001";
+
+ SetStatefulDiskAtomicOperationConverter converter;
+
+ @Before
+ public void setUp() {
+ GoogleClusterProvider clusterProvider = mock(GoogleClusterProvider.class);
+ GoogleServerGroupManagersFactory serverGroupManagersFactory =
+ mock(GoogleServerGroupManagersFactory.class);
+ converter =
+ new SetStatefulDiskAtomicOperationConverter(clusterProvider, serverGroupManagersFactory);
+
+ AccountCredentialsProvider accountCredentialsProvider = mock(AccountCredentialsProvider.class);
+ GoogleNamedAccountCredentials accountCredentials =
+ new GoogleNamedAccountCredentials.Builder()
+ .name(ACCOUNT_NAME)
+ .credentials(new FakeGoogleCredentials())
+ .build();
+ when(accountCredentialsProvider.getCredentials(any())).thenReturn(accountCredentials);
+ converter.setAccountCredentialsProvider(accountCredentialsProvider);
+ converter.setObjectMapper(new ObjectMapper());
+ }
+
+ @Test
+ public void testConvertDescription() {
+ Map<String, String> input = new HashMap<>();
+ input.put("accountName", ACCOUNT_NAME);
+ input.put("serverGroupName", SERVER_GROUP_NAME);
+ input.put("region", REGION);
+ input.put("deviceName", DEVICE_NAME);
+ SetStatefulDiskDescription description = converter.convertDescription(input);
+
+ assertThat(description.getAccount()).isEqualTo(ACCOUNT_NAME);
+ assertThat(description.getServerGroupName()).isEqualTo(SERVER_GROUP_NAME);
+ assertThat(description.getRegion()).isEqualTo(REGION);
+ assertThat(description.getDeviceName()).isEqualTo(DEVICE_NAME);
+ }
+
+ @Test
+ public void testConvertOperation() {
+ Map<String, String> input = new HashMap<>();
+ input.put("accountName", ACCOUNT_NAME);
+ input.put("serverGroupName", SERVER_GROUP_NAME);
+ input.put("region", REGION);
+ input.put("deviceName", DEVICE_NAME);
+ SetStatefulDiskAtomicOperation operation = converter.convertOperation(input);
+
+ SetStatefulDiskDescription description = operation.getDescription();
+ assertThat(description.getAccount()).isEqualTo(ACCOUNT_NAME);
+ assertThat(description.getServerGroupName()).isEqualTo(SERVER_GROUP_NAME);
+ assertThat(description.getRegion()).isEqualTo(REGION);
+ assertThat(description.getDeviceName()).isEqualTo(DEVICE_NAME);
+ }
+}

ADD
clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/SetStatefulDiskAtomicOperationUnitSpec.groovy
+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.deploy.description.SetStatefulDiskDescription
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupManagers
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupManagersFactory
+import com.netflix.spinnaker.clouddriver.google.deploy.instancegroups.GoogleServerGroupOperationPoller
+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
+import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials
+import spock.lang.Specification
+
+class SetStatefulDiskAtomicOperationUnitSpec extends Specification {
+
+ private static final String SERVER_GROUP = "testapp-v000"
+ private static final String REGION = "us-central1"
+ private static final String DEVICE_NAME = "testapp-v000-001"
+ private static final GoogleNamedAccountCredentials CREDENTIALS =
+ new GoogleNamedAccountCredentials.Builder()
+ .name("spinnaker-account")
+ .credentials(new FakeGoogleCredentials())
+ .build()
+
+ Task task
+ GoogleClusterProvider clusterProvider
+ GoogleServerGroupManagersFactory serverGroupManagersFactory
+ GoogleServerGroupManagers serverGroupManagers
+ GoogleNamedAccountCredentials credentials
+ GoogleServerGroupOperationPoller poller
+
+ def setup() {
+ task = Mock(Task)
+ TaskRepository.threadLocalTask.set(task)
+
+ poller = Mock(GoogleServerGroupOperationPoller)
+
+ serverGroupManagers = Mock(GoogleServerGroupManagers) {
+ _ * getOperationPoller() >> poller
+ }
+
+ serverGroupManagersFactory = Mock(GoogleServerGroupManagersFactory) {
+ _ * getManagers(*_) >> serverGroupManagers
+ }
+
+ clusterProvider = Mock(GoogleClusterProvider) {
+ _ * getServerGroup(*_) >> new GoogleServerGroup(name: SERVER_GROUP).view
+ }
+ }
+
+ void "sets stateful policy on instance group"() {
+ setup:
+ def description = new SetStatefulDiskDescription(
+ serverGroupName: SERVER_GROUP,
+ region: REGION,
+ deviceName: DEVICE_NAME,
+ credentials: CREDENTIALS)
+ def operation = new SetStatefulDiskAtomicOperation(clusterProvider, serverGroupManagersFactory, description)
+ def updateOp = new Operation(name: "xyzzy", status: "DONE")
+ _ * serverGroupManagers.get() >> new InstanceGroupManager()
+
+ when:
+ operation.operate([])
+
+ then:
+ 1 * serverGroupManagers.update({
+ it.getStatefulPolicy().getPreservedState().getDisks().containsKey(DEVICE_NAME)
+ }) >> updateOp
+ 1 * poller.waitForOperation(updateOp, /* timeout= */ null, task, /* phase= */ _)
+ }
+}

ADD
clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/deploy/validators/SetStatefulDiskDescriptionValidatorTest.java
+package com.netflix.spinnaker.clouddriver.google.deploy.validators;
+
+import static org.assertj.core.api.Java6Assertions.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.netflix.spinnaker.clouddriver.google.deploy.description.SetStatefulDiskDescription;
+import com.netflix.spinnaker.clouddriver.google.security.FakeGoogleCredentials;
+import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials;
+import org.assertj.core.api.Condition;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.validation.BeanPropertyBindingResult;
+import org.springframework.validation.Errors;
+import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
+
+@RunWith(JUnit4.class)
+public class SetStatefulDiskDescriptionValidatorTest {
+
+ private static final String ACCOUNT_NAME = "spintest";
+ private static final String REGION = "us-central1";
+ private GoogleNamedAccountCredentials CREDENTIALS =
+ new GoogleNamedAccountCredentials.Builder()
+ .name(ACCOUNT_NAME)
+ .credentials(new FakeGoogleCredentials())
+ .regionToZonesMap(ImmutableMap.of(REGION, ImmutableList.of("us-central1-b")))
+ .build();
+
+ private SetStatefulDiskDescriptionValidator validator;
+
+ @Before
+ public void setUp() {
+ validator = new SetStatefulDiskDescriptionValidator();
+ }
+
+ @Test
+ public void testNoErrors() {
+ SetStatefulDiskDescription description = new SetStatefulDiskDescription();
+ description.setCredentials(CREDENTIALS);
+ description.setRegion(REGION);
+ description.setServerGroupName("testapp-v000");
+ description.setDeviceName("testapp-v000-1");
+
+ Errors errors = new BeanPropertyBindingResult(description, "description");
+
+ validator.validate(ImmutableList.of(), description, errors);
+
+ assertThat(errors.hasErrors()).isFalse();
+ }
+
+ @Test
+ public void testNoFields() {
+ SetStatefulDiskDescription description = new SetStatefulDiskDescription();
+
+ Errors errors = new BeanPropertyBindingResult(description, "description");
+
+ validator.validate(ImmutableList.of(), description, errors);
+
+ assertThat(errors.getAllErrors()).hasSize(3);
+ assertThat(errors.getAllErrors()).haveAtLeastOne(errorOnField("region"));
+ assertThat(errors.getAllErrors()).haveAtLeastOne(errorOnField("serverGroupName"));
+ assertThat(errors.getAllErrors()).haveAtLeastOne(errorOnField("deviceName"));
+ }
+
+ @Test
+ public void testInvalidRegion() {
+ SetStatefulDiskDescription description = new SetStatefulDiskDescription();
+ description.setCredentials(CREDENTIALS);
+ description.setRegion("some-unknown-region");
+ description.setServerGroupName("testapp-v000");
+ description.setDeviceName("testapp-v000-1");
+
+ Errors errors = new BeanPropertyBindingResult(description, "description");
+
+ validator.validate(ImmutableList.of(), description, errors);
+
+ assertThat(errors.getAllErrors()).hasSize(1);
+ assertThat(errors.getAllErrors()).haveAtLeastOne(errorOnField("region"));
+ }
+
+ private Condition<ObjectError> errorOnField(String field) {
+ return new Condition<>(
+ e -> e instanceof FieldError && ((FieldError) e).getField().equals(field),
+ "has field named " + field);
+ }
+}

feature(google): Add a SetStatefulDisk operation. (#3598)