Skip to content

Conversation

@michalskrivanek
Copy link
Contributor

@michalskrivanek michalskrivanek commented Oct 8, 2025

Summary by CodeRabbit

  • New Features
    • Graceful service restarts via SIGHUP to reduce downtime when applicable.
  • Bug Fixes
    • Avoids unnecessary restarts during ongoing updates.
    • Attempts to start services that are not running instead of always deferring.
    • More robust container label parsing to prevent missed version checks.
  • Refactor
    • Streamlined config validation; apply now proceeds even when validation issues are detected.
  • Chores
    • Added per-image debug logs during container version checks for easier troubleshooting.

@coderabbitai
Copy link

coderabbitai bot commented Oct 8, 2025

Walkthrough

Removes error propagation from config validation in cmd/apply.go. Adds per-image debug logging in internal/config/loader.go. Overhauls restart/control flow in internal/exporter/ssh/ssh.go: introduces restartGracefully (SIGHUP via podman/systemd), bootc-aware restart deferral, refined container label parsing, and minor messaging changes. Adds HostManager.Diff() and noValuePlaceholder (unexported).

Changes

Cohort / File(s) Summary
CLI apply validation flow
cmd/apply.go
Dropped config_lint.ValidateWithError(cfg) error handling; now calls config_lint.Validate(cfg) without returning validation errors. Control flow no longer exits on validation failure.
Config loader logging
internal/config/loader.go
Added debug log per image before registry query in retrieveContainerVersionsFromExporters; no logic changes to retrieval or error handling.
SSH exporter restart/control
internal/exporter/ssh/ssh.go
Replaced restartService with restartGracefully using SIGHUP (podman kill -s SIGHUP, fallback systemctl kill -s SIGHUP). Updated Apply to avoid restarts during bootc updating, attempt direct restart if service not running, and use new helper in version checks. Revised label extraction via single podman inspect format and strings.Fields; normalized noValuePlaceholder. Public surface: added HostManager.Diff(); introduced unexported noValuePlaceholder constant.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CLI as cmd/apply
  participant Lint as config_lint

  U->>CLI: run apply
  CLI->>Lint: Validate(cfg)
  note right of Lint: No error returned to caller
  Lint-->>CLI: validation result (non-fatal)
  CLI-->>U: continue apply flow
Loading
sequenceDiagram
  autonumber
  participant Orchestrator as SSH Exporter Apply
  participant Host as HostManager
  participant Podman as podman
  participant Systemd as systemd

  Orchestrator->>Host: get BootcStatus
  alt bootc updating
    Orchestrator-->>Host: skip immediate restarts
    note right of Orchestrator: Defer restarts during update
  else normal
    Orchestrator->>Podman: inspect running container labels (formatted)
    Podman-->>Orchestrator: labels (normalized)
    Orchestrator->>Host: check container version vs desired
    alt service running and needs reload
      rect rgba(200,255,200,0.2)
      Orchestrator->>Podman: kill -s SIGHUP <container>
      Podman-->>Orchestrator: ok or error
      end
      opt fallback
        Orchestrator->>Systemd: systemctl kill -s SIGHUP <service>
        Systemd-->>Orchestrator: ok or error
      end
    else service not running
      Orchestrator->>Host: attempt restart (or log dry-run)
      Host-->>Orchestrator: result
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo

Poem

A bunny taps the keys—hop, tap, hop—
Swaps harsh restarts for a gentle SIGHUP bop.
Logs peep per image, secrets kept tight,
Bootc’s mid-munch? We’ll pause the bite.
Configs don’t scold, they softly imply—
Ship it with whiskers, and watch it fly! 🐰✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “graceful restart” succinctly reflects the primary change of introducing a restartGracefully mechanism and updating service restart logic in the SSH exporter, making it clear and relevant to the main update.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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: 1

♻️ Duplicate comments (2)
internal/exporter/ssh/ssh.go (1)

17-17: Import relies on internal/container (see config.go comment).

Same build-break risk applies here. Refer to the critical comment in internal/config/config.go Lines 7-8.

internal/config/loader.go (1)

12-12: Import relies on internal/container (see config.go comment).

Same build-break risk; refer to critical comment in internal/config/config.go Lines 7-7.

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

252-254: Make service-running check robust using exit code.

Relying on exact stdout "active\n" is brittle. Prefer --quiet with exit code.

Apply this diff:

-        statusResult, err := m.runCommand("systemctl is-active " + fmt.Sprintf("%q", svcName))
-        serviceRunning := err == nil && statusResult.Stdout == "active\n"
+        statusResult, err := m.runCommand("systemctl is-active --quiet " + fmt.Sprintf("%q", svcName))
+        serviceRunning := err == nil && statusResult.ExitCode == 0

336-385: Parse labels via JSON instead of space-splitting.

strings.Fields breaks if a label contains spaces and depends on the "" placeholder. Prefer {{json .Config.Labels}} and decode.

Apply this diff to the function:

 func (m *SSHHostManager) getRunningContainerLabels(serviceName string) (*container.ImageLabels, error) {
-    // Try jumpstarter labels first, then fall back to OCI standard labels
-    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))
+    // Inspect labels as JSON and parse reliably
+    result, err := m.runCommand(fmt.Sprintf("podman inspect --format '{{json .Config.Labels}}' %q", serviceName))
     if err != nil {
         return nil, fmt.Errorf("failed to inspect container %s: %w", serviceName, err)
     }
 
-    parts := strings.Fields(strings.TrimSpace(result.Stdout))
-    // Pad with empty strings if we got fewer parts
-    for len(parts) < 4 {
-        parts = append(parts, "")
-    }
-
-    jumpstarterVersion := parts[0]  // jumpstarter.version
-    jumpstarterRevision := parts[1] // jumpstarter.revision
-    ociVersion := parts[2]          // org.opencontainers.image.version
-    ociRevision := parts[3]         // org.opencontainers.image.revision
-
-    // Clean up "<no value>" to empty string
-    if jumpstarterVersion == noValuePlaceholder {
-        jumpstarterVersion = ""
-    }
-    if jumpstarterRevision == noValuePlaceholder {
-        jumpstarterRevision = ""
-    }
-    if ociVersion == noValuePlaceholder {
-        ociVersion = ""
-    }
-    if ociRevision == noValuePlaceholder {
-        ociRevision = ""
-    }
+    labelsJSON := strings.TrimSpace(result.Stdout)
+    var labels map[string]string
+    if labelsJSON != "" {
+        if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
+            return nil, fmt.Errorf("failed to parse container labels for %s: %w", serviceName, err)
+        }
+    } else {
+        labels = map[string]string{}
+    }
+
+    jumpstarterVersion := strings.TrimSpace(labels["jumpstarter.version"])
+    jumpstarterRevision := strings.TrimSpace(labels["jumpstarter.revision"])
+    ociVersion := strings.TrimSpace(labels["org.opencontainers.image.version"])
+    ociRevision := strings.TrimSpace(labels["org.opencontainers.image.revision"])
 
     // Use jumpstarter labels if both exist, otherwise fall back to OCI labels
     var version, revision string
     if jumpstarterVersion != "" && jumpstarterRevision != "" {
         version = jumpstarterVersion
         revision = jumpstarterRevision
     } else {
         version = ociVersion
         revision = ociRevision
     }
 
     // Return labels even if only partially available (version OR revision can be empty)
     return &container.ImageLabels{
         Version:  version,
         Revision: revision,
     }, nil
 }

Add import:

// at top of file
import "encoding/json"
internal/config/loader.go (2)

309-314: Attaching ContainerVersions at load-time: OK; consider gating.

Fetching registry metadata during config load can be slow. Consider lazy retrieval, caching, or a flag to skip in performance-sensitive paths.


318-349: Consider parallel fetch with timeout and basic caching.

To improve UX:

  • Use context with timeout per image (e.g., 5–10s).
  • Fetch in parallel (errgroup with bounded concurrency).
  • Cache results across runs to avoid re-querying unchanged images.

I can draft this if desired.

📜 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 354103e.

📒 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:

  • internal/exporter/ssh/ssh.go
  • internal/config/config.go
  • cmd/apply.go
  • internal/config/loader.go
🧬 Code graph analysis (1)
internal/config/config.go (1)
internal/config/loader.go (1)
  • LoadedLabConfig (24-37)
🪛 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:

🔇 Additional comments (6)
internal/exporter/ssh/ssh.go (3)

33-35: Good: normalize Podman’s "" placeholder.


189-201: Graceful restart flow LGTM.

Clear SIGHUP signaling with podman/systemd fallback and bootc-upgrade skip.

Also applies to: 223-230, 248-248


279-334: Version-aware restart logic is sensible; confirm image pull semantics.

Flow is conservative (non-fatal on errors) and restarts only when versions differ. Ensure the systemd container unit or Quadlet uses an “pull on restart/auto-update” policy; SIGHUP alone won’t fetch newer images unless configured (e.g., ImagePullPolicy=always or podman auto-update).

Can you confirm the container unit ensures fresh image on restart?

cmd/apply.go (1)

63-64: No-op formatting change. LGTM.

internal/config/config.go (2)

13-18: Config.ContainerVersions addition looks good.

Properly non-serialized (yaml:"-") surfacing version data to callers.


47-53: Load error handling is correct.

Returning nil on LoadAllResources error avoids partially-initialized configs.

"os"
"path/filepath"

"github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container"
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

Build break: missing internal/container package or wrong import path.

CI shows: “no required module provides package github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container”. Ensure the package exists in-repo and exports ImageLabels, IsEmpty/Matches/String, and GetImageLabelsFromRegistry, or fix the import path/module name.

Run this to verify presence and expected symbols:

Also, please run:

  • make fmt
  • make build
  • make lint
    As per coding guidelines.

🏁 Script executed:

#!/bin/bash
# Check package directory exists
fd -t d '^internal/container$' -H

# Confirm package declaration and key symbols
rg -n --type=go -C1 '^\s*package\s+container\b'
rg -n --type=go '\btype\s+ImageLabels\b'
rg -n --type=go '\bGetImageLabelsFromRegistry\s*\('

# Also ensure module path matches repo path (go.mod)
rg -n '^module\s+'

Length of output: 274


Invalid import: missing internal/container package

The import github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container doesn’t exist—add the directory with its package container and expected symbols or fix the import path.

🧰 Tools
🪛 GitHub Check: Run jumpstarter-lab-config lint

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

🤖 Prompt for AI Agents
internal/config/config.go around line 7: the import
"github.com/jumpstarter-dev/jumpstarter-lab-config/internal/container" is
invalid because that package path does not exist; either create an
internal/container directory with a Go file declaring "package container" and
exporting the symbols referenced by config.go (types/functions/constants), or
correct the import to the proper existing module path; after adding or fixing
the package, run go mod tidy and update any references in config.go to match the
exported names from package container.

just sending SIGHUP to trigger restart only once it's not leased. Expecting the service configuration is set up to pull a newer image on startup (i.e. upgrade)
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: 0

🧹 Nitpick comments (1)
internal/exporter/ssh/ssh.go (1)

40-40: Document or remove the unimplemented Diff() stub

No callers reference SSHHostManager.Diff() and it returns a placeholder; either remove it from the interface until it’s implemented or add a clear // TODO and documentation marking it as pending functionality.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 354103e and 6b8b003.

📒 Files selected for processing (3)
  • cmd/apply.go (0 hunks)
  • internal/config/loader.go (1 hunks)
  • internal/exporter/ssh/ssh.go (4 hunks)
💤 Files with no reviewable changes (1)
  • cmd/apply.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/config/loader.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
🔇 Additional comments (8)
internal/exporter/ssh/ssh.go (8)

33-35: LGTM!

The noValuePlaceholder constant is well-defined and improves code maintainability by avoiding magic strings when handling missing container labels.


248-248: LGTM!

The call to restartGracefully is consistent with the function rename and maintains the same parameters.


257-266: LGTM! Logic is correct given the bootc guard.

The direct restart approach for non-running services is appropriate since the bootc update check (lines 223-230) already gates this code path. When the service isn't running, a full restart is more suitable than a graceful signal.


269-269: LGTM!

Passing restartGracefully to checkContainerVersion is consistent with the function rename and maintains the correct signature.


339-339: LGTM! More efficient label retrieval.

The consolidated format string efficiently retrieves all four labels in a single podman inspect call, reducing command overhead while maintaining correct handling of missing labels.


344-348: LGTM! Correct parsing approach.

Using strings.Fields with padding is the right approach for parsing space-separated label values from the consolidated format string. This handles edge cases where fewer than four labels are present.


356-367: LGTM! Proper placeholder normalization.

The normalization of noValuePlaceholder to empty strings ensures consistent handling of missing labels throughout the codebase.


189-201: Fix typo and align podman kill with container names

  • Replace “wating” with “waiting” in the function comment.
  • Podman kill requires a container name, not a systemd service. Confirm that your container names match serviceName (or look up the actual container name before invoking podman); otherwise, use systemctl kill for both.

@mangelajo mangelajo merged commit 8d80a3c into jumpstarter-dev:main Oct 15, 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