diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java index 3dd854774dd..97f1c74c715 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java @@ -723,4 +723,8 @@ private String buildFinalAsgName(String clusterName) { return AbstractServerGroupNameResolver.generateServerGroupName( names.getApp(), names.getStack(), names.getDetail(), 999, false); } + + public void restageApplication(String appGuid) { + safelyCall(() -> api.restageApplication(appGuid, "")); + } } diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/ServiceInstances.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/ServiceInstances.java index b60a92cb7b2..db1a3f0fabc 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/ServiceInstances.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/ServiceInstances.java @@ -68,6 +68,16 @@ public void createServiceBinding(CreateServiceBinding createServiceBinding) { } } + public List> findAllServiceBindingsByApp(String appGuid) { + String bindingsQuery = "app_guid:" + appGuid; + return collectPageResources( + "service bindings", pg -> api.getAllServiceBindings(singletonList(bindingsQuery))); + } + + public void deleteServiceBinding(String serviceBindingGuid) { + safelyCall(() -> api.deleteServiceBinding(serviceBindingGuid)); + } + private Resource findServiceByServiceName(String serviceName) { List> services = collectPageResources( diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ApplicationService.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ApplicationService.java index 47c5812ecbe..812d43df0ae 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ApplicationService.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ApplicationService.java @@ -114,4 +114,7 @@ Response mapRoute( @PATCH("/v3/apps/{guid}/relationships/current_droplet") Response setCurrentDroplet(@Path("guid") String appGuid, @Body ToOneRelationship body); + + @POST("/v2/apps/{guid}/restage") + Response restageApplication(@Path("guid") String appGuid, @Body Object dummy); } diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ServiceInstanceService.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ServiceInstanceService.java index ac6b9440b13..94460e69292 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ServiceInstanceService.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/api/ServiceInstanceService.java @@ -30,9 +30,6 @@ public interface ServiceInstanceService { Page allUserProvided( @Query("page") Integer page, @Query("q") List queryParam); - @POST("/v2/service_bindings?accepts_incomplete=true") - Resource createServiceBinding(@Body CreateServiceBinding body); - @GET("/v2/services") Page findService(@Query("page") Integer page, @Query("q") List queryParams); @@ -68,6 +65,9 @@ Resource updateUserProvidedServiceInstance( @Path("guid") String userProvidedServiceInstanceGuid, @Body CreateUserProvidedServiceInstance body); + @POST("/v2/service_bindings?accepts_incomplete=true") + Resource createServiceBinding(@Body CreateServiceBinding body); + @GET("/v2/service_instances/{guid}/service_bindings") Page getBindingsForServiceInstance( @Path("guid") String serviceInstanceGuid, @@ -80,6 +80,12 @@ Page getBindingsForUserProvidedServiceInstance( @Query("page") Integer page, @Query("q") List queryParams); + @GET("/v2/service_bindings") + Page getAllServiceBindings(@Query("q") List queryParams); + + @DELETE("/v2/service_bindings/{guid}?accepts_incomplete=true") + Response deleteServiceBinding(@Path("guid") String serviceBindingGuid); + @DELETE("/v2/service_instances/{guid}?accepts_incomplete=true") Response destroyServiceInstance(@Path("guid") String serviceInstanceGuid); diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CreateCloudFoundryServiceBindingAtomicOperationConverter.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CreateCloudFoundryServiceBindingAtomicOperationConverter.java new file mode 100644 index 00000000000..c417f014426 --- /dev/null +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CreateCloudFoundryServiceBindingAtomicOperationConverter.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.converters; + +import com.netflix.spinnaker.clouddriver.artifacts.ArtifactDownloader; +import com.netflix.spinnaker.clouddriver.cloudfoundry.CloudFoundryOperation; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryApiException; +import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.CreateCloudFoundryServiceBindingDescription; +import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops.CreateCloudFoundryServiceBindingAtomicOperation; +import com.netflix.spinnaker.clouddriver.helpers.OperationPoller; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@CloudFoundryOperation(AtomicOperations.CREATE_SERVICE_BINDINGS) +@Component +public class CreateCloudFoundryServiceBindingAtomicOperationConverter + extends AbstractCloudFoundryServerGroupAtomicOperationConverter { + + private final OperationPoller operationPoller; + private final ArtifactDownloader artifactDownloader; + + public CreateCloudFoundryServiceBindingAtomicOperationConverter( + @Qualifier("cloudFoundryOperationPoller") OperationPoller operationPoller, + ArtifactDownloader artifactDownloader) { + this.operationPoller = operationPoller; + this.artifactDownloader = artifactDownloader; + } + + @Nullable + @Override + public AtomicOperation convertOperation(Map input) { + return new CreateCloudFoundryServiceBindingAtomicOperation( + operationPoller, convertDescription(input)); + } + + @Override + public CreateCloudFoundryServiceBindingDescription convertDescription(Map input) { + List> requests = + (List>) input.get("serviceBindingRequests"); + for (Map request : requests) { + if (request.get("artifact") != null) { + Artifact artifact = getObjectMapper().convertValue(request.get("artifact"), Artifact.class); + try (InputStream inputStream = artifactDownloader.download(artifact)) { + Map paramMap = getObjectMapper().readValue(inputStream, Map.class); + request.put("parameters", paramMap); + } catch (Exception e) { + throw new CloudFoundryApiException( + "Could not convert service binding request parameters to json."); + } + } + } + input.put("serviceBindingRequests", requests); + + CreateCloudFoundryServiceBindingDescription description = + getObjectMapper().convertValue(input, CreateCloudFoundryServiceBindingDescription.class); + description.setCredentials(getCredentialsObject(input.get("credentials").toString())); + description.setClient(getClient(input)); + description.setServerGroupId( + getServerGroupId( + description.getServerGroupName(), description.getRegion(), description.getClient())); + findSpace(description.getRegion(), description.getClient()) + .ifPresentOrElse( + description::setSpace, + () -> { + throw new CloudFoundryApiException("Could not determine CloudFoundry Space."); + }); + return description; + } +} diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/CreateCloudFoundryServiceBindingDescription.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/CreateCloudFoundryServiceBindingDescription.java new file mode 100644 index 00000000000..a7974c55920 --- /dev/null +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/CreateCloudFoundryServiceBindingDescription.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description; + +import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundrySpace; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor +public class CreateCloudFoundryServiceBindingDescription + extends AbstractCloudFoundryServerGroupDescription { + + private CloudFoundrySpace space; + private List serviceBindingRequests; + private boolean restageRequired = true; + private boolean restartRequired; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ServiceBindingRequest { + private String serviceInstanceName; + private Map parameters; + private boolean updatable; + } +} diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingAtomicOperation.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingAtomicOperation.java new file mode 100644 index 00000000000..338b9f931e0 --- /dev/null +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingAtomicOperation.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops; + +import static com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops.CloudFoundryOperationUtils.describeProcessState; + +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryApiException; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.CreateServiceBinding; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.ProcessStats; +import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.CreateCloudFoundryServiceBindingDescription; +import com.netflix.spinnaker.clouddriver.data.task.Task; +import com.netflix.spinnaker.clouddriver.data.task.TaskRepository; +import com.netflix.spinnaker.clouddriver.helpers.OperationPoller; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CreateCloudFoundryServiceBindingAtomicOperation implements AtomicOperation { + + private static final String PHASE = "CREATE_SERVICE_BINDINGS"; + private final OperationPoller operationPoller; + private final CreateCloudFoundryServiceBindingDescription description; + + private static Task getTask() { + return TaskRepository.threadLocalTask.get(); + } + + @Override + public Void operate(List priorOutputs) { + + List serviceInstanceNames = + description.getServiceBindingRequests().stream() + .map(s -> s.getServiceInstanceName()) + .collect(Collectors.toList()); + + getTask() + .updateStatus( + PHASE, + "Creating Cloud Foundry service bindings between application '" + + description.getServerGroupName() + + "' and services: " + + serviceInstanceNames); + + Map serviceInstanceGuids = new HashMap<>(); + + description + .getClient() + .getServiceInstances() + .findAllServicesBySpaceAndNames(description.getSpace(), serviceInstanceNames) + .stream() + .forEach(s -> serviceInstanceGuids.put(s.getEntity().getName(), s.getMetadata().getGuid())); + + if (serviceInstanceNames.size() != description.getServiceBindingRequests().size()) { + throw new CloudFoundryApiException( + "Number of service instances found does not match the number of service binding requests."); + } + + List bindings = + description.getServiceBindingRequests().stream() + .map( + s -> { + String serviceGuid = serviceInstanceGuids.get(s.getServiceInstanceName()); + if (serviceGuid == null || serviceGuid.isEmpty()) { + throw new CloudFoundryApiException( + "Unable to find service with the name: '" + + s.getServiceInstanceName() + + "'"); + } + if (s.isUpdatable()) { + removeBindings(serviceGuid, description.getServerGroupId()); + } + return new CreateServiceBinding( + serviceGuid, description.getServerGroupId(), s.getParameters()); + }) + .collect(Collectors.toList()); + + bindings.forEach(b -> description.getClient().getServiceInstances().createServiceBinding(b)); + + if (description.isRestageRequired()) { + getTask().updateStatus(PHASE, "Restaging application '" + description.getServerGroupName()); + description.getClient().getApplications().restageApplication(description.getServerGroupId()); + } else { + getTask().updateStatus(PHASE, "Restarting application '" + description.getServerGroupName()); + description.getClient().getApplications().stopApplication(description.getServerGroupId()); + operationPoller.waitForOperation( + () -> + description + .getClient() + .getApplications() + .getProcessState(description.getServerGroupId()), + inProgressState -> + inProgressState == ProcessStats.State.DOWN + || inProgressState == ProcessStats.State.CRASHED, + null, + getTask(), + description.getServerGroupName(), + PHASE); + description.getClient().getApplications().startApplication(description.getServerGroupId()); + } + + ProcessStats.State state = + operationPoller.waitForOperation( + () -> + description + .getClient() + .getApplications() + .getProcessState(description.getServerGroupId()), + inProgressState -> + inProgressState == ProcessStats.State.RUNNING + || inProgressState == ProcessStats.State.CRASHED, + null, + getTask(), + description.getServerGroupName(), + PHASE); + + if (state != ProcessStats.State.RUNNING) { + getTask() + .updateStatus( + PHASE, + "Failed to create Cloud Foundry service bindings between application '" + + description.getServerGroupName() + + "' and services: " + + serviceInstanceNames); + throw new CloudFoundryApiException( + "Failed to start '" + + description.getServerGroupName() + + "' which instead " + + describeProcessState(state)); + } + + getTask() + .updateStatus( + PHASE, + "Created Cloud Foundry service bindings between application '" + + description.getServerGroupName() + + "' and services: " + + serviceInstanceNames); + + return null; + } + + private void removeBindings(String serviceGuid, String appGuid) { + description.getClient().getServiceInstances().findAllServiceBindingsByApp(appGuid).stream() + .filter(s -> serviceGuid.equalsIgnoreCase(s.getEntity().getServiceInstanceGuid())) + .findAny() + .ifPresent( + s -> + description + .getClient() + .getServiceInstances() + .deleteServiceBinding(s.getMetadata().getGuid())); + } +} diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/validators/CreateCloudFoundryServiceBindingDescriptionValidator.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/validators/CreateCloudFoundryServiceBindingDescriptionValidator.java new file mode 100644 index 00000000000..3f694273bf3 --- /dev/null +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/validators/CreateCloudFoundryServiceBindingDescriptionValidator.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.validators; + +import com.netflix.spinnaker.clouddriver.cloudfoundry.CloudFoundryOperation; +import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations; +import org.springframework.stereotype.Component; + +@CloudFoundryOperation(AtomicOperations.CREATE_SERVICE_BINDINGS) +@Component("createCloudFoundryServiceBindingDescriptionValidator") +public class CreateCloudFoundryServiceBindingDescriptionValidator + extends AbstractCloudFoundryDescriptionValidator {} diff --git a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingRequestAtomicOperationTest.java b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingRequestAtomicOperationTest.java new file mode 100644 index 00000000000..250b3eca122 --- /dev/null +++ b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingRequestAtomicOperationTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.atIndex; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryClient; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.MockCloudFoundryClient; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.AbstractServiceInstance; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.Resource; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.UserProvidedServiceInstance; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.ProcessStats; +import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.CreateCloudFoundryServiceBindingDescription; +import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundryOrganization; +import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundrySpace; +import com.netflix.spinnaker.clouddriver.data.task.Task; +import com.netflix.spinnaker.clouddriver.helpers.OperationPoller; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; + +public class CreateCloudFoundryServiceBindingRequestAtomicOperationTest + extends AbstractCloudFoundryAtomicOperationTest { + + OperationPoller poller = mock(OperationPoller.class); + CloudFoundryClient client = new MockCloudFoundryClient(); + + private final CloudFoundrySpace cloudFoundrySpace = + CloudFoundrySpace.builder() + .name("space") + .organization(CloudFoundryOrganization.builder().name("org").build()) + .build(); + + @Test + public void shouldCreateServiceBinding() { + CreateCloudFoundryServiceBindingDescription desc = + new CreateCloudFoundryServiceBindingDescription(); + desc.setSpace(cloudFoundrySpace); + desc.setRegion(cloudFoundrySpace.getRegion()); + desc.setClient(client); + desc.setRestageRequired(true); + desc.setServerGroupName("app1"); + CreateCloudFoundryServiceBindingDescription.ServiceBindingRequest binding = + new CreateCloudFoundryServiceBindingDescription.ServiceBindingRequest( + "service1", null, false); + desc.setServiceBindingRequests(Collections.singletonList(binding)); + + CreateCloudFoundryServiceBindingAtomicOperation operation = + new CreateCloudFoundryServiceBindingAtomicOperation(poller, desc); + + UserProvidedServiceInstance serviceInstance = new UserProvidedServiceInstance(); + serviceInstance.setName("service1"); + + Resource resource = new Resource<>(); + Resource.Metadata metadata = new Resource.Metadata(); + metadata.setGuid("123abc"); + resource.setEntity(serviceInstance); + resource.setMetadata(metadata); + + List> instances = List.of(resource); + when(desc.getClient().getServiceInstances().findAllServicesBySpaceAndNames(any(), any())) + .thenReturn(instances); + when(poller.waitForOperation(any(Supplier.class), any(), any(), any(), any(), any())) + .thenReturn(ProcessStats.State.RUNNING); + + Task task = runOperation(operation); + + verify(client.getServiceInstances()).createServiceBinding(any()); + assertThat(task.getHistory()) + .has( + status( + "Creating Cloud Foundry service bindings between application 'app1' and services: [service1]"), + atIndex(1)); + assertThat(task.getHistory()) + .has( + status( + "Created Cloud Foundry service bindings between application 'app1' and services: [service1]"), + atIndex(3)); + } + + @Test + public void shouldCreateServiceBindingWithParameters() { + CreateCloudFoundryServiceBindingDescription desc = + new CreateCloudFoundryServiceBindingDescription(); + desc.setSpace(cloudFoundrySpace); + desc.setRegion(cloudFoundrySpace.getRegion()); + desc.setClient(client); + desc.setRestageRequired(true); + desc.setServerGroupName("app1"); + CreateCloudFoundryServiceBindingDescription.ServiceBindingRequest binding = + new CreateCloudFoundryServiceBindingDescription.ServiceBindingRequest( + "service1", null, false); + desc.setServiceBindingRequests(Collections.singletonList(binding)); + + CreateCloudFoundryServiceBindingAtomicOperation operation = + new CreateCloudFoundryServiceBindingAtomicOperation(poller, desc); + + UserProvidedServiceInstance serviceInstance = new UserProvidedServiceInstance(); + serviceInstance.setName("service1"); + + Resource resource = new Resource<>(); + Resource.Metadata metadata = new Resource.Metadata(); + metadata.setGuid("123abc"); + resource.setEntity(serviceInstance); + resource.setMetadata(metadata); + + List> instances = List.of(resource); + when(desc.getClient().getServiceInstances().findAllServicesBySpaceAndNames(any(), any())) + .thenReturn(instances); + when(poller.waitForOperation(any(Supplier.class), any(), any(), any(), any(), any())) + .thenReturn(ProcessStats.State.RUNNING); + + Task task = runOperation(operation); + + verify(client.getServiceInstances()).createServiceBinding(any()); + assertThat(task.getHistory()) + .has( + status( + "Creating Cloud Foundry service bindings between application 'app1' and services: [service1]"), + atIndex(1)); + assertThat(task.getHistory()) + .has( + status( + "Created Cloud Foundry service bindings between application 'app1' and services: [service1]"), + atIndex(3)); + } +} 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 97b01c63405..237d2e42693 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 @@ -105,6 +105,7 @@ public final class AtomicOperations { public static final String DESTROY_SERVICE = "destroyService"; public static final String SHARE_SERVICE = "shareService"; public static final String UNSHARE_SERVICE = "unshareService"; + public static final String CREATE_SERVICE_BINDINGS = "createServiceBindings"; // CloudFormation operations public static final String DEPLOY_CLOUDFORMATION_STACK = "deployCloudFormation";