-
Notifications
You must be signed in to change notification settings - Fork 0
Add DvcService to retrieve NVR list by version from DVC repository #97
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
Merged
Merged
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
d127ef6
Add DvcService to retrieve NVR list by version from DVC repository.
operetz-rh d031df5
Merge branch 'main' into feature/dvc-service-nvr-retrieval
operetz-rh 4387385
fixx pom.xml: add missing closing tag for snakeyaml dependency
operetz-rh 0fe3f41
specify UTF-8 encoding explicitly for string conversions
operetz-rh b007e1b
-Create DvcException for better exception handling
operetz-rh 7193aa9
add input validation function
operetz-rh 549075b
remove duplicate version validation
operetz-rh 4f3f2b4
wrap yaml parse with try/catch
operetz-rh 8f478eb
introduce ProcessExecutor to run dvc commands
JudeNiroshan 43b333e
add aws creds as env vars on deployment.yaml
operetz-rh 42031d8
Update pom.xml
operetz-rh 1cf1ffc
wrapped ProcessExecutor in try-finally block to ensure flag will alwa…
operetz-rh 67c783b
wrapped yaml parsing with try-catch block to ensure runtimeExceptions…
operetz-rh 475e486
restrict DVC binary permissions to owner-only
operetz-rh 474a06f
Merge branch 'main' into feature/dvc-service-nvr-retrieval
operetz-rh ab0388b
fix Dockerfile.jvm conflict before merge
operetz-rh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/main/java/com/redhat/sast/api/exceptions/DvcException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.redhat.sast.api.exceptions; | ||
|
|
||
| /** | ||
| * Exception thrown when DVC operations fail. | ||
| * This includes failures in fetching data from DVC repositories, | ||
| * parsing DVC YAML files, or DVC command execution errors. | ||
| */ | ||
| public class DvcException extends RuntimeException { | ||
|
|
||
| /** | ||
| * Constructs a new DvcException with the specified detail message. | ||
| * | ||
| * @param message the detail message | ||
| */ | ||
| public DvcException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| /** | ||
| * Constructs a new DvcException with the specified detail message and cause. | ||
| * | ||
| * @param message the detail message | ||
| * @param cause the cause of this exception | ||
| */ | ||
| public DvcException(String message, Throwable cause) { | ||
| super(message, cause); | ||
| } | ||
| } |
203 changes: 203 additions & 0 deletions
203
src/main/java/com/redhat/sast/api/service/DvcService.java
operetz-rh marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| package com.redhat.sast.api.service; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import org.eclipse.microprofile.config.inject.ConfigProperty; | ||
| import org.yaml.snakeyaml.LoaderOptions; | ||
| import org.yaml.snakeyaml.Yaml; | ||
| import org.yaml.snakeyaml.constructor.SafeConstructor; | ||
|
|
||
| import com.redhat.sast.api.exceptions.DvcException; | ||
|
|
||
| import jakarta.enterprise.context.ApplicationScoped; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @ApplicationScoped | ||
| @Slf4j | ||
| public class DvcService { | ||
|
|
||
| @ConfigProperty(name = "dvc.repo.url") | ||
| String dvcRepoUrl; | ||
|
|
||
| @ConfigProperty(name = "dvc.batch.yaml.path") | ||
| String batchYamlPath; | ||
|
|
||
| /** | ||
| * Get list of NVRs from DVC repository by version tag | ||
| * Fetches YAML file from DVC and extracts NVR list | ||
| * | ||
| * @param version DVC version tag (e.g., "1.0.0" or "v1.0.0") | ||
| * @return List of package NVR strings (empty list if no NVRs found) | ||
| * @throws DvcException if DVC fetch fails or parsing fails | ||
| * @throws IllegalArgumentException if version is null or empty | ||
| */ | ||
| public List<String> getNvrListByVersion(String version) { | ||
operetz-rh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| LOGGER.info("Fetching NVR list from DVC repository: version={}", version); | ||
| LOGGER.debug("Fetching YAML from DVC: path={}", batchYamlPath); | ||
|
|
||
| String yamlContent = fetchFromDvc(batchYamlPath, version); | ||
operetz-rh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| LOGGER.debug("Raw YAML content from DVC ({} bytes)", yamlContent.length()); | ||
JudeNiroshan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Parse YAML to extract NVRs | ||
| Object data; | ||
| try { | ||
| LoaderOptions loaderOptions = new LoaderOptions(); | ||
| Yaml yaml = new Yaml(new SafeConstructor(loaderOptions)); | ||
| data = yaml.load(yamlContent); | ||
| } catch (RuntimeException e) { | ||
| LOGGER.error("Failed to parse YAML content for version {}: {}", version, e.getMessage(), e); | ||
| throw new DvcException("Failed to parse YAML content for version " + version, e); | ||
| } | ||
|
|
||
| List<String> nvrList = new ArrayList<>(); | ||
|
|
||
| if (data instanceof java.util.Map) { | ||
| // YAML has a map structure, find list of strings | ||
| java.util.Map<String, Object> map = (java.util.Map<String, Object>) data; | ||
| for (Object value : map.values()) { | ||
| if (value instanceof List) { | ||
| List<?> list = (List<?>) value; | ||
| // Validate each item individually | ||
| for (Object item : list) { | ||
| if (item instanceof String) { | ||
| nvrList.add((String) item); | ||
| } else { | ||
| LOGGER.warn("Skipping non-string item in NVR list: {}", item); | ||
| } | ||
| } | ||
| if (!nvrList.isEmpty()) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } else if (data instanceof List) { | ||
| // YAML is just a list of NVRs | ||
| List<?> list = (List<?>) data; | ||
| for (Object item : list) { | ||
| if (item instanceof String) { | ||
| nvrList.add((String) item); | ||
| } else { | ||
| LOGGER.warn("Skipping non-string item in NVR list: {}", item); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (nvrList.isEmpty()) { | ||
| LOGGER.warn("No NVRs found in YAML for DVC version {}", version); | ||
| return nvrList; | ||
| } | ||
|
|
||
| LOGGER.info("Successfully retrieved {} NVRs from YAML (DVC version {})", nvrList.size(), version); | ||
| LOGGER.debug("NVR list: {}", nvrList); | ||
| return nvrList; | ||
| } | ||
|
|
||
| /** | ||
| * Validates DVC command inputs to prevent command injection - ALIGNED WITH some of DVCMETADATASERVICE validations | ||
| * | ||
| * @param filePath Path to file in DVC repo | ||
| * @param version DVC version tag | ||
| * @throws IllegalArgumentException if inputs contain unsafe characters | ||
| */ | ||
| private void validateDvcInputs(String filePath, String version) { | ||
| // Validate filePath | ||
| if (filePath == null || filePath.isBlank()) { | ||
| throw new IllegalArgumentException("DVC filePath cannot be null or empty"); | ||
| } | ||
|
|
||
| // Prevent directory traversal attacks | ||
| if (filePath.contains("..")) { | ||
| throw new IllegalArgumentException("DVC filePath cannot contain '..' (directory traversal)"); | ||
| } | ||
|
|
||
| // Allow only safe characters in file path: alphanumeric, dash, underscore, dot, forward slash | ||
| if (!filePath.matches("^[a-zA-Z0-9._/-]+$")) { | ||
| throw new IllegalArgumentException("DVC filePath contains invalid characters: " + filePath); | ||
| } | ||
|
|
||
| // Validate version matches DvcMetadataService.validateDataVersion() for consistency | ||
| if (version == null || version.isBlank()) { | ||
| throw new IllegalArgumentException("DVC version cannot be null or empty"); | ||
| } | ||
|
|
||
| // Prevent ReDoS by limiting input length | ||
| if (version.length() > 100) { | ||
| String displayVersion = version.substring(0, 50) + "..."; | ||
| throw new IllegalArgumentException("DVC version too long (max 100 characters): " + displayVersion); | ||
| } | ||
|
|
||
| if (!version.matches( | ||
| "^(v?\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9]+)?(?:\\+[a-zA-Z0-9]+)?|[a-zA-Z][a-zA-Z0-9_-]{0,49}|\\d{4}-\\d{2}-\\d{2})$")) { | ||
| throw new IllegalArgumentException("Invalid DVC version format: '" + version | ||
| + "' - expected semantic version (v1.0.0) or valid identifier"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetches raw file content from DVC repository using DVC CLI | ||
| * | ||
| * @param filePath Path to file in DVC repo | ||
| * @param version DVC version tag | ||
| * @return File content as String | ||
| * @throws DvcException if DVC command fails or times out | ||
| */ | ||
| private String fetchFromDvc(String filePath, String version) { | ||
| // Validate inputs to prevent command injection | ||
| validateDvcInputs(filePath, version); | ||
|
|
||
| LOGGER.debug("Executing DVC get command: repo={}, path={}, version={}", dvcRepoUrl, filePath, version); | ||
|
|
||
| java.nio.file.Path tempFile = null; | ||
| Process process = null; | ||
| try { | ||
| tempFile = java.nio.file.Files.createTempFile("dvc-fetch-", ".tmp"); | ||
|
|
||
| ProcessBuilder processBuilder = new ProcessBuilder( | ||
| "dvc", "get", dvcRepoUrl, filePath, "--rev", version, "-o", tempFile.toString(), "--force"); | ||
|
|
||
| process = processBuilder.start(); | ||
|
|
||
| // read stderr for error messages | ||
| String error = new String(process.getErrorStream().readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); | ||
|
|
||
| boolean finished = process.waitFor(60, TimeUnit.SECONDS); | ||
| if (!finished) { | ||
| LOGGER.error("DVC command timed out after 60 seconds"); | ||
| throw new DvcException("DVC command timed out after 60 seconds"); | ||
| } | ||
|
|
||
| int exitCode = process.exitValue(); | ||
|
|
||
| if (exitCode != 0) { | ||
| LOGGER.error("DVC command failed with exit code {}: {}", exitCode, error); | ||
| throw new DvcException("Failed to fetch data from DVC (exit code " + exitCode + "): " + error); | ||
| } | ||
|
|
||
| // read content from temp file - the nvrs content | ||
| String output = java.nio.file.Files.readString(tempFile, java.nio.charset.StandardCharsets.UTF_8); | ||
| LOGGER.debug("Successfully fetched {} bytes from DVC", output.length()); | ||
| return output; | ||
| } catch (IOException e) { | ||
| throw new DvcException("I/O error during DVC fetch operation", e); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new DvcException("DVC fetch operation was interrupted", e); | ||
| } finally { | ||
| // force kill process if still running | ||
| if (process != null && process.isAlive()) { | ||
| process.destroyForcibly(); | ||
| } | ||
| // clean up temp file | ||
| if (tempFile != null) { | ||
| try { | ||
| java.nio.file.Files.deleteIfExists(tempFile); | ||
| } catch (IOException e) { | ||
| LOGGER.warn("Failed to delete temp file: {}", tempFile, e); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.