Skip to content

Commit

Permalink
feat: Added Rollback State (#1282)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcanizalez authored Sep 11, 2024
1 parent a00624d commit 9b45945
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.terrakube.api.plugin.storage.controller;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.ResponseEntity;
import org.terrakube.api.plugin.storage.StorageTypeService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.terrakube.api.repository.ArchiveRepository;
import org.terrakube.api.repository.HistoryRepository;
import org.terrakube.api.repository.WorkspaceRepository;
import org.terrakube.api.rs.workspace.history.History;
import org.terrakube.api.rs.workspace.history.archive.Archive;

import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -16,85 +19,160 @@
import java.util.Optional;
import java.util.UUID;

@AllArgsConstructor
@RestController
@Slf4j
@RequestMapping("/tfstate/v1")
public class TerraformStateController {

private StorageTypeService storageTypeService;
private final StorageTypeService storageTypeService;
private final ArchiveRepository archiveRepository;
private final WorkspaceRepository workspaceRepository;
private final HistoryRepository historyRepository;
private final String hostname;

private ArchiveRepository archiveRepository;

@GetMapping(
value = "/organization/{organizationId}/workspace/{workspaceId}/jobId/{jobId}/step/{stepId}/terraform.tfstate",
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE
)
public @ResponseBody byte[] getTerraformPlanBinary(@PathVariable("organizationId") String organizationId, @PathVariable("workspaceId") String workspaceId, @PathVariable("jobId") String jobId, @PathVariable("stepId") String stepId) {
public TerraformStateController(StorageTypeService storageTypeService,
ArchiveRepository archiveRepository,
WorkspaceRepository workspaceRepository,
HistoryRepository historyRepository,
@Value("${org.terrakube.hostname}") String hostname) {
this.storageTypeService = storageTypeService;
this.archiveRepository = archiveRepository;
this.workspaceRepository = workspaceRepository;
this.historyRepository = historyRepository;
this.hostname = hostname;
}
@GetMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/jobId/{jobId}/step/{stepId}/terraform.tfstate", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public @ResponseBody byte[] getTerraformPlanBinary(@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId, @PathVariable("jobId") String jobId,
@PathVariable("stepId") String stepId) {
return storageTypeService.getTerraformPlan(organizationId, workspaceId, jobId, stepId);
}

@GetMapping(
value = "/organization/{organizationId}/workspace/{workspaceId}/state/{stateFilename}.json",
produces = MediaType.APPLICATION_JSON_VALUE
)
public @ResponseBody byte[] getTerraformStateJson(@PathVariable("organizationId") String organizationId, @PathVariable("workspaceId") String workspaceId, @PathVariable("stateFilename") String stateFilename) {
@GetMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/state/{stateFilename}.json", produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody byte[] getTerraformStateJson(@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId, @PathVariable("stateFilename") String stateFilename) {
return storageTypeService.getTerraformStateJson(organizationId, workspaceId, stateFilename);
}

@GetMapping(
value = "/organization/{organizationId}/workspace/{workspaceId}/state/terraform.tfstate",
produces = MediaType.APPLICATION_JSON_VALUE
)
public @ResponseBody byte[] getCurrentTerraformState(@PathVariable("organizationId") String organizationId, @PathVariable("workspaceId") String workspaceId) {
@GetMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/state/terraform.tfstate", produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody byte[] getCurrentTerraformState(@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId) {
return storageTypeService.getCurrentTerraformState(organizationId, workspaceId);
}

@PutMapping(
value = "/archive/{archiveId}/terraform.tfstate",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<String> uploadHostedState(HttpServletRequest httpServletRequest, @PathVariable("archiveId") String archiveId) throws IOException {
@PutMapping(value = "/archive/{archiveId}/terraform.tfstate", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> uploadHostedState(HttpServletRequest httpServletRequest,
@PathVariable("archiveId") String archiveId) throws IOException {
log.info("uploadHostedState for: {}", archiveId);
Optional<Archive> archive = archiveRepository.findById(UUID.fromString(archiveId));
if (archive.isPresent()) {
Archive archiveData = archive.get();
String terraformState = IOUtils.toString(httpServletRequest.getInputStream(), StandardCharsets.UTF_8.name());
String terraformState = IOUtils.toString(httpServletRequest.getInputStream(),
StandardCharsets.UTF_8.name());
log.debug(terraformState);
storageTypeService.uploadState(
archiveData.getHistory().getWorkspace().getOrganization().getId().toString(),
archiveData.getHistory().getWorkspace().getId().toString(),
terraformState,
archiveData.getHistory().getId().toString()
);
archiveData.getHistory().getId().toString());
archiveRepository.deleteById(archiveData.getId());
return ResponseEntity.status(201).body("");
} else {
return ResponseEntity.status(403).body("");
}
}

@PutMapping(
value = "/archive/{archiveId}/terraform.json.tfstate",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<String> uploadJsonHostedState(HttpServletRequest httpServletRequest, @PathVariable("archiveId") String archiveId) throws IOException {
@PutMapping(value = "/archive/{archiveId}/terraform.json.tfstate", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> uploadJsonHostedState(HttpServletRequest httpServletRequest,
@PathVariable("archiveId") String archiveId) throws IOException {
log.info("uploadJsonHostedState for: {}", archiveId);
Optional<Archive> archive = archiveRepository.findById(UUID.fromString(archiveId));
if (archive.isPresent()) {
Archive archiveData = archive.get();
String terraformJsonState = IOUtils.toString(httpServletRequest.getInputStream(), StandardCharsets.UTF_8.name());
String terraformJsonState = IOUtils.toString(httpServletRequest.getInputStream(),
StandardCharsets.UTF_8.name());
log.debug(terraformJsonState);
storageTypeService.uploadTerraformStateJson(
archiveData.getHistory().getWorkspace().getOrganization().getId().toString(),
archiveData.getHistory().getWorkspace().getId().toString(),
terraformJsonState,
archiveData.getHistory().getId().toString()
);
archiveData.getHistory().getId().toString());
archiveRepository.deleteById(archiveData.getId());
return ResponseEntity.status(201).body("");
} else {
return ResponseEntity.status(403).body("");
}
}

@PutMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/rollback/{stateFilename}.json", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> rollbackToState(
@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId,
@PathVariable("stateFilename") String stateFilename) {

log.info("Rolling back workspace {} in organization {} to state {}", workspaceId, organizationId,
stateFilename);

try {
// Retrieve the previous JSON state
byte[] previousJsonState = storageTypeService.getTerraformStateJson(organizationId, workspaceId,
stateFilename);
if (previousJsonState == null || previousJsonState.length == 0) {
log.error("Failed to retrieve the JSON state: {}", stateFilename);
return ResponseEntity.status(404).body("JSON state not found");
}

// Retrieve the previous raw Terraform state by replacing ".json" with
// ".raw.json"
String rawStateFilename = stateFilename + ".raw";
byte[] previousRawState = storageTypeService.getTerraformStateJson(organizationId, workspaceId,
rawStateFilename);
if (previousRawState == null || previousRawState.length == 0) {
log.error("Failed to retrieve the raw Terraform state: {}", rawStateFilename);
return ResponseEntity.status(404).body("Raw Terraform state not found");
}

// Create a new history entry for the rollback
History newHistory = new History();
newHistory.setWorkspace(workspaceRepository.findById(UUID.fromString(workspaceId)).orElse(null));
newHistory.setSerial(1);
newHistory.setMd5("0");
newHistory.setLineage("0");
newHistory.setOutput(""); // Output will be updated with the new state URL
newHistory.setJobReference(stateFilename.replace(".json", "")); // Use the previous history id as the job reference
historyRepository.save(newHistory);

// Upload the previous JSON state as the current state
String jsonStateContent = new String(previousJsonState, StandardCharsets.UTF_8);
storageTypeService.uploadTerraformStateJson(
organizationId,
workspaceId,
jsonStateContent,
newHistory.getId().toString());

// Upload the previous raw Terraform state as the current state
String rawStateContent = new String(previousRawState, StandardCharsets.UTF_8);
storageTypeService.uploadState(
organizationId,
workspaceId,
rawStateContent,
newHistory.getId().toString());

// Update history output with new state URL
newHistory.setOutput(String.format("https://%s/tfstate/v1/organization/%s/workspace/%s/state/%s.json",
hostname,
organizationId,
workspaceId,
newHistory.getId().toString()));
historyRepository.save(newHistory);

log.info("State rollback successful for workspace: {}", workspaceId);
return ResponseEntity.status(201).body("Rollback successful");

} catch (Exception e) {
log.error("Error during rollback: {}", e.getMessage());
return ResponseEntity.status(500).body("Rollback failed");
}
}

}
1 change: 1 addition & 0 deletions api/src/main/resources/db/changelog/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@
<include file="/db/changelog/local/changelog-2.22.1-vcs-key-connection-type.xml"/>
<include file="/db/changelog/local/changelog-2.22.1-module-github-token-id.xml"/>
<include file="/db/changelog/local/changelog-2.22.2-webhook-triggers.xml"/>
<include file="/db/changelog/local/changelog-2.23.0-job-reference-size.xml"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="42" author="[email protected]">
<modifyDataType
columnName="job_reference"
newDataType="varchar2(36)"
tableName="history"/>
</changeSet>
</databaseChangeLog>
1 change: 1 addition & 0 deletions ui/src/domain/Workspaces/Details.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ export const WorkspaceDetails = ({ setOrganizationName, selectedTab }) => {
workspace={workspace.data}
organizationId={organizationId}
organizationName={organizationNameLocal}
onRollback={loadWorkspace}
/>
</TabPane>
<TabPane tab="Variables" key="4">
Expand Down
Loading

0 comments on commit 9b45945

Please sign in to comment.