-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cloudfoundry): add service-binding stage (#5166)
- Loading branch information
1 parent
6d68316
commit 4ebfaf6
Showing
10 changed files
with
511 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
...udfoundry/deploy/converters/CreateCloudFoundryServiceBindingAtomicOperationConverter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Map<String, Object>> requests = | ||
(List<Map<String, Object>>) input.get("serviceBindingRequests"); | ||
for (Map<String, Object> request : requests) { | ||
if (request.get("artifact") != null) { | ||
Artifact artifact = getObjectMapper().convertValue(request.get("artifact"), Artifact.class); | ||
try (InputStream inputStream = artifactDownloader.download(artifact)) { | ||
Map<String, Object> 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; | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
...uddriver/cloudfoundry/deploy/description/CreateCloudFoundryServiceBindingDescription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ServiceBindingRequest> serviceBindingRequests; | ||
private boolean restageRequired = true; | ||
private boolean restartRequired; | ||
|
||
@Data | ||
@AllArgsConstructor | ||
@NoArgsConstructor | ||
public static class ServiceBindingRequest { | ||
private String serviceInstanceName; | ||
private Map<String, Object> parameters; | ||
private boolean updatable; | ||
} | ||
} |
172 changes: 172 additions & 0 deletions
172
.../clouddriver/cloudfoundry/deploy/ops/CreateCloudFoundryServiceBindingAtomicOperation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Void> { | ||
|
||
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<Void> priorOutputs) { | ||
|
||
List<String> 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<String, String> 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<CreateServiceBinding> 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())); | ||
} | ||
} |
Oops, something went wrong.