Skip to content

Conversation

@michalskrivanek
Copy link
Contributor

@michalskrivanek michalskrivanek commented Sep 25, 2025

relies on labels jumpstarter.version and jumpstarter.revision on image, or standard OCI labels org.opencontainers.image.version and org.opencontainers.image.revision

Summary by CodeRabbit

  • New Features

    • Automatic container version detection for exporter images with registry lookups and comparison to running containers to drive updates and restarts.
  • Improvements

    • Apply command now validates configuration earlier to surface issues sooner.
    • SSH export operations avoid unnecessary restarts and show clearer status messages.
  • Bug Fixes

    • More robust handling when container label data is missing to reduce false positives.
  • Tests

    • Added unit tests covering host exporter filtering and skip logic.

@coderabbitai
Copy link

coderabbitai bot commented Sep 25, 2025

Walkthrough

Adds registry-based image label retrieval to config loading, surfaces labels on Config.ContainerVersions, enforces config validation in the apply command, implements container-version comparison and restart logic in SSH HostManager, and adds unit tests for host exporter filtering behavior.

Changes

Cohort / File(s) Summary
CLI apply validation
cmd/apply.go
Calls config_lint.ValidateWithError(cfg) after loading config and returns a wrapped error "config validation failed: %w" on validation failure (prevents proceeding to template applier creation).
Config struct
internal/config/config.go
Adds exported field ContainerVersions map[string]*container.ImageLabels; imports internal/container; ensures LoadConfig returns (&cfg, nil) on success.
Config loader / registry queries
internal/config/loader.go
After LoadAllResources, collects unique exporter images and populates cfg.ContainerVersions via container.GetImageLabelsFromRegistry; adds retrieveContainerVersionsFromExporters(...); logs retrieval results and stores empty labels on error.
Container image label utilities
internal/container/version.go
New ImageLabels type and functions: GetImageLabelsFromRegistry, GetRunningContainerLabels, plus Matches, IsEmpty, String; uses skopeo and podman to extract/compare labels.
SSH exporter host manager
internal/exporter/ssh/ssh.go
Extends HostManager interface (Diff, Apply, RunHostCommand); adds noValuePlaceholder; implements checkContainerVersion, checkDetailedContainerVersion, getRunningContainerLabels; integrates registry-vs-running label comparison and conditional restart logic into Apply; exposes RunHostCommand.
Host exporter tests
internal/exporter/host/host_test.go
Adds TestDeadAnnotationFiltering and TestHostSkippingBehavior to validate dead/alive instance filtering and host skipping decisions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as apply cmd
  participant Loader as LoadConfig / LoadAllResources
  participant Registry as Container Registry (skopeo)
  participant Lint as config_lint.ValidateWithError
  participant Applier as Template Applier

  User->>CLI: jumpstarter apply
  CLI->>Loader: LoadConfig()
  Loader->>Registry: GetImageLabelsFromRegistry(image) [for each unique image]
  Registry-->>Loader: ImageLabels / error
  Loader-->>CLI: cfg (with ContainerVersions)
  CLI->>Lint: ValidateWithError(cfg)
  alt validation ok
    CLI->>Applier: New(cfg) and proceed with apply
  else validation failed
    Lint-->>CLI: error -> CLI returns wrapped error
  end
Loading
sequenceDiagram
  autonumber
  participant SSH as SSHHostManager
  participant Podman as Remote Podman
  participant Registry as Config.ContainerVersions (cached)
  note right of SSH: During Apply for container-based exporter
  SSH->>Podman: GetRunningContainerLabels(serviceName)
  Podman-->>SSH: running ImageLabels (or empty)
  SSH->>Registry: lookup = cfg.ContainerVersions[image]
  Registry-->>SSH: registry ImageLabels (from loader)
  SSH->>SSH: compare running vs registry labels
  alt labels differ -> restart needed
    SSH->>SSH: print actions (dry-run vs real) and restart service
  else labels match or unknown -> no restart
    SSH-->>SSH: skip restart
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

Possibly related PRs

Poem

I nibble configs, sniff each label and tag,
I hop to registries and peek in the bag.
When running and registry disagree, I start;
If they match, I rest — a careful heart. 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the primary change, namely adding checks for running containers on exporter hosts, aligning directly with the PR’s focus on container version verification across hosts.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
internal/exporter/ssh/ssh.go (2)

287-299: Avoid duplicate registry lookups; reuse pre-fetched labels

This unconditionally fetches labels from the registry again during Apply. You already populate cfg.ContainerVersions during load. Plumb expected labels to this call (or add a small cache in the container package) to avoid repeated remote calls per host.


191-197: Align log messages with action (restart vs start)

The helper performs a restart, but messages say “start”. Adjust for clarity.

Apply:

-				fmt.Printf("        ❌ Failed to start service %s: %v\n", serviceName, enableErr)
+				fmt.Printf("        ❌ Failed to restart service %s: %v\n", serviceName, enableErr)
@@
-				fmt.Printf("        ✅ Service %s started\n", serviceName)
+				fmt.Printf("        ✅ Service %s restarted\n", serviceName)
internal/config/loader.go (3)

309-314: Avoid network I/O during config load or gate it

Fetching container labels during LoadAllResources increases startup latency and couples loading with external calls. Consider:

  • Deferring to the validation phase (and handle its errors) or
  • Making it optional via a flag or
  • Using a cache so apply-time checks reuse these results.

318-348: Parallelize label retrieval for multiple images

Retrieval is fully serial. For many images, this will be slow. Use a bounded worker pool with context cancellation, and collect results into the map.

I can provide a goroutine-based worker implementation if desired.


332-344: Prefer structured logging over stdout prints in library code

Emojis/prints are fine for CLI, but loader functions are library-like. Consider using a logger with levels or returning the info to the caller to print.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f608da and f97fac9.

📒 Files selected for processing (4)
  • cmd/apply.go (1 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/loader.go (2 hunks)
  • internal/exporter/ssh/ssh.go (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (.cursorrules)

**/*.go: Run golangci-lint via make lint and fix all linting errors before proceeding
Run make build to ensure the binary compiles successfully and fix any build errors
Run make fmt to ensure code formatting (go fmt) before finalizing changes

Files:

  • cmd/apply.go
  • internal/config/config.go
  • internal/exporter/ssh/ssh.go
  • internal/config/loader.go
🧬 Code graph analysis (2)
internal/config/config.go (2)
internal/vars/vars.go (1)
  • Variables (11-14)
internal/config/loader.go (1)
  • LoadedLabConfig (24-37)
internal/exporter/ssh/ssh.go (1)
api/v1alpha1/exporterconfigtemplate_types.go (1)
  • ExporterConfigTemplate (74-80)
🪛 GitHub Check: Run jumpstarter-lab-config lint
internal/config/config.go

[failure] 7-7:
no required module provides package github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container; to add it:

🪛 GitHub Actions: Tests
internal/config/config.go

[error] 7-7: No required module provides package 'github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container'.

🔇 Additional comments (3)
internal/config/config.go (2)

7-7: Import references missing internal/container package (build break)

Same issue as flagged in ssh.go; the package isn’t present per CI. Ensure the package is added or adjust types to remove this dependency.

See the verification script in the ssh.go comment. As per coding guidelines


48-53: LGTM: corrected LoadConfig return handling

Returning (&cfg, nil) on success after an explicit error check is correct.

internal/config/loader.go (1)

12-12: Import references missing internal/container package (build break)

This is another occurrence; add the package or refactor to avoid it.

Use the same repository scan script provided in ssh.go. As per coding guidelines

Comment on lines 62 to 66
config_lint.Validate(cfg)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Handle validation errors; fail fast on invalid config

The return value from config_lint.Validate(cfg) is ignored. This likely violates linting and allows applying invalid configs.

Apply:

-		config_lint.Validate(cfg)
+		if err := config_lint.Validate(cfg); err != nil {
+			return fmt.Errorf("config validation failed: %w", err)
+		}

If Validate returns a slice of errors, aggregate them. To confirm signature:

As per coding guidelines


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP '(?s)^package\s+config_lint\b.*?func\s+Validate\s*\(' -C2 --type=go

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== cmd/apply.go (first 100 lines) ==="
head -n 100 cmd/apply.go || true

echo
echo "=== config_lint.Validate call context (±2 lines) ==="
rg -nC2 'config_lint\.Validate' cmd/apply.go || true

echo
echo "=== Files declaring package config_lint ==="
rg -l 'package config_lint' -g '*.go' || true

echo
echo "=== All Validate function signatures in repo ==="
rg -n 'func Validate' --type=go || true

Length of output: 3904


Use ValidateWithError to handle config validation errors
Replace the call to config_lint.Validate(cfg) with:

if err := config_lint.ValidateWithError(cfg); err != nil {
    return fmt.Errorf("config validation failed: %w", err)
}

This ensures invalid configs cause a fast failure.

🤖 Prompt for AI Agents
In cmd/apply.go around lines 62-63, the current call to
config_lint.Validate(cfg) ignores validation failures; replace it with a call to
config_lint.ValidateWithError(cfg), check the returned error, and if non-nil
return a wrapped error using fmt.Errorf("config validation failed: %w", err) so
invalid configs cause an immediate failure.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
cmd/apply.go (1)

62-64: Handle config validation errors.

config_lint.Validate(cfg) still discards failures, so invalid configs proceed to apply. Please switch to the error-returning helper and fail fast.

-		config_lint.Validate(cfg)
+		if err := config_lint.ValidateWithError(cfg); err != nil {
+			return fmt.Errorf("config validation failed: %w", err)
+		}

As per coding guidelines

internal/exporter/ssh/ssh.go (1)

330-332: Quote serviceName in the podman inspect command.

The unquoted placeholder lets spaces or shell metacharacters break the command (or worse, allows injection). Please wrap it with %q before passing to runCommand.

-	result, err := m.runCommand(fmt.Sprintf("podman inspect --format '{{index .Config.Labels \"jumpstarter.version\"}} {{index .Config.Labels \"jumpstarter.revision\"}} {{index .Config.Labels \"org.opencontainers.image.version\"}} {{index .Config.Labels \"org.opencontainers.image.revision\"}}' %s", serviceName))
+	result, err := m.runCommand(fmt.Sprintf("podman inspect --format '{{index .Config.Labels \"jumpstarter.version\"}} {{index .Config.Labels \"jumpstarter.revision\"}} {{index .Config.Labels \"org.opencontainers.image.version\"}} {{index .Config.Labels \"org.opencontainers.image.revision\"}}' %q", serviceName))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f97fac9 and 1c036f7.

📒 Files selected for processing (6)
  • cmd/apply.go (1 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/loader.go (2 hunks)
  • internal/container/version.go (1 hunks)
  • internal/exporter/host/host_test.go (1 hunks)
  • internal/exporter/ssh/ssh.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/config/loader.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (.cursorrules)

**/*.go: Run golangci-lint via make lint and fix all linting errors before proceeding
Run make build to ensure the binary compiles successfully and fix any build errors
Run make fmt to ensure code formatting (go fmt) before finalizing changes

Files:

  • internal/container/version.go
  • internal/exporter/ssh/ssh.go
  • internal/config/config.go
  • cmd/apply.go
  • internal/exporter/host/host_test.go
**/*_test.go

📄 CodeRabbit inference engine (.cursorrules)

Run the full test suite via make test and ensure all tests pass before considering the iteration complete

Files:

  • internal/exporter/host/host_test.go
🧬 Code graph analysis (3)
internal/exporter/ssh/ssh.go (1)
internal/container/version.go (2)
  • GetImageLabelsFromRegistry (17-57)
  • ImageLabels (11-14)
internal/config/config.go (2)
internal/config/loader.go (1)
  • LoadedLabConfig (24-37)
internal/container/version.go (1)
  • ImageLabels (11-14)
internal/exporter/host/host_test.go (1)
api/v1alpha1/exporterinstance_types.go (1)
  • ExporterInstance (71-77)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c036f7 and 40233fa.

📒 Files selected for processing (6)
  • cmd/apply.go (1 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/loader.go (2 hunks)
  • internal/container/version.go (1 hunks)
  • internal/exporter/host/host_test.go (1 hunks)
  • internal/exporter/ssh/ssh.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/container/version.go
  • internal/exporter/host/host_test.go
  • cmd/apply.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (.cursorrules)

**/*.go: Run golangci-lint via make lint and fix all linting errors before proceeding
Run make build to ensure the binary compiles successfully and fix any build errors
Run make fmt to ensure code formatting (go fmt) before finalizing changes

Files:

  • internal/exporter/ssh/ssh.go
  • internal/config/loader.go
  • internal/config/config.go
🧠 Learnings (1)
📚 Learning: 2025-10-03T16:50:32.459Z
Learnt from: michalskrivanek
PR: jumpstarter-dev/jumpstarter-lab-config#21
File: internal/exporter/ssh/ssh.go:330-333
Timestamp: 2025-10-03T16:50:32.459Z
Learning: In internal/exporter/ssh/ssh.go, the podman inspect command for serviceName should use %s (not %q) formatting, as adding quotes breaks the command execution. The serviceName is passed directly to podman as a container name argument and does not require shell quoting.

Applied to files:

  • internal/exporter/ssh/ssh.go
🧬 Code graph analysis (3)
internal/exporter/ssh/ssh.go (1)
internal/container/version.go (2)
  • GetImageLabelsFromRegistry (17-57)
  • ImageLabels (11-14)
internal/config/loader.go (1)
internal/container/version.go (2)
  • ImageLabels (11-14)
  • GetImageLabelsFromRegistry (17-57)
internal/config/config.go (2)
internal/config/loader.go (1)
  • LoadedLabConfig (24-37)
internal/container/version.go (1)
  • ImageLabels (11-14)

relies on labels jumpstarter.version and jumpstarter.revision on image, or standard OCI labels
org.opencontainers.image.version and org.opencontainers.image.revision
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40233fa and 77c725c.

📒 Files selected for processing (6)
  • cmd/apply.go (1 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/loader.go (2 hunks)
  • internal/container/version.go (1 hunks)
  • internal/exporter/host/host_test.go (1 hunks)
  • internal/exporter/ssh/ssh.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • cmd/apply.go
  • internal/container/version.go
  • internal/config/loader.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (.cursorrules)

**/*.go: Run golangci-lint via make lint and fix all linting errors before proceeding
Run make build to ensure the binary compiles successfully and fix any build errors
Run make fmt to ensure code formatting (go fmt) before finalizing changes

Files:

  • internal/config/config.go
  • internal/exporter/ssh/ssh.go
  • internal/exporter/host/host_test.go
**/*_test.go

📄 CodeRabbit inference engine (.cursorrules)

Run the full test suite via make test and ensure all tests pass before considering the iteration complete

Files:

  • internal/exporter/host/host_test.go
🧠 Learnings (1)
📚 Learning: 2025-10-03T16:50:32.459Z
Learnt from: michalskrivanek
PR: jumpstarter-dev/jumpstarter-lab-config#21
File: internal/exporter/ssh/ssh.go:330-333
Timestamp: 2025-10-03T16:50:32.459Z
Learning: In internal/exporter/ssh/ssh.go, the podman inspect command for serviceName should use %s (not %q) formatting, as adding quotes breaks the command execution. The serviceName is passed directly to podman as a container name argument and does not require shell quoting.

Applied to files:

  • internal/exporter/ssh/ssh.go
🧬 Code graph analysis (3)
internal/config/config.go (2)
internal/config/loader.go (1)
  • LoadedLabConfig (24-37)
internal/container/version.go (1)
  • ImageLabels (11-14)
internal/exporter/ssh/ssh.go (1)
internal/container/version.go (2)
  • GetImageLabelsFromRegistry (17-57)
  • ImageLabels (11-14)
internal/exporter/host/host_test.go (1)
api/v1alpha1/exporterinstance_types.go (1)
  • ExporterInstance (71-77)
🔇 Additional comments (9)
internal/config/config.go (3)

7-7: LGTM!

The import of internal/container is correct and the package exists per the relevant code snippets.


17-17: LGTM!

The new ContainerVersions field is well-typed and properly documented with the yaml:"-" tag to exclude it from serialization.


48-52: LGTM! Error handling fix.

The change from returning (&cfg, err) to returning (nil, err) on error is correct. Previously, returning a partially loaded config alongside an error was confusing for callers. This change ensures consistent error handling semantics.

internal/exporter/host/host_test.go (1)

130-224: LGTM with caveat!

The test logic correctly validates host skipping behavior. However, this test will inherit the same semantic issue flagged in the previous comment regarding how the "dead" annotation is interpreted (presence vs. value).

internal/exporter/ssh/ssh.go (5)

33-35: LGTM!

The noValuePlaceholder constant correctly captures the Podman/Docker placeholder for missing labels.


253-268: LGTM! Container version checking integrated correctly.

The logic appropriately checks container versions only when the service is running, avoiding unnecessary checks during service restarts. Error handling is correct with non-fatal errors for version checks.


270-284: LGTM!

The function correctly limits version checking to container-based exporters only, with proper early returns for non-container exporters.


286-325: LGTM! Robust error handling.

The detailed version comparison logic is well-structured:

  • Non-fatal error handling (warnings only) prevents version check failures from blocking operations
  • Clear user feedback for version mismatches in both dry-run and real execution modes
  • Appropriate conditional restart based on version comparison

327-375: LGTM! Past review issues resolved.

The implementation correctly addresses all previous feedback:

  • Uses %s (not %q) for serviceName formatting (as per learnings)
  • Uses strings.Split on newlines instead of strings.Fields, preserving <no value> placeholder intact
  • Normalizes <no value> to empty string correctly (lines 346-358)
  • Prioritizes jumpstarter labels over OCI labels, matching the logic in internal/container/version.go

Based on learnings

return fmt.Errorf("error loading config file %s: %w", configFilePath, err)
}

config_lint.Validate(cfg)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
config_lint.Validate(cfg)

Should we remove this line since we have the other?

Copy link
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

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

just one nit not worth keeping this PR waiting, we can do it later.

@mangelajo mangelajo merged commit adae770 into jumpstarter-dev:main Oct 8, 2025
4 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.

2 participants