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 2 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
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,13 @@ It´s purpose is to replace the InspectIT Ocelot project with a more modern and
Currently, the project is in an early stage of development and not yet ready for production use.
However, if you want to try it out, you can follow the instructions in [CONTRIBUTING](./CONTRIBUTING.md).

## Local Setup
To persist the Config-Server configuration, we create a local Git repository. The path to the repository needs to be provided in the `application.yaml` with the property `inspectit-config-server.configurations.local-path`, e.g.
```
inspectit-config-server:
configurations:
local-path: ".config"
```

## Useful Links
- [SonarCloud](https://sonarcloud.io/)
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
@@ -0,0 +1,79 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.configuration.file;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class FileAccessor {

private final Path filePath;

private final Lock readLock;

private final Lock writeLock;

private FileAccessor(Path filePath, ReadWriteLock readWriteLock) {
this.filePath = filePath;
this.readLock = readWriteLock.readLock();
this.writeLock = readWriteLock.writeLock();
}

/**
* Factory method to create a {@link FileAccessor}
*
* @param filePath the path of the accessible file
* @return the created accessor
*/
public static FileAccessor create(Path filePath) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
return new FileAccessor(filePath, readWriteLock);
}

/**
* Tries to read data from a file.
*
* @return the content of the file
*/
public String readFile() throws IOException {
readLock.lock();
try {
if (Files.notExists(filePath)) throw new FileNotFoundException("File not found: " + filePath);

if (!Files.isReadable(filePath))
throw new AccessDeniedException("File is not readable: " + filePath);

byte[] rawFileContent = Files.readAllBytes(filePath);
return new String(rawFileContent);
} finally {
readLock.unlock();
}
}

/**
* Tries to write data into a file.
*
* @param content the data, which should be written into the file
*/
public void writeFile(String content) throws IOException {
writeLock.lock();
try {
if (Files.notExists(filePath)) {
Files.createDirectories(filePath.getParent());
Files.createFile(filePath);
}

if (!Files.isWritable(filePath))
throw new AccessDeniedException("File is not writable: " + filePath);

Files.write(filePath, content.getBytes());
} finally {
writeLock.unlock();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
/* (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;

/**
* Retrieves the current configuration from the local Git repository.
*
* @return a valid {@link InspectitConfiguration}
*/
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);
}
}

/**
* Updates or creates an {@link InspectitConfiguration }
*
* @param configuration The configuration to be saved
*/
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,110 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.configuration.service;

import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
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.configuration.file.FileAccessor;
import rocks.inspectit.gepard.agentmanager.exception.FileAccessException;
import rocks.inspectit.gepard.agentmanager.exception.GitOperationException;

/** Service-Implementation for communication with Git repositories. */
@Slf4j
@Service
public class GitService {

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

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

private FileAccessor fileAccessor;

private Git git;

@PostConstruct
public void init() {
fileAccessor = FileAccessor.create(Path.of(localRepoPath + "/" + FILE));
}

/**
* Add and commit the current changes to the local Git repository.
*
* @throws GitOperationException if commiting the changes fails
*/
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);
}
}

/**
* Update the content of the current configuration file.
*
* @param content the content to be saved a {@link String}
* @throws FileAccessException if updating the file fails
*/
public void updateFileContent(String content) {
try {
fileAccessor.writeFile(content);
} catch (IOException e) {
throw new FileAccessException("Failed to update file content", e);
}
}

/**
* Retrieve the current content of the configuration file.
*
* @return the current configuration as {@link String}
* @throws FileAccessException if reading the file fails
*/
public String getFileContent() {
try {
return fileAccessor.readFile();
} catch (IOException e) {
throw new FileAccessException("Failed to read file content", e);
}
}

/**
* Initialize the local Git repository. If there is no local repository found, a new one is
* created. Otherwise, the existing one is used.
*
* @throws GitOperationException if the initialization of the local repository fails or if it
* fails to open the local repository
*/
public void initializeLocalRepository() {
File file = Path.of(localRepoPath).toFile();

try {
if (!isGitRepository(file)) {
log.info("Local repository does not exist, creating local repository");
git = Git.init().setDirectory(file).call();
} else {
log.info("Local repository found, using local repository");
git = Git.open(file);
}
} catch (GitAPIException gitAPIException) {
throw new GitOperationException("Failed to initialize local repository", gitAPIException);
} catch (IOException ioException) {
throw new GitOperationException("Failed to open local repository", ioException);
}
}

private boolean isGitRepository(File file) {
try (Git ignored = Git.open(file)) {
return true;
} catch (IOException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.exception;

public class FileAccessException extends RuntimeException {
public FileAccessException(String message, Throwable cause) {
super(message, cause);
}
}
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 @@ -18,12 +18,7 @@
@ControllerAdvice
public class GlobalExceptionHandler {

/**
* Handles MethodArgumentNotValidException (e.g. if a NotNull-Field isn´t present).
*
* @param ex the exception
* @return the response entity
*/
/** Handles MethodArgumentNotValidException (e.g. if a NotNull-Field isn´t present). */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidationErrors(
MethodArgumentNotValidException ex, HttpServletRequest request) {
Expand All @@ -37,13 +32,8 @@ public ResponseEntity<ApiError> handleValidationErrors(
return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST);
}

/**
* Handles HttpMessageNotReadableException (e.g. if no request body is provided).
*
* @param ex the exception
* @return the response entity
*/
@ExceptionHandler({HttpMessageNotReadableException.class})
/** Handles HttpMessageNotReadableException (e.g. if no request body is provided). */
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleBadRequestError(
HttpMessageNotReadableException ex, HttpServletRequest request) {
ApiError apiError =
Expand Down Expand Up @@ -83,4 +73,46 @@ public ResponseEntity<ApiError> handleNotFoundError(
LocalDateTime.now());
return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND);
}

/** Handles FileAccessException (e.g. if a file could not be read). */
@ExceptionHandler(FileAccessException.class)
public ResponseEntity<ApiError> handleFileAccessException(
FileAccessException 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);
binarycoded marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Handles GitOperationException (e.g. if the initialization of the local repository fails or if
* it fails to open the local repository)
*/
@ExceptionHandler(GitOperationException.class)
public ResponseEntity<ApiError> handleGitOperationException(
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);
}

/** Handles JsonParseException (e.g. if (de-) serialization of JSON fails */
@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);
}
}
Loading