Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: terrakube actions implementation #853

Merged
merged 8 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading