Skip to content

Commit

Permalink
feature: terrakube actions (#853)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcanizalez authored May 29, 2024
1 parent 506f281 commit bd1aa2e
Show file tree
Hide file tree
Showing 20 changed files with 1,680 additions and 81 deletions.
37 changes: 37 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
version: 2
updates:
# Maven: API project
- package-ecosystem: "maven"
directory: "/api"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"

# Yarn: React application
- package-ecosystem: "npm"
directory: "/ui"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"

# Maven: Executor project
- package-ecosystem: "maven"
directory: "/executor"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"

# Maven: Registry project
- package-ecosystem: "maven"
directory: "/registry"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.terrakube.api.plugin.proxy;

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/proxy/v1")
public class ProxyController {

private final ProxyService proxyService;

public ProxyController(ProxyService proxyService) {
this.proxyService = proxyService;
}

@GetMapping("/**")
public ResponseEntity<String> proxyGetRequest(RequestEntity<String> requestEntity, @RequestParam("targetUrl") String targetUrl, @RequestParam(value = "proxyheaders", required = false) String proxyHeaders, @RequestParam("workspaceId") UUID workspaceId) {
return proxyService.proxyRequest(requestEntity, targetUrl, proxyHeaders, workspaceId);
}

@PostMapping(value = "/**", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity<String> proxyPostRequest(RequestEntity<String> requestEntity, @RequestParam("targetUrl") String targetUrl, @RequestParam(value = "proxyheaders", required = false) String proxyHeaders, @RequestParam("workspaceId") UUID workspaceId) {
return proxyService.proxyRequest(requestEntity, targetUrl, proxyHeaders, workspaceId);
}

@PutMapping("/**")
public ResponseEntity<String> proxyPutRequest(RequestEntity<String> requestEntity, @RequestParam("targetUrl") String targetUrl, @RequestParam(value = "proxyheaders", required = false) String proxyHeaders, @RequestParam("workspaceId") UUID workspaceId) {
return proxyService.proxyRequest(requestEntity, targetUrl, proxyHeaders, workspaceId);
}

@DeleteMapping("/**")
public ResponseEntity<String> proxyDeleteRequest(RequestEntity<String> requestEntity, @RequestParam("targetUrl") String targetUrl, @RequestParam(value = "proxyheaders", required = false) String proxyHeaders, @RequestParam("workspaceId") UUID workspaceId) {
return proxyService.proxyRequest(requestEntity, targetUrl, proxyHeaders, workspaceId);
}

@PatchMapping("/**")
public ResponseEntity<String> proxyPatchRequest(RequestEntity<String> requestEntity, @RequestParam("targetUrl") String targetUrl, @RequestParam(value = "proxyheaders", required = false) String proxyHeaders, @RequestParam("workspaceId") UUID workspaceId) {
return proxyService.proxyRequest(requestEntity, targetUrl, proxyHeaders, workspaceId);
}
}
131 changes: 131 additions & 0 deletions api/src/main/java/org/terrakube/api/plugin/proxy/ProxyService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.terrakube.api.plugin.proxy;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import org.terrakube.api.repository.GlobalVarRepository;
import org.terrakube.api.repository.VariableRepository;
import org.terrakube.api.repository.WorkspaceRepository;
import org.terrakube.api.rs.Organization;
import org.terrakube.api.rs.workspace.Workspace;
import org.terrakube.api.rs.workspace.parameters.Variable;
import org.terrakube.api.rs.globalvar.Globalvar;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
@Service
public class ProxyService {

private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
private final WorkspaceRepository workspaceRepository;
private final VariableRepository variableRepository;
private final GlobalVarRepository globalVarRepository;

public static final Map<String, String> VARS = new HashMap<>();

public ProxyService(WorkspaceRepository workspaceRepository, VariableRepository variableRepository, GlobalVarRepository globalVarRepository) {
this.workspaceRepository = workspaceRepository;
this.variableRepository = variableRepository;
this.globalVarRepository = globalVarRepository;
}

@Transactional(propagation = Propagation.REQUIRED)
public ResponseEntity<String> proxyRequest(RequestEntity<String> requestEntity, String targetUrl, String proxyHeadersJson, UUID workspaceId) {
HttpMethod method = requestEntity.getMethod();
HttpHeaders headers = new HttpHeaders();

// Fetch workspace and variables
fetchWorkspaceVars(workspaceId);

// Replace variables in targetUrl
targetUrl = replaceVars(targetUrl);

// Add custom headers
if (proxyHeadersJson != null) {
try {
Map<String, String> customHeaders = objectMapper.readValue(proxyHeadersJson, Map.class);
customHeaders.forEach((key, value) -> headers.set(key, replaceVars(value)));
} catch (Exception e) {
log.error("Error parsing proxyheaders JSON: ", e);
}
}

String body = requestEntity.getBody();
if (body != null) {
try {
// Decode the body
String decodedBody = URLDecoder.decode(body, StandardCharsets.UTF_8);

// Extract proxyBody if present
JsonNode jsonNode = objectMapper.readTree(decodedBody);
JsonNode proxyBodyNode = jsonNode.get("proxyBody");
if (proxyBodyNode != null) {
String proxyBodyString = proxyBodyNode.asText();
// Replace variables in the proxy body
String replacedBody = replaceVars(proxyBodyString);

// Reassign the processed body
body = replacedBody;
}
} catch (Exception e) {
log.error("Error processing request body: ", e);
}
}

// Log headers and body for debugging
log.info("Request Headers: {}", headers);
log.info("Request Body: {}", body);
log.info("Target URL: {}", targetUrl);

HttpEntity<String> entity = new HttpEntity<>(body, headers);

try {
ResponseEntity<String> response = restTemplate.exchange(targetUrl, method, entity, String.class);
return ResponseEntity.status(response.getStatusCode()).body(response.getBody());
} catch (Exception e) {
log.error("Error forwarding request to {}: ", targetUrl, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error forwarding request");
}
}

@Transactional
public void fetchWorkspaceVars(UUID workspaceId) {
VARS.clear();
Workspace workspace = workspaceRepository.findById(workspaceId).orElseThrow(() -> new IllegalArgumentException("Invalid workspace ID"));
Organization organization = workspace.getOrganization();

List<Globalvar> globalVariables = globalVarRepository.findByOrganization(organization);
globalVariables.forEach(globalvar -> VARS.put(globalvar.getKey(), globalvar.getValue()));

List<Variable> variables = variableRepository.findByWorkspace(workspace);
variables.forEach(variable -> VARS.put(variable.getKey(), variable.getValue()));
}

public String replaceVars(String input) {
if (input == null) {
return null;
}

Pattern pattern = Pattern.compile("\\{\\{var\\.(\\w+)\\}\\}");
Matcher matcher = pattern.matcher(input);
StringBuffer buffer = new StringBuffer();

while (matcher.find()) {
String replacement = VARS.getOrDefault(matcher.group(1), matcher.group(0));
matcher.appendReplacement(buffer, replacement);
}
matcher.appendTail(buffer);
return buffer.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import lombok.extern.slf4j.Slf4j;
import org.terrakube.api.plugin.vcs.TokenService;

import com.amazonaws.services.s3.internal.eventstreaming.HeaderValue;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import org.terrakube.api.rs.globalvar.Globalvar;
import org.terrakube.api.rs.workspace.parameters.Category;

import java.util.List;
import java.util.UUID;

public interface GlobalVarRepository extends JpaRepository<Globalvar, UUID> {

Globalvar getGlobalvarByOrganizationAndCategoryAndKey(Organization organization, Category category, String key);
List<Globalvar> findByOrganization(Organization organization);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import org.springframework.data.jpa.repository.JpaRepository;
import org.terrakube.api.rs.workspace.parameters.Variable;
import org.terrakube.api.rs.workspace.Workspace;

import java.util.List;
import java.util.UUID;

public interface VariableRepository extends JpaRepository<Variable, UUID> {

List<Variable> findByWorkspace(Workspace organization);

}
53 changes: 53 additions & 0 deletions api/src/main/java/org/terrakube/api/rs/action/Action.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.terrakube.api.rs.action;

import com.yahoo.elide.annotation.CreatePermission;
import com.yahoo.elide.annotation.DeletePermission;
import com.yahoo.elide.annotation.Include;
import com.yahoo.elide.annotation.UpdatePermission;

import lombok.Getter;
import lombok.Setter;
import org.terrakube.api.plugin.security.audit.GenericAuditFields;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Include
@Getter
@Setter
@Entity(name = "action")
@CreatePermission(expression = "user is a superuser")
@UpdatePermission(expression = "user is a superuser")
@DeletePermission(expression = "user is a superuser")
public class Action extends GenericAuditFields {
@Id
private String id;

@Column(name = "type", nullable = false)
private String type;

@Column(name = "action", nullable = false, columnDefinition = "TEXT")
private String action;

@Column(name = "display_criteria", nullable = false, columnDefinition = "TEXT")
private String displayCriteria;

@Column(name = "name", nullable = false)
private String name;

@Column(name = "label", nullable = false)
private String label;

@Column(name = "category", nullable = false)
private String category;

@Column(name = "description")
private String description;

@Column(name = "version", nullable = false)
private String version;

@Column(name = "active", nullable = false)
private boolean active;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.terrakube.api.plugin.security.audit.GenericAuditFields;
import org.terrakube.api.rs.IdConverter;
import org.terrakube.api.rs.Organization;
import org.hibernate.annotations.Type;

import jakarta.persistence.*;

Expand Down
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 @@ -54,5 +54,6 @@
<include file="/db/changelog/local/changelog-2.20.0-registry-monorepo.xml"/>
<include file="/db/changelog/local/changelog-2.21.0-auto-apply.xml"/>
<include file="/db/changelog/local/changelog-2.21.0-gcp-dynamic.xml"/>
<include file="/db/changelog/local/changelog-2.22.0-actions.xml"/>
<include file="/db/changelog/local/changelog-2.21.0-has-changes.xml"/>
</databaseChangeLog>
Loading

0 comments on commit bd1aa2e

Please sign in to comment.