package org.alien4cloud.tosca.editor; import java.io.IOException; import java.io.InputStream; import java.util.List; import javax.annotation.Resource; import javax.inject.Inject; import javax.validation.Valid; import io.swagger.annotations.Api; import org.alien4cloud.tosca.editor.operations.AbstractEditorOperation; import org.alien4cloud.tosca.editor.operations.UpdateFileOperation; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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.component.repository.IFileRepository; import alien4cloud.git.SimpleGitHistoryEntry; import alien4cloud.rest.model.RestResponse; import alien4cloud.rest.model.RestResponseBuilder; import alien4cloud.topology.TopologyDTO; import alien4cloud.topology.TopologyValidationResult; import io.swagger.annotations.ApiOperation; import springfox.documentation.annotations.ApiIgnore; /** * Controller endpoint for topology edition. */ @RestController @RequestMapping({ "/rest/v2/editor", "/rest/latest/editor" }) @Api public class EditorController { @Inject private EditorService editorService; @Inject private EditionContextManager editionContextManager; /** We use the artifact repository to store temporary files from the edition context. */ @Resource private IFileRepository artifactRepository; /** * Execute an operation on a topology. * * @param topologyId The id of the topology/archive under edition. * @param operation The operation to execute */ @ApiIgnore @RequestMapping(value = "/{topologyId:.+}/execute", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") public RestResponse<TopologyDTO> execute(@PathVariable String topologyId, @RequestBody @Valid AbstractEditorOperation operation) { TopologyDTO topologyDTO = editorService.execute(topologyId, operation); return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTO).build(); } /** * Undo or redo operations. * * @param topologyId The id of the topology under edition on which to undo operations. * @param at The index in the operations array to reach (0 means no operations, 1 means first operation etc.). * @param lastOperationId The id of the last operation from editor client point of view (for optimistic locking). * @return A topology DTO with the updated topology. */ @ApiIgnore @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/undo", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyDTO> undoRedo(@PathVariable String topologyId, @RequestParam("at") int at, @RequestParam("lastOperationId") String lastOperationId) { if (lastOperationId != null && "null".equals(lastOperationId)) { lastOperationId = null; } // Call the service that will save and commit TopologyDTO topologyDTO = editorService.undoRedo(topologyId, at, lastOperationId); return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTO).build(); } /** * Method exposed to REST to upload a file in an archive under edition. * * @param topologyId The id of the topology/archive under edition. * @param lastOperationId The id of the user last known operation (for optimistic locking edition). * @param path The path in which to save/override the file in the archive. * @param file The file to save in the archive. */ @ApiIgnore @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyDTO> upload(@PathVariable String topologyId, @RequestParam("lastOperationId") String lastOperationId, @RequestParam("path") String path, @RequestParam(value = "file") MultipartFile file) throws IOException { if (lastOperationId != null && "null".equals(lastOperationId)) { lastOperationId = null; } try (InputStream artifactStream = file.getInputStream()) { UpdateFileOperation updateFileOperation = new UpdateFileOperation(path, artifactStream); updateFileOperation.setPreviousOperationId(lastOperationId); TopologyDTO topologyDTO = editorService.execute(topologyId, updateFileOperation); return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTO).build(); } } /** * Download a temporary file which is not yet commited (uploaded or modified through an operation). * * @param topologyId The if of the topology. * @param artifactId The id of the temporary artifact. * @return The response entity with the input stream of the file. */ @ApiIgnore @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/file/{artifactId:.+}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<InputStreamResource> downloadTempFile(@PathVariable String topologyId, @PathVariable String artifactId) { editorService.checkAuthorization(topologyId); HttpHeaders headers = new HttpHeaders(); headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); headers.add("Pragma", "no-cache"); headers.add("Expires", "0"); long length = artifactRepository.getFileLength(artifactId); return ResponseEntity.ok().headers(headers).contentLength(length).contentType(MediaType.parseMediaType("application/octet-stream")) .body(new InputStreamResource(artifactRepository.getFile(artifactId))); } /** * Save the given topology and commit to the local git repository. * * @param topologyId The id of the topology/archive under edition to save. * @param lastOperationId The id of the last operation from editor client point of view (for optimistic locking). * @return A topology DTO with the updated topology. */ @ApiIgnore @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyDTO> save(@PathVariable String topologyId, @RequestParam("lastOperationId") String lastOperationId) { if (lastOperationId != null && "null".equals(lastOperationId)) { lastOperationId = null; } // Call the service that will save and commit TopologyDTO topologyDTO = editorService.save(topologyId, lastOperationId); return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTO).build(); } @ApiIgnore @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/isvalid", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyValidationResult> isTopologyValid(@PathVariable String topologyId) { TopologyValidationResult dto = editorService.validateTopology(topologyId); return RestResponseBuilder.<TopologyValidationResult> builder().data(dto).build(); } @ApiIgnore @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/history", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<List<SimpleGitHistoryEntry>> history(@PathVariable String topologyId, @RequestParam("from") int from, @RequestParam("count") int count) { List<SimpleGitHistoryEntry> historyEntries = editorService.history(topologyId, from, count); return RestResponseBuilder.<List<SimpleGitHistoryEntry>> builder().data(historyEntries).build(); } @ApiOperation(value = "Override the topology archive with the one provided as a parameter.", notes = "This operation will fail if the topology is under edition (meaning a context with some operations exists). The topology will be fully overriden with the new archive content (if valid) and a local commit will be dispatched.") @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/override", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<Void> updateTopologyArchive(@PathVariable String topologyId, @RequestParam(value = "file") MultipartFile file) throws IOException { try (InputStream inputStream = file.getInputStream()) { editorService.override(topologyId, inputStream); } return RestResponseBuilder.<Void> builder().build(); } /** * Recovers the topology after a dependency have change. This will apply the registered recovery operations and save the topology * * @param topologyId The id of the topology/archive under edition to save. * @return A topology DTO with the updated topology. */ @ApiOperation(value = "Recovers the topology after a dependency have change. This will apply the registered recovery operations and save the topology.", notes = "Application role required [ APPLICATION_MANAGER | APPLICATION_DEVOPS ]") @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/recover", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyDTO> recover(@PathVariable String topologyId, @RequestParam("lastOperationId") String lastOperationId) { if (lastOperationId != null && "null".equals(lastOperationId)) { lastOperationId = null; } TopologyDTO topologyDTO = editorService.recover(topologyId, lastOperationId); return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTO).build(); } /** * Reset a topology. This will delete everything inside the topology, leaving it as if it is just created now. * * @param topologyId The id of the topology/archive under edition to save. * @return A topology DTO with the updated topology. */ @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/reset", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyDTO> reset(@PathVariable String topologyId, @RequestParam("lastOperationId") String lastOperationId) { if (lastOperationId != null && "null".equals(lastOperationId)) { lastOperationId = null; } TopologyDTO topologyDTO = editorService.reset(topologyId, lastOperationId); return RestResponseBuilder.<TopologyDTO> builder().data(topologyDTO).build(); } /** * Clear the edition cache. * * @param force * @return Void */ @PreAuthorize("hasAnyAuthority('ADMIN')") @RequestMapping(value = "/clearCache", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<Void> clearCache(@RequestParam("force") Boolean force) { if (force) { editionContextManager.clearCache(); } return RestResponseBuilder.<Void> builder().build(); } /** * Pull modifications from a git repository. * If a conflict occurs when pulling the repository, an exception will be throw asking the end user to manually revolve the merge. * * @param topologyId The id of the topology. * @param gitUser The git credentials if any. * @param remoteBranch The name of the remote branch to pull from (default: 'master'). * @return An empty RestResponse. */ @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/git/pull", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<TopologyDTO> pull(@PathVariable String topologyId, @RequestBody EditorGitUserDTO gitUser, @RequestParam(name = "remoteBranch", defaultValue = "master", required = false) String remoteBranch) { return RestResponseBuilder.<TopologyDTO> builder().data(editorService.pull(topologyId, gitUser.getUsername(), gitUser.getPassword(), remoteBranch)) .build(); } /** * Push modifications to a git repository. * * If a conflict occurs when pushing the repository: * <ul> * <li>It will create push the current commits to a temporary branch.</li> * <li>Then will re-branch the local branch to the last commit of the remote branch.</li> * <li>Finally a runtime exception will be thrown asking the end user to manually revolve the merge.</li> * </ul> * * @param topologyId the id of the topology. * @param gitUser The git credentials if any. * @param remoteBranch The name of the remote branch to push to (default: 'master). * @return An empty RestResponse. */ @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/git/push", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<Void> push(@PathVariable String topologyId, @RequestBody EditorGitUserDTO gitUser, @RequestParam(name = "remoteBranch", defaultValue = "master", required = false) String remoteBranch) { editorService.push(topologyId, gitUser.getUsername(), gitUser.getPassword(), remoteBranch); return RestResponseBuilder.<Void> builder().build(); } /** * Set the remote git repository url. * * @param topologyId the id of the topology. * @param remoteUrl The git url of the remote repository. * @return An empty RestResponse. */ @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/git/remote", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<Void> setRemote(@PathVariable String topologyId, @RequestParam("remoteUrl") String remoteUrl) { editorService.setRemote(topologyId, "origin", remoteUrl); // The remote is always 'origin' right now. return RestResponseBuilder.<Void> builder().build(); } /** * Get the url of the git repository. * * @param topologyId The id of the topology. * @return The url of the git repository of the topology. */ @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/{topologyId:.+}/git/remote", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<EditorGitRemoteDTO> getRemote(@PathVariable String topologyId) { String remoteUrl = editorService.getRemoteUrl(topologyId, "origin"); // The remote is always 'origin' right now. return RestResponseBuilder.<EditorGitRemoteDTO> builder().data(new EditorGitRemoteDTO("origin", remoteUrl)).build(); } }