package alien4cloud.rest.deployment;
import static alien4cloud.utils.AlienUtils.safe;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.inject.Inject;
import org.alien4cloud.tosca.model.definitions.DeploymentArtifact;
import org.apache.commons.collections4.MapUtils;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import alien4cloud.application.ApplicationEnvironmentService;
import alien4cloud.application.ApplicationService;
import alien4cloud.audit.annotation.Audit;
import alien4cloud.component.repository.ArtifactRepositoryConstants;
import alien4cloud.component.repository.IFileRepository;
import alien4cloud.deployment.DeploymentTopologyService;
import alien4cloud.deployment.OrchestratorPropertiesValidationService;
import alien4cloud.deployment.model.DeploymentConfiguration;
import alien4cloud.exception.NotFoundException;
import alien4cloud.model.application.Application;
import alien4cloud.model.application.ApplicationEnvironment;
import alien4cloud.model.deployment.DeploymentTopology;
import alien4cloud.paas.exception.OrchestratorDisabledException;
import alien4cloud.rest.application.model.SetLocationPoliciesRequest;
import alien4cloud.rest.application.model.UpdateDeploymentTopologyRequest;
import alien4cloud.rest.model.RestErrorBuilder;
import alien4cloud.rest.model.RestErrorCode;
import alien4cloud.rest.model.RestResponse;
import alien4cloud.rest.model.RestResponseBuilder;
import alien4cloud.rest.topology.UpdatePropertyRequest;
import alien4cloud.security.AuthorizationUtil;
import alien4cloud.tosca.context.ToscaContext;
import alien4cloud.tosca.properties.constraints.ConstraintUtil;
import alien4cloud.tosca.properties.constraints.exception.ConstraintFunctionalException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintValueDoNotMatchPropertyTypeException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintViolationException;
import alien4cloud.utils.RestConstraintValidator;
import alien4cloud.utils.services.PropertyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.Authorization;
@RestController
@RequestMapping({ "/rest/applications/{appId}/environments/{environmentId}/deployment-topology",
"/rest/v1/applications/{appId}/environments/{environmentId}/deployment-topology",
"/rest/latest/applications/{appId}/environments/{environmentId}/deployment-topology" })
@Api(value = "", description = "Prepare a topology to be deployed on a specific environment (location matching, node matching and inputs configuration).")
public class DeploymentTopologyController {
@Inject
private DeploymentTopologyService deploymentTopologyService;
@Inject
private ApplicationService applicationService;
@Inject
private ApplicationEnvironmentService appEnvironmentService;
@Inject
public IDeploymentTopologyHelper deploymentTopologyHelper;
@Inject
public PropertyService propertyService;
@Inject
private OrchestratorPropertiesValidationService orchestratorPropertiesValidationService;
@Resource
private IFileRepository artifactRepository;
/**
* Get the deployment topology of an application given an environment
*
* @param appId application Id
* @param environmentId environment Id
* @return the deployment topology DTO
*/
@ApiOperation(value = "Get the deployment topology of an application given an environment.", notes = "Application role required [ APPLICATION_MANAGER | APPLICATION_DEVOPS ] and Application environment role required [ DEPLOYMENT_MANAGER ]")
@RequestMapping(value = "", method = RequestMethod.GET)
@PreAuthorize("isAuthenticated()")
public RestResponse<DeploymentTopologyDTO> getDeploymentTopology(@PathVariable String appId, @PathVariable String environmentId) {
checkAuthorizations(appId, environmentId);
DeploymentConfiguration deploymentConfiguration = deploymentTopologyService.getDeploymentConfiguration(environmentId);
DeploymentTopologyDTO dto = deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentConfiguration);
return RestResponseBuilder.<DeploymentTopologyDTO> builder().data(dto).build();
}
/**
* Update application's input artifact.
*
* @param appId application Id
* @param environmentId environment Id
* @param inputArtifactId artifact's id
* @return nothing if success, error will be handled in global exception strategy
* @throws IOException
*/
@ApiOperation(value = "Upload input artifact.", notes = "The logged-in user must have the application manager role for this application. Application role required [ APPLICATION_MANAGER | DEPLOYMENT_MANAGER ]")
@RequestMapping(value = "/inputArtifacts/{inputArtifactId}/upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("isAuthenticated()")
public RestResponse<DeploymentTopologyDTO> updateDeploymentInputArtifact(@PathVariable String appId, @PathVariable String environmentId,
@PathVariable String inputArtifactId, @RequestParam("file") MultipartFile artifactFile) throws IOException {
// Get the artifact to update
checkAuthorizations(appId, environmentId);
DeploymentTopology topology = deploymentTopologyService.getDeploymentTopology(environmentId);
if (topology.getInputArtifacts() == null || !topology.getInputArtifacts().containsKey(inputArtifactId)) {
throw new NotFoundException("Artifact with key [" + inputArtifactId + "] do not exist");
}
Map<String, DeploymentArtifact> artifacts = topology.getUploadedInputArtifacts();
if (artifacts == null) {
artifacts = new HashMap<>();
topology.setUploadedInputArtifacts(artifacts);
}
DeploymentArtifact artifact = artifacts.get(inputArtifactId);
if (artifact == null) {
artifact = new DeploymentArtifact();
artifacts.put(inputArtifactId, artifact);
} else if (ArtifactRepositoryConstants.ALIEN_ARTIFACT_REPOSITORY.equals(artifact.getArtifactRepository())) {
artifactRepository.deleteFile(artifact.getArtifactRef());
}
try (InputStream artifactStream = artifactFile.getInputStream()) {
String artifactFileId = artifactRepository.storeFile(artifactStream);
artifact.setArtifactName(artifactFile.getOriginalFilename());
artifact.setArtifactRef(artifactFileId);
artifact.setArtifactRepository(ArtifactRepositoryConstants.ALIEN_ARTIFACT_REPOSITORY);
deploymentTopologyService.updateDeploymentTopologyInputsAndSave(topology);
return RestResponseBuilder.<DeploymentTopologyDTO> builder()
.data(deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentTopologyService.getDeploymentConfiguration(environmentId))).build();
}
}
/**
* Update node substitution.
*
* @param appId id of the application.
* @param environmentId id of the environment.
* @return response containing the available substitutions.
*/
@ApiOperation(value = "Substitute a specific node by the location resource template in the topology of an application given an environment.", notes = "Application role required [ APPLICATION_MANAGER | APPLICATION_DEVOPS ] and Application environment role required [ DEPLOYMENT_MANAGER ]")
@RequestMapping(value = "/substitutions/{nodeId}", method = RequestMethod.POST)
@PreAuthorize("isAuthenticated()")
@Audit
public RestResponse<DeploymentTopologyDTO> updateSubstitution(@PathVariable String appId, @PathVariable String environmentId, @PathVariable String nodeId,
@RequestParam String locationResourceTemplateId) {
checkAuthorizations(appId, environmentId);
DeploymentConfiguration deploymentConfiguration = deploymentTopologyService.updateSubstitution(environmentId, nodeId, locationResourceTemplateId);
return RestResponseBuilder.<DeploymentTopologyDTO> builder().data(deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentConfiguration)).build();
}
@ApiOperation(value = "Update substitution's property.", authorizations = { @Authorization("ADMIN") })
@RequestMapping(value = "/substitutions/{nodeId}/properties", method = RequestMethod.POST)
@PreAuthorize("isAuthenticated()")
@Audit
public RestResponse<?> updateSubstitutionProperty(@PathVariable String appId, @PathVariable String environmentId, @PathVariable String nodeId,
@RequestBody UpdatePropertyRequest updateRequest) {
checkAuthorizations(appId, environmentId);
try {
deploymentTopologyService.updateProperty(environmentId, nodeId, updateRequest.getPropertyName(), updateRequest.getPropertyValue());
return RestResponseBuilder.<DeploymentTopologyDTO> builder()
.data(deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentTopologyService.getDeploymentConfiguration(environmentId))).build();
} catch (ConstraintFunctionalException e) {
return RestConstraintValidator.fromException(e, updateRequest.getPropertyName(), updateRequest.getPropertyValue());
}
}
@ApiOperation(value = "Update substitution's capability property.", authorizations = { @Authorization("ADMIN") })
@RequestMapping(value = "/substitutions/{nodeId}/capabilities/{capabilityName}/properties", method = RequestMethod.POST)
@PreAuthorize("isAuthenticated()")
@Audit
public RestResponse<?> updateSubstitutionCapabilityProperty(@PathVariable String appId, @PathVariable String environmentId, @PathVariable String nodeId,
@PathVariable String capabilityName, @RequestBody UpdatePropertyRequest updateRequest) {
checkAuthorizations(appId, environmentId);
try {
deploymentTopologyService.updateCapabilityProperty(environmentId, nodeId, capabilityName, updateRequest.getPropertyName(),
updateRequest.getPropertyValue());
return RestResponseBuilder.<DeploymentTopologyDTO> builder()
.data(deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentTopologyService.getDeploymentConfiguration(environmentId))).build();
} catch (ConstraintFunctionalException e) {
return RestConstraintValidator.fromException(e, updateRequest.getPropertyName(), updateRequest.getPropertyValue());
}
}
/**
* Set location policies for a deployment. Creates if not yet the {@link DeploymentTopology} object linked to this deployment
*
* @param appId application Id
* @param request {@link SetLocationPoliciesRequest} object: location policies
* @return
*/
@ApiOperation(value = "Set location policies for a deployment. Creates if not yet the {@link DeploymentTopology} object linked to this deployment.", notes = "Application role required [ APPLICATION_MANAGER | APPLICATION_DEVOPS ] and Application environment role required [ DEPLOYMENT_MANAGER ]")
@RequestMapping(value = "/location-policies", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@Audit
@PreAuthorize("isAuthenticated()")
public RestResponse<DeploymentTopologyDTO> setLocationPolicies(@ApiParam(value = "Id of the application.", required = true) @PathVariable String appId,
@ApiParam(value = "Id of the environment on which to set the location policies.", required = true) @PathVariable String environmentId,
@ApiParam(value = "Location policies request body.", required = true) @RequestBody SetLocationPoliciesRequest request) {
checkAuthorizations(appId, environmentId);
DeploymentConfiguration deploymentConfiguration = deploymentTopologyService.setLocationPolicies(environmentId, request.getOrchestratorId(),
request.getGroupsToLocations());
return RestResponseBuilder.<DeploymentTopologyDTO> builder().data(deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentConfiguration)).build();
}
/**
*
* @param appId The application id
* @param environmentId Id of the environment we want to update
* @param updateRequest an {@link UpdateDeploymentTopologyRequest} object
* @return a {@link RestResponse} with:<br>
* the {@link DeploymentTopologyDTO} if everything went well, the <br>
* Error if not
*
* @throws OrchestratorDisabledException
*/
@ApiOperation(value = "Updates by merging the given request into the given application's deployment topology.", notes = "Application role required [ APPLICATION_MANAGER | APPLICATION_DEVOPS ] and Application environment role required [ DEPLOYMENT_MANAGER ]")
@RequestMapping(method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("isAuthenticated()")
@Audit
public RestResponse<?> updateDeploymentSetup(@PathVariable String appId, @PathVariable String environmentId,
@RequestBody UpdateDeploymentTopologyRequest updateRequest) throws OrchestratorDisabledException {
// check rights on related environment
checkAuthorizations(appId, environmentId);
DeploymentConfiguration deploymentConfiguration = deploymentTopologyService.getDeploymentConfiguration(environmentId);
DeploymentTopology deploymentTopology = deploymentConfiguration.getDeploymentTopology();
try {
ToscaContext.init(deploymentTopology.getDependencies());
// update topology inputs
for (Map.Entry<String, Object> inputPropertyValue : safe(updateRequest.getInputProperties()).entrySet()) {
if (deploymentTopology.getInputs() == null || deploymentTopology.getInputs().get(inputPropertyValue.getKey()) == null) {
throw new NotFoundException("Input", inputPropertyValue.getKey(), "Input <" + inputPropertyValue.getKey()
+ "> cannot be found on topology for application <" + appId + "> environement <" + environmentId + ">");
}
propertyService.setPropertyValue(deploymentTopology.getInputProperties(), deploymentTopology.getInputs().get(inputPropertyValue.getKey()),
inputPropertyValue.getKey(), inputPropertyValue.getValue());
}
// update
if (MapUtils.isNotEmpty(updateRequest.getProviderDeploymentProperties())) {
deploymentTopology.getProviderDeploymentProperties().putAll(updateRequest.getProviderDeploymentProperties());
orchestratorPropertiesValidationService.checkConstraints(deploymentTopology.getOrchestratorId(),
updateRequest.getProviderDeploymentProperties());
}
deploymentTopologyService.updateDeploymentTopologyInputsAndSave(deploymentTopology);
} catch (ConstraintViolationException e) {
return RestResponseBuilder.<ConstraintUtil.ConstraintInformation> builder().data(e.getConstraintInformation())
.error(RestErrorBuilder.builder(RestErrorCode.PROPERTY_CONSTRAINT_VIOLATION_ERROR).message(e.getMessage()).build()).build();
} catch (ConstraintValueDoNotMatchPropertyTypeException e) {
return RestResponseBuilder.<ConstraintUtil.ConstraintInformation> builder().data(e.getConstraintInformation())
.error(RestErrorBuilder.builder(RestErrorCode.PROPERTY_TYPE_VIOLATION_ERROR).message(e.getMessage()).build()).build();
} finally {
ToscaContext.destroy();
}
return RestResponseBuilder.<DeploymentTopologyDTO> builder().data(deploymentTopologyHelper.buildDeploymentTopologyDTO(deploymentConfiguration)).build();
}
/**
* Security check on application and environment
*
* @param appId application's id
* @param environmentId environment's id
*/
private void checkAuthorizations(String appId, String environmentId) {
Application application = applicationService.getOrFail(appId);
ApplicationEnvironment environment = appEnvironmentService.getOrFail(environmentId);
// // Security check user must be authorized to deploy the environment (or be application manager)
AuthorizationUtil.checkAuthorizationForEnvironment(application, environment);
}
}