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: Persist configuration in local Git repository #5

Merged
merged 3 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# User-specific stuff
.idea/
.config/

# File-based project format
*.iws
Expand Down
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
annotationProcessor 'org.projectlombok:lombok'

// JGit for accessing configurations in Git repositories
implementation 'org.eclipse.jgit:org.eclipse.jgit:7.0.0.202409031743-r'
// Actuator - for management endpoints
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Swagger - for API documentation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.application.config;

import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import rocks.inspectit.gepard.agentmanager.configuration.service.GitService;

@Configuration
@AllArgsConstructor
public class GitConfiguration {

private final GitService gitService;

@PostConstruct
public void initialize() {
gitService.initializeLocalRepository();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.configuration.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import rocks.inspectit.gepard.agentmanager.configuration.model.InspectitConfiguration;
import rocks.inspectit.gepard.agentmanager.exception.JsonParseException;

@Service
@AllArgsConstructor
public class ConfigurationService {

private volatile InspectitConfiguration inspectitConfiguration;
private final GitService gitService;

private final ObjectMapper objectMapper;

public InspectitConfiguration getConfiguration() {
return inspectitConfiguration;
try {
return objectMapper.readValue(gitService.getFileContent(), InspectitConfiguration.class);
} catch (JsonProcessingException e) {
throw new JsonParseException(
"Failed to deserialize JSON content to InspectitConfiguration", e);
}
}

public void updateConfiguration(InspectitConfiguration configuration) {
inspectitConfiguration = configuration;
try {
String jsonContent = objectMapper.writeValueAsString(configuration);
gitService.updateFileContent(jsonContent);
gitService.commit();
} catch (JsonProcessingException e) {
throw new JsonParseException("Failed to serialize InspectitConfiguration to JSON", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.configuration.service;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import rocks.inspectit.gepard.agentmanager.exception.GitOperationException;

@Slf4j
@Service
public class GitService {

private static final String FILE = "configuration.json";

@Value("${inspectit-config-server.configurations.local-path}")
private String localRepoPath;

private Git git;

public void commit() {
try {
git.add().addFilepattern(FILE).call();
git.commit().setMessage("update configuration").call();
} catch (GitAPIException e) {
throw new GitOperationException("Failed to commit changes to local repository", e);
}
}

public void updateFileContent(String content) {
try {
File file = new File(localRepoPath + "/configuration.json");
java.nio.file.Files.write(file.toPath(), content.getBytes());
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
} catch (IOException e) {
throw new GitOperationException("Failed to update file content", e);
}
}

public String getFileContent() {
try {
Path filePath = Path.of(localRepoPath, FILE);
if (Files.exists(filePath)) {
return Files.readString(filePath);
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw new IOException("File not found: " + FILE);
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (IOException e) {
throw new GitOperationException("Failed to read file content", e);
}
}

public void initializeLocalRepository() {
Path path = Path.of(localRepoPath);

try {
if (!Files.exists(path)) {
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
log.warn("Local repository does not exist, creating local repository");
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
git = Git.init().setDirectory(path.toFile()).call();
} else {
log.info("Local repository found, using local repository");
git = Git.open(path.toFile());
}
} catch (GitAPIException gitAPIException) {
throw new GitOperationException("Failed to initialize local repository", gitAPIException);
} catch (IOException ioException) {
throw new GitOperationException("Failed to open local repository", ioException);
}
}
}
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.exception;

public class FileNotFoundException extends RuntimeException {
public FileNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.exception;

public class GitOperationException extends RuntimeException {
public GitOperationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ResponseEntity<ApiError> handleValidationErrors(
* @param ex the exception
* @return the response entity
*/
@ExceptionHandler({HttpMessageNotReadableException.class})
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleBadRequestError(
HttpMessageNotReadableException ex, HttpServletRequest request) {
ApiError apiError =
Expand Down Expand Up @@ -83,4 +83,40 @@ public ResponseEntity<ApiError> handleNotFoundError(
LocalDateTime.now());
return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(FileNotFoundException.class)
public ResponseEntity<ApiError> handleFileNotFoundError(
FileNotFoundException ex, HttpServletRequest request) {
ApiError apiError =
new ApiError(
request.getRequestURI(),
List.of(ex.getMessage()),
HttpStatus.NOT_FOUND.value(),
LocalDateTime.now());
return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(GitOperationException.class)
public ResponseEntity<ApiError> handleFileNotFoundError(
GitOperationException ex, HttpServletRequest request) {
ApiError apiError =
new ApiError(
request.getRequestURI(),
List.of(ex.getMessage()),
HttpStatus.NOT_FOUND.value(),
LocalDateTime.now());
return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(JsonParseException.class)
public ResponseEntity<ApiError> handleJsonParseError(
JsonParseException ex, HttpServletRequest request) {
ApiError apiError =
new ApiError(
request.getRequestURI(),
List.of(ex.getMessage()),
HttpStatus.NOT_FOUND.value(),
LocalDateTime.now());
return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.exception;

public class JsonParseException extends RuntimeException {
public JsonParseException(String message, Throwable cause) {
super(message, cause);
}
}
4 changes: 3 additions & 1 deletion backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ inspectit-config-server:
cors:
path-pattern: "/**"
allowed-origins: "*"
allowed-methods: "*"
allowed-methods: "*"
configurations:
local-path: ".config"
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand Down Expand Up @@ -52,11 +53,9 @@ void updateConfiguration_shouldReturnOkAndConfiguration() throws Exception {

InspectitConfiguration configuration = createConfiguration();

when(configurationService.getConfiguration()).thenReturn(configuration);

mockMvc
.perform(
get("/api/v1/agent-configuration")
put("/api/v1/agent-configuration")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(configuration)))
.andExpect(status().isOk());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.configuration.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import rocks.inspectit.gepard.agentmanager.configuration.model.InspectitConfiguration;
import rocks.inspectit.gepard.agentmanager.configuration.model.instrumentation.InstrumentationConfiguration;
import rocks.inspectit.gepard.agentmanager.configuration.model.instrumentation.Scope;
import rocks.inspectit.gepard.agentmanager.exception.JsonParseException;

@ExtendWith(MockitoExtension.class)
class ConfigurationServiceTest {

@InjectMocks private ConfigurationService configurationService;

@Mock private GitService gitService;

@Spy private ObjectMapper objectMapper;

private InspectitConfiguration configuration;

@BeforeEach
public void setUp() {
Scope scope = new Scope("org.domain.test", List.of("method1", "method2"), true);
InstrumentationConfiguration instrumentationConfiguration =
new InstrumentationConfiguration(List.of(scope));
configuration = new InspectitConfiguration(instrumentationConfiguration);
}

@Test
void testGetConfiguration_SuccessfulDeserialization() throws JsonProcessingException {
String fileContent =
"""
{
"instrumentation": {
"scopes": [
{
"fqn": "org.domain.test1",
"methods": [
"method1",
"method2"
],
"enabled": true
}
]
}
}
""";
when(gitService.getFileContent()).thenReturn(fileContent);
when(objectMapper.readValue(fileContent, InspectitConfiguration.class))
.thenReturn(configuration);

configurationService.getConfiguration();

verify(gitService, times(1)).getFileContent();
}

@Test
void testGetConfiguration_NotSuccessfulDeserialization() {
String fileContent =
"""
{
"wrongAttributeName": {
}
}
""";
when(gitService.getFileContent()).thenReturn(fileContent);

Exception exception =
assertThrows(JsonParseException.class, () -> configurationService.getConfiguration());
assertEquals(
"Failed to deserialize JSON content to InspectitConfiguration", exception.getMessage());
}

@Test
void testUpdateConfiguration_SuccessfulSerialization() {
configurationService.updateConfiguration(configuration);

verify(gitService, times(1)).updateFileContent(anyString());
verify(gitService, times(1)).commit();
}

@Test
void testUpdateConfiguration_NotSuccessfulSerialization() throws JsonProcessingException {
when(objectMapper.writeValueAsString(any())).thenThrow(JsonProcessingException.class);

Exception exception =
assertThrows(
JsonParseException.class,
() -> configurationService.updateConfiguration(configuration));

verify(gitService, times(0)).updateFileContent(anyString());
verify(gitService, times(0)).commit();
assertEquals("Failed to serialize InspectitConfiguration to JSON", exception.getMessage());
}
}
Loading