diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/converters/DeleteCloudFormationChangeSetAtomicOperationConverter.java b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/converters/DeleteCloudFormationChangeSetAtomicOperationConverter.java new file mode 100644 index 00000000000..56759be688a --- /dev/null +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/converters/DeleteCloudFormationChangeSetAtomicOperationConverter.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Adevinta + * + * 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.aws.deploy.converters; + +import com.netflix.spinnaker.clouddriver.aws.AmazonOperation; +import com.netflix.spinnaker.clouddriver.aws.deploy.description.DeleteCloudFormationChangeSetDescription; +import com.netflix.spinnaker.clouddriver.aws.deploy.ops.DeleteCloudFormationChangeSetAtomicOperation; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations; +import com.netflix.spinnaker.clouddriver.security.AbstractAtomicOperationsCredentialsSupport; +import java.util.Map; +import org.springframework.stereotype.Component; + +@AmazonOperation(AtomicOperations.DELETE_CLOUDFORMATION_CHANGESET) +@Component("deleteCloudFormationChangeSetDescription") +public class DeleteCloudFormationChangeSetAtomicOperationConverter + extends AbstractAtomicOperationsCredentialsSupport { + @Override + public AtomicOperation convertOperation(Map input) { + return new DeleteCloudFormationChangeSetAtomicOperation(convertDescription(input)); + } + + @Override + public DeleteCloudFormationChangeSetDescription convertDescription(Map input) { + DeleteCloudFormationChangeSetDescription converted = + getObjectMapper().convertValue(input, DeleteCloudFormationChangeSetDescription.class); + converted.setCredentials(getCredentialsObject((String) input.get("credentials"))); + return converted; + } +} diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/DeleteCloudFormationChangeSetDescription.java b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/DeleteCloudFormationChangeSetDescription.java new file mode 100644 index 00000000000..b28595ce2e5 --- /dev/null +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/DeleteCloudFormationChangeSetDescription.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 Adevinta + * + * 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.aws.deploy.description; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = false) +@Data +public class DeleteCloudFormationChangeSetDescription extends AbstractAmazonCredentialsDescription { + + private String stackName; + private String changeSetName; + private String region; +} diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/ops/DeleteCloudFormationChangeSetAtomicOperation.java b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/ops/DeleteCloudFormationChangeSetAtomicOperation.java new file mode 100644 index 00000000000..5e721c5ae31 --- /dev/null +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/ops/DeleteCloudFormationChangeSetAtomicOperation.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019 Adevinta + * + * 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.aws.deploy.ops; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.cloudformation.AmazonCloudFormation; +import com.amazonaws.services.cloudformation.model.*; +import com.netflix.spinnaker.clouddriver.aws.deploy.description.DeleteCloudFormationChangeSetDescription; +import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider; +import com.netflix.spinnaker.clouddriver.data.task.Task; +import com.netflix.spinnaker.clouddriver.data.task.TaskRepository; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class DeleteCloudFormationChangeSetAtomicOperation implements AtomicOperation { + + private static final String BASE_PHASE = "DELETE_CLOUDFORMATION_CHANGESET"; + + @Autowired AmazonClientProvider amazonClientProvider; + + private DeleteCloudFormationChangeSetDescription description; + + public DeleteCloudFormationChangeSetAtomicOperation( + DeleteCloudFormationChangeSetDescription deployCloudFormationDescription) { + this.description = deployCloudFormationDescription; + } + + @Override + public Map operate(List priorOutputs) { + Task task = TaskRepository.threadLocalTask.get(); + AmazonCloudFormation amazonCloudFormation = + amazonClientProvider.getAmazonCloudFormation( + description.getCredentials(), description.getRegion()); + + DeleteChangeSetRequest deleteChangeSetRequest = + new DeleteChangeSetRequest() + .withStackName(description.getStackName()) + .withChangeSetName(description.getChangeSetName()); + try { + task.updateStatus(BASE_PHASE, "Deleting CloudFormation ChangeSet"); + amazonCloudFormation.deleteChangeSet(deleteChangeSetRequest); + } catch (AmazonServiceException e) { + log.error( + "Error removing change set " + + description.getChangeSetName() + + " on stack " + + description.getStackName(), + e); + throw e; + } + return Collections.emptyMap(); + } +} diff --git a/clouddriver-aws/src/test/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/converters/DeleteCloudFormationChangeSetAtomicOperationConverterSpec.groovy b/clouddriver-aws/src/test/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/converters/DeleteCloudFormationChangeSetAtomicOperationConverterSpec.groovy new file mode 100644 index 00000000000..c58ba5adc1c --- /dev/null +++ b/clouddriver-aws/src/test/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/converters/DeleteCloudFormationChangeSetAtomicOperationConverterSpec.groovy @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019 Adevinta + * + * 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.aws.deploy.converters + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.clouddriver.aws.deploy.description.DeleteCloudFormationChangeSetDescription +import com.netflix.spinnaker.clouddriver.aws.deploy.ops.DeleteCloudFormationChangeSetAtomicOperation +import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider +import spock.lang.Shared +import spock.lang.Specification + +class DeleteCloudFormationChangeSetAtomicOperationConverterSpec extends Specification { + + @Shared + ObjectMapper mapper = new ObjectMapper() + + @Shared + DeleteCloudFormationChangeSetAtomicOperationConverter converter + + def setupSpec() { + this.converter = new DeleteCloudFormationChangeSetAtomicOperationConverter(objectMapper: mapper) + def accountCredentialsProvider = Mock(AccountCredentialsProvider) + def mockCredentials = Mock(NetflixAmazonCredentials) + accountCredentialsProvider.getCredentials(_) >> mockCredentials + converter.accountCredentialsProvider = accountCredentialsProvider + } + + void "DeleteCloudFormationChangeSetConverter returns DeleteCloudFormationChangeSetDescription"() { + setup: + def input = [stackName : "stack", + changeSetName : "changeset", + region : "eu-west-1", + credentials : "credentials"] + + when: + DeleteCloudFormationChangeSetDescription description = converter.convertDescription(input) + + then: + description instanceof DeleteCloudFormationChangeSetDescription + description.stackName == "stack" + description.changeSetName == "changeset" + description.region == "eu-west-1" + + when: + def operation = converter.convertOperation(input) + + then: + operation instanceof DeleteCloudFormationChangeSetAtomicOperation + } +} diff --git a/clouddriver-aws/src/test/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/ops/DeleteCloudFormationChangeSetAtomicOperationSpec.groovy b/clouddriver-aws/src/test/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/ops/DeleteCloudFormationChangeSetAtomicOperationSpec.groovy new file mode 100644 index 00000000000..7d94b659482 --- /dev/null +++ b/clouddriver-aws/src/test/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/ops/DeleteCloudFormationChangeSetAtomicOperationSpec.groovy @@ -0,0 +1,96 @@ +/* + * Copyright 2019 Adevinta, 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.aws.deploy.ops + +import com.amazonaws.AmazonServiceException +import com.amazonaws.services.cloudformation.AmazonCloudFormation +import com.amazonaws.services.cloudformation.model.* +import com.netflix.spinnaker.clouddriver.aws.TestCredential +import com.netflix.spinnaker.clouddriver.aws.deploy.description.DeleteCloudFormationChangeSetDescription +import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider +import com.netflix.spinnaker.clouddriver.data.task.Task +import com.netflix.spinnaker.clouddriver.data.task.TaskRepository +import spock.lang.Specification + +class DeleteCloudFormationChangeSetAtomicOperationSpec extends Specification { + void setupSpec() { + TaskRepository.threadLocalTask.set(Mock(Task)) + } + + void "should build a DeleteChangeSetRequest and submit through aws client"() { + given: + def amazonClientProvider = Mock(AmazonClientProvider) + def amazonCloudFormation = Mock(AmazonCloudFormation) + def deleteChangeSetResult = Mock(DeleteChangeSetResult) + def op = new DeleteCloudFormationChangeSetAtomicOperation( + new DeleteCloudFormationChangeSetDescription( + [ + stackName: "stackTest", + changeSetName: "changeSetName", + region: "eu-west-1", + credentials: TestCredential.named("test") + ] + ) + ) + op.amazonClientProvider = amazonClientProvider + + when: + op.operate([]) + + then: + 1 * amazonClientProvider.getAmazonCloudFormation(_, _) >> amazonCloudFormation + 1 * amazonCloudFormation.deleteChangeSet(_) >> { DeleteChangeSetRequest request -> + assert request.getStackName() == "stackTest" + assert request.getChangeSetName() == "changeSetName" + deleteChangeSetResult + } + } + + void "should propagate exceptions when deleting the change set"() { + given: + def amazonClientProvider = Mock(AmazonClientProvider) + def amazonCloudFormation = Mock(AmazonCloudFormation) + def op = new DeleteCloudFormationChangeSetAtomicOperation( + new DeleteCloudFormationChangeSetDescription( + [ + stackName: "stackTest", + changeSetName: "changeSetName", + region: "eu-west-1", + credentials: TestCredential.named("test") + ] + ) + ) + op.amazonClientProvider = amazonClientProvider + def exception = new AmazonServiceException("error") + + when: + try { + op.operate([]) + } + catch (Exception e) { + e instanceof AmazonServiceException + } + + then: + 1 * amazonClientProvider.getAmazonCloudFormation(_, _) >> amazonCloudFormation + 1 * amazonCloudFormation.deleteChangeSet(_) >> { + throw exception + } + } + + +} diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/orchestration/AtomicOperations.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/orchestration/AtomicOperations.java index 73d609a69a7..b25a26e9c3c 100644 --- a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/orchestration/AtomicOperations.java +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/orchestration/AtomicOperations.java @@ -107,4 +107,5 @@ public final class AtomicOperations { // CloudFormation operations public static final String DEPLOY_CLOUDFORMATION_STACK = "deployCloudFormation"; + public static final String DELETE_CLOUDFORMATION_CHANGESET = "deleteCloudFormationChangeSet"; }