package alien4cloud.rest.runtime;
import java.util.ArrayList;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Resource;
import javax.inject.Inject;
import javax.validation.Valid;
import io.swagger.annotations.Api;
import org.alien4cloud.tosca.catalog.index.IToscaTypeSearchService;
import org.alien4cloud.tosca.model.definitions.IValue;
import org.alien4cloud.tosca.model.definitions.Interface;
import org.alien4cloud.tosca.model.definitions.Operation;
import org.alien4cloud.tosca.model.definitions.PropertyDefinition;
import org.alien4cloud.tosca.model.templates.NodeTemplate;
import org.alien4cloud.tosca.model.templates.Topology;
import org.alien4cloud.tosca.model.types.NodeType;
import org.alien4cloud.tosca.topology.TopologyDTOBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import com.google.common.collect.Lists;
import alien4cloud.application.ApplicationEnvironmentService;
import alien4cloud.application.ApplicationService;
import alien4cloud.audit.annotation.Audit;
import alien4cloud.deployment.DeploymentRuntimeService;
import alien4cloud.deployment.DeploymentRuntimeStateService;
import alien4cloud.deployment.DeploymentService;
import alien4cloud.exception.NotFoundException;
import alien4cloud.model.application.Application;
import alien4cloud.model.application.ApplicationEnvironment;
import alien4cloud.model.components.IndexedModelUtils;
import alien4cloud.model.deployment.Deployment;
import alien4cloud.model.deployment.DeploymentTopology;
import alien4cloud.paas.IPaaSCallback;
import alien4cloud.paas.exception.OperationExecutionException;
import alien4cloud.paas.exception.OrchestratorDisabledException;
import alien4cloud.paas.model.OperationExecRequest;
import alien4cloud.paas.plan.TopologyTreeBuilderService;
import alien4cloud.rest.model.RestError;
import alien4cloud.rest.model.RestErrorCode;
import alien4cloud.rest.model.RestResponse;
import alien4cloud.rest.model.RestResponseBuilder;
import alien4cloud.security.AuthorizationUtil;
import alien4cloud.topology.TopologyDTO;
import alien4cloud.topology.TopologyServiceCore;
import alien4cloud.tosca.properties.constraints.ConstraintUtil.ConstraintInformation;
import alien4cloud.tosca.properties.constraints.exception.ConstraintFunctionalException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintRequiredParameterException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintValueDoNotMatchPropertyTypeException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintViolationException;
import alien4cloud.utils.services.ConstraintPropertyService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.Authorization;
import lombok.extern.slf4j.Slf4j;
@RestController
@Slf4j
@RequestMapping({ "/rest/runtime", "/rest/v1/runtime", "/rest/latest/runtime" })
@Api
public class RuntimeController {
@Resource
private DeploymentService deploymentService;
@Resource
private ApplicationService applicationService;
@Resource
private ApplicationEnvironmentService applicationEnvironmentService;
@Inject
private IToscaTypeSearchService toscaTypeSearchService;
@Resource
private ConstraintPropertyService constraintPropertyService;
@Inject
private TopologyDTOBuilder topologyDTOBuilder;
@Inject
private TopologyTreeBuilderService topologyTreeBuilderService;
@Inject
private DeploymentRuntimeStateService deploymentRuntimeStateService;
@Inject
private DeploymentRuntimeService deploymentRuntimeService;
@ApiOperation(value = "Trigger a custom command on a specific node template of a topology .", authorizations = {
@Authorization("APPLICATION_MANAGER") }, notes = "Returns a response with no errors and the command response as data in success case. Application role required [ APPLICATION_MANAGER ]")
@RequestMapping(value = "/{applicationId:.+?}/operations", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@PreAuthorize("isAuthenticated()")
@Audit
public DeferredResult<RestResponse<Object>> executeOperation(@PathVariable String applicationId,
@RequestBody @Valid OperationExecRequest operationRequest) {
final DeferredResult<RestResponse<Object>> result = new DeferredResult<>(15L * 60L * 1000L);
Application application = applicationService.getOrFail(applicationId);
ApplicationEnvironment environment = applicationEnvironmentService.getEnvironmentByIdOrDefault(applicationId,
operationRequest.getApplicationEnvironmentId());
AuthorizationUtil.checkAuthorizationForEnvironment(application, environment);
Topology topology = deploymentRuntimeStateService.getRuntimeTopologyFromEnvironment(operationRequest.getApplicationEnvironmentId());
// validate the operation request
try {
validateCommand(operationRequest, topology);
} catch (ConstraintViolationException e) {
result.setErrorResult(RestResponseBuilder.<Object> builder().data(e.getConstraintInformation())
.error(new RestError(RestErrorCode.PROPERTY_CONSTRAINT_VIOLATION_ERROR.getCode(), e.getMessage())).build());
return result;
} catch (ConstraintValueDoNotMatchPropertyTypeException e) {
result.setErrorResult(RestResponseBuilder.<Object> builder().data(e.getConstraintInformation())
.error(new RestError(RestErrorCode.PROPERTY_TYPE_VIOLATION_ERROR.getCode(), e.getMessage())).build());
return result;
} catch (ConstraintRequiredParameterException e) {
result.setErrorResult(RestResponseBuilder.<Object> builder().data(e.getConstraintInformation())
.error(new RestError(RestErrorCode.PROPERTY_REQUIRED_VIOLATION_ERROR.getCode(), e.getMessage())).build());
return result;
} catch (ConstraintFunctionalException e) {
result.setErrorResult(RestResponseBuilder.<Object> builder().data(e.getConstraintInformation())
.error(new RestError(RestErrorCode.PROPERTY_UNKNOWN_VIOLATION_ERROR.getCode(), e.getMessage())).build());
return result;
}
// try to trigger the execution of the operation
try {
deploymentRuntimeService.triggerOperationExecution(operationRequest, new IPaaSCallback<Map<String, String>>() {
@Override
public void onSuccess(Map<String, String> data) {
result.setResult(RestResponseBuilder.<Object> builder().data(data).build());
}
@Override
public void onFailure(Throwable throwable) {
result.setErrorResult(RestResponseBuilder.<Object> builder()
.error(new RestError(RestErrorCode.NODE_OPERATION_EXECUTION_ERROR.getCode(), throwable.getMessage())).build());
}
});
} catch (OperationExecutionException e) {
result.setErrorResult(RestResponseBuilder.<Object> builder()
.error(new RestError(RestErrorCode.NODE_OPERATION_EXECUTION_ERROR.getCode(), e.getMessage())).build());
} catch (OrchestratorDisabledException e) {
result.setErrorResult(
RestResponseBuilder.<Object> builder().error(new RestError(RestErrorCode.CLOUD_DISABLED_ERROR.getCode(), e.getMessage())).build());
}
return result;
}
/**
* Get runtime (deployed) topology of an application on a specific environment
*
* @param applicationId application id for which to get the topology
* @param applicationEnvironmentId application environment for which to get the topology through the version
* @return {@link RestResponse}<{@link TopologyDTO}> containing the requested runtime {@link Topology} and the
* {@link NodeType} related to his {@link NodeTemplate}s
*/
@ApiOperation(value = "Get runtime (deployed) topology of an application on a specific cloud.")
@RequestMapping(value = "/{applicationId:.+?}/environment/{applicationEnvironmentId:.+?}/topology", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("isAuthenticated()")
public RestResponse<TopologyDTO> getDeployedTopology(
@ApiParam(value = "Id of the application for which to get deployed topology.", required = true) @PathVariable String applicationId,
@ApiParam(value = "Id of the environment for which to get deployed topology.", required = true) @PathVariable String applicationEnvironmentId) {
ApplicationEnvironment environment = applicationEnvironmentService.getEnvironmentByIdOrDefault(applicationId, applicationEnvironmentId);
if (!environment.getApplicationId().equals(applicationId)) {
throw new NotFoundException("Unable to find environment with id <" + applicationEnvironmentId + "> for application <" + applicationId + ">");
}
// Security check user must be authorized to deploy the environment (or be application manager)
AuthorizationUtil.checkAuthorizationForEnvironment(applicationService.getOrFail(applicationId), environment);
Deployment deployment = deploymentService.getActiveDeploymentOrFail(environment.getId());
DeploymentTopology deploymentTopology = deploymentRuntimeStateService.getRuntimeTopology(deployment.getId());
return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTOBuilder.buildTopologyDTO(deploymentTopology)).build();
}
private void validateCommand(OperationExecRequest operationRequest, Topology topology) throws ConstraintFunctionalException {
NodeTemplate nodeTemplate = TopologyServiceCore.getNodeTemplate(topology.getId(), operationRequest.getNodeTemplateName(),
TopologyServiceCore.getNodeTemplates(topology));
NodeType indexedNodeType = toscaTypeSearchService.getRequiredElementInDependencies(NodeType.class, nodeTemplate.getType(), topology.getDependencies());
Map<String, Interface> interfaces = IndexedModelUtils.mergeInterfaces(indexedNodeType.getInterfaces(), nodeTemplate.getInterfaces());
if (interfaces == null || interfaces.get(operationRequest.getInterfaceName()) == null) {
throw new NotFoundException("Interface [" + operationRequest.getInterfaceName() + "] not found in the node template ["
+ operationRequest.getNodeTemplateName() + "] related to [" + indexedNodeType.getId() + "]");
}
Interface interfass = interfaces.get(operationRequest.getInterfaceName());
validateOperation(interfass, operationRequest);
}
private void validateParameters(Interface interfass, OperationExecRequest operationRequest)
throws ConstraintViolationException, ConstraintValueDoNotMatchPropertyTypeException, ConstraintRequiredParameterException {
ArrayList<String> missingParams = Lists.newArrayList();
Operation operation = interfass.getOperations().get(operationRequest.getOperationName());
if (operation.getInputParameters() != null) {
for (Entry<String, IValue> inputParameter : operation.getInputParameters().entrySet()) {
if (inputParameter.getValue().isDefinition()) {
String requestInputParameter = operationRequest.getParameters() == null ? null
: operationRequest.getParameters().get(inputParameter.getKey());
PropertyDefinition currentOperationParameter = (PropertyDefinition) inputParameter.getValue();
if (StringUtils.isNotBlank(requestInputParameter)) {
// recover the good property definition for the current parameter
constraintPropertyService.checkSimplePropertyConstraint(inputParameter.getKey(), requestInputParameter, currentOperationParameter);
} else if (currentOperationParameter.isRequired()) {
// input param not in the request, id required this is a missing parameter...
missingParams.add(inputParameter.getKey());
} else {
// set the value to null
operation.getInputParameters().put(inputParameter.getKey(), null);
}
}
}
}
// check required input issue
if (!missingParams.isEmpty()) {
log.error("Missing required parameter", missingParams);
ConstraintInformation constraintInformation = new ConstraintInformation(null, null, missingParams.toString(), "required");
throw new ConstraintRequiredParameterException("Missing required parameters", null, constraintInformation);
}
}
private void validateOperation(Interface interfass, OperationExecRequest operationRequest) throws ConstraintFunctionalException {
Operation operation = interfass.getOperations().get(operationRequest.getOperationName());
if (operation == null) {
throw new NotFoundException("Operation [" + operationRequest.getOperationName() + "] is not defined in the interface ["
+ operationRequest.getInterfaceName() + "] of the node [" + operationRequest.getNodeTemplateName() + "]");
}
// validate parameters (value/type and required value)
validateParameters(interfass, operationRequest);
}
@ApiOperation(value = "Get non-natives node template of a topology.", notes = "Returns An map of non-natives {@link NodeTemplate}. Application role required [ APPLICATION_MANAGER | DEPLOYMENT_MANAGER ]")
@RequestMapping(value = "/{applicationId:.+?}/environment/{applicationEnvironmentId:.+?}/nonNatives", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("isAuthenticated()")
public RestResponse<Map<String, NodeTemplate>> getNonNativesNodes(@PathVariable String applicationId, @PathVariable String applicationEnvironmentId) {
Application application = applicationService.getOrFail(applicationId);
ApplicationEnvironment environment = applicationEnvironmentService.getEnvironmentByIdOrDefault(applicationId, applicationEnvironmentId);
AuthorizationUtil.checkAuthorizationForEnvironment(application, environment);
Deployment deployment = deploymentService.getActiveDeploymentOrFail(environment.getId());
DeploymentTopology deploymentTopology = deploymentRuntimeStateService.getRuntimeTopology(deployment.getId());
Map<String, NodeTemplate> nonNativesNode = topologyTreeBuilderService.getNonNativesNodes(deploymentTopology);
return RestResponseBuilder.<Map<String, NodeTemplate>> builder().data(nonNativesNode).build();
}
}