Skip to content

fix(image): gracefully handle missing Docker/Podman engine#539

Merged
ruromero merged 2 commits into
guacsec:mainfrom
ruromero:fix/docker-podman-graceful-fallback
Jun 25, 2026
Merged

fix(image): gracefully handle missing Docker/Podman engine#539
ruromero merged 2 commits into
guacsec:mainfrom
ruromero:fix/docker-podman-graceful-fallback

Conversation

@ruromero

@ruromero ruromero commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • ImageUtils.hostInfo() now returns an empty string instead of throwing RuntimeException when the container engine binary is missing (IOException) or returns error output
  • This allows callers to fall back gracefully rather than crashing when neither Docker nor Podman is installed
  • Updated test to assert empty string return instead of exception

Test plan

  • Verify image analysis works normally when Docker is available
  • Verify image analysis degrades gracefully when neither Docker nor Podman is installed
  • Run existing ImageUtilsTest suite

🤖 Generated with Claude Code

Summary by Sourcery

Handle missing container engine binaries gracefully when retrieving image host info.

Bug Fixes:

  • Return an empty string from ImageUtils.hostInfo when the Docker/Podman binary is missing or returns only error output instead of throwing a RuntimeException.

Tests:

  • Update ImageUtilsTest to assert an empty string result when no Docker path is available instead of expecting an exception.

@sourcery-ai

sourcery-ai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

ImageUtils.hostInfo() is changed to return an empty string instead of throwing when the Docker/Podman engine is missing or returns only error output, and the corresponding unit test is updated to assert the new behavior.

Sequence diagram for ImageUtils.hostInfo graceful fallback

sequenceDiagram
    participant Caller
    participant ImageUtils
    participant Operations

    Caller->>ImageUtils: hostInfo(engine, info)
    ImageUtils->>Operations: getCustomPathOrElse(engine)
    Operations-->>ImageUtils: exec
    ImageUtils->>Operations: runProcessGetFullOutput(null, cmd, null)
    alt IOException during runProcessGetFullOutput
        Operations-->>ImageUtils: RuntimeException(cause=IOException)
        ImageUtils-->>Caller: "" 
    else Successful process execution
        Operations-->>ImageUtils: ProcessExecOutput
        alt output.getOutput().isEmpty() and (output.getError() not empty or exitCode != 0)
            ImageUtils-->>Caller: ""
        else Normal output
            ImageUtils-->>Caller: output
        end
    end
Loading

File-Level Changes

Change Details Files
Handle missing or failing container engine by returning an empty string from hostInfo() instead of throwing.
  • Wrap Operations.runProcessGetFullOutput in a try/catch for RuntimeException and return empty string when the cause is an IOException, allowing graceful degradation when the engine binary is missing.
  • Change the logic that previously threw a RuntimeException on non-empty stderr or non-zero exit code to instead return an empty string, treating it as a soft failure.
  • Keep the normal successful path unchanged, still returning the parsed JSON string from the process output.
src/main/java/io/github/guacsec/trustifyda/image/ImageUtils.java
Adjust ImageUtils hostInfo unit test to assert empty string instead of exception for missing docker path scenario.
  • Update the test to call ImageUtils.hostInfo and assert that it returns an empty string when the mocked process output has empty stdout and non-empty stderr.
  • Remove assertions that a RuntimeException is thrown and that its message matches the error output.
src/test/java/io/github/guacsec/trustifyda/image/ImageUtilsTest.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Using an empty string as the sentinel for "engine unavailable" makes it hard to distinguish from a legitimate empty docker info output; consider returning an Optional<String> or a dedicated result type instead so callers can explicitly distinguish error from content.
  • Catching a generic RuntimeException and then inspecting getCause() for IOException is a bit brittle and obscures the real failure mode; if possible, narrow the exception type at the Operations.runProcessGetFullOutput boundary (e.g., by exposing checked exceptions or a domain-specific exception) so this method can handle only the intended error cases.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using an empty string as the sentinel for "engine unavailable" makes it hard to distinguish from a legitimate empty `docker info` output; consider returning an `Optional<String>` or a dedicated result type instead so callers can explicitly distinguish error from content.
- Catching a generic `RuntimeException` and then inspecting `getCause()` for `IOException` is a bit brittle and obscures the real failure mode; if possible, narrow the exception type at the `Operations.runProcessGetFullOutput` boundary (e.g., by exposing checked exceptions or a domain-specific exception) so this method can handle only the intended error cases.

## Individual Comments

### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/image/ImageUtils.java" line_range="288-289" />
<code_context>
+    try {
+      output = Operations.runProcessGetFullOutput(null, cmd, null);
+    } catch (RuntimeException e) {
+      if (e.getCause() instanceof IOException) {
+        return "";
+      }
+      throw e;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider preserving error information (e.g., via logging) before returning an empty string on IO failures.

This behavior makes IO failures indistinguishable from a successful call that just returns no data, which complicates debugging and monitoring. If you want to avoid throwing, please at least log the exception (including the message), or use a distinct sentinel/status object so callers can tell the difference between “no info available” and “failed to get info.”

Suggested implementation:

```java
    Operations.ProcessExecOutput output;
    try {
      output = Operations.runProcessGetFullOutput(null, cmd, null);
    } catch (RuntimeException e) {
      if (e.getCause() instanceof IOException) {
        LOGGER.warn("Failed to run '{}' info command due to I/O error", exec, e.getCause());
        return "";
      }
      throw e;
    }

```

To compile successfully you also need to ensure that:
1. The class defines a logger named `LOGGER`, for example:
   `private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(ImageUtils.class);`
2. If SLF4J is not the logging framework used in this project, replace `LOGGER.warn(...)` with the appropriate logging call consistent with the rest of the codebase.
</issue_to_address>

### Comment 2
<location path="src/test/java/io/github/guacsec/trustifyda/image/ImageUtilsTest.java" line_range="551-552" />
<code_context>
-      var exception =
-          assertThrows(RuntimeException.class, () -> ImageUtils.hostInfo("docker", "info"));
-      assertEquals("test-error", exception.getMessage());
+      var result = ImageUtils.hostInfo("docker", "info");
+      assertEquals("", result);
     }
   }
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test ensuring non-IO-related RuntimeExceptions are still propagated

The new test covers the empty-string behavior when error output is returned, but it doesn’t verify that non‑IO `RuntimeException`s are still propagated.

Please add a dedicated test (e.g., `test_host_info_other_runtime_exceptions_propagate`) that mocks `Operations.runProcessGetFullOutput` to throw a `RuntimeException` with a non‑IO cause (or no cause), and uses `assertThrows` to confirm `ImageUtils.hostInfo` rethrows it. This will clearly distinguish expected graceful failures from unexpected errors and protect against regressions.

Suggested implementation:

```java
      var result = ImageUtils.hostInfo("docker", "info");
      assertEquals("", result);
    }

    @Test
    void test_host_info_other_runtime_exceptions_propagate() {
      var runtimeException = new RuntimeException("non-io-error");

      // Configure Operations.runProcessGetFullOutput to throw a non-IO-related RuntimeException
      // following the same argument pattern as the other hostInfo tests.
      operations
          .when(
              () ->
                  Operations.runProcessGetFullOutput(
                      eq("docker"), isNull(), aryEq(new String[] {"docker", "info"}), isNull()))
          .thenThrow(runtimeException);

      var thrown =
          assertThrows(RuntimeException.class, () -> ImageUtils.hostInfo("docker", "info"));

      // Ensure the same RuntimeException instance is propagated
      assertSame(runtimeException, thrown);
    }
  }

```

To make this compile and integrate correctly with the rest of the test file, you will also need to ensure:
1. The existing static mock object for `Operations` is named `operations` and is in scope for this test (the new test assumes the same mock field/variable used by the other hostInfo tests).
2. The following static imports are present at the top of the file (if they are not already):
   - `import static org.junit.jupiter.api.Assertions.assertThrows;`
   - `import static org.junit.jupiter.api.Assertions.assertSame;`
   - `import static org.mockito.ArgumentMatchers.eq;`
3. If the test class uses a different pattern for mocking (e.g., `try (MockedStatic<Operations> operations = mockStatic(Operations.class)) { ... }` inside each test instead of a shared `operations` field), adjust the new test to follow that pattern:
   - Wrap the mocking and assertions in the same `try (MockedStatic<Operations> operations = mockStatic(Operations.class)) { ... }` structure.
   - Use the same argument matcher combination (`eq`, `isNull`, `aryEq`, etc.) as the existing hostInfo tests.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/main/java/io/github/guacsec/trustifyda/image/ImageUtils.java
@codecov-commenter

codecov-commenter commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@7d20efe). Learn more about missing BASE report.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #539   +/-   ##
=======================================
  Coverage        ?   68.72%           
  Complexity      ?     1000           
=======================================
  Files           ?       65           
  Lines           ?     4253           
  Branches        ?      744           
=======================================
  Hits            ?     2923           
  Misses          ?      990           
  Partials        ?      340           
Flag Coverage Δ
integration-tests 68.72% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ruromero ruromero requested a review from a-oren June 25, 2026 00:17

@a-oren a-oren left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

ruromero and others added 2 commits June 25, 2026 11:59
ImageUtils.hostInfo() now returns an empty string instead of throwing
RuntimeException when the container engine binary is missing (IOException)
or returns error output. This allows callers to fall back gracefully
rather than crashing when neither Docker nor Podman is installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Address Sourcery review feedback: log a warning when a container engine
is not available instead of silently returning empty, and add a test
verifying that non-IO RuntimeExceptions still propagate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ruromero ruromero force-pushed the fix/docker-podman-graceful-fallback branch from 95be102 to f493385 Compare June 25, 2026 09:59
@ruromero ruromero merged commit 2db003f into guacsec:main Jun 25, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants