package firecracker import ( "context" "fmt" "regexp" "strconv" "strings" ) // MinSupportedVersion is the lowest firecracker version banger has // been validated against. Below this, banger refuses to launch — the // jailer flags banger relies on (notably the `--exec-file` / // `--chroot-base-dir` pair plus the structured chroot layout) might // behave differently or be missing entirely. // // Bumping this is a deliberate decision; it should change in lockstep // with whatever feature in the helper started requiring the newer // firecracker. const MinSupportedVersion = "1.5.0" // KnownTestedVersion is the firecracker version banger's smoke suite // is currently exercised against. Newer versions usually work // (firecracker keeps its API stable within a major) but they sit // outside the tested window — `banger doctor` warns rather than fails // when it finds a higher version. const KnownTestedVersion = "1.14.1" // versionPattern matches the canonical `Firecracker v1.14.1` line // emitted by `firecracker --version`. The pre-release suffix // (e.g. `-beta`) is captured for fidelity in the reported string but // ignored for ordering. var versionPattern = regexp.MustCompile(`Firecracker v(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?`) // SemVer is a structural representation of a `MAJOR.MINOR.PATCH` // triple plus an optional pre-release label. Comparisons use only // the triple; pre-releases are kept for display. type SemVer struct { Major, Minor, Patch int PreRelease string } // String renders the SemVer back to its canonical form, with a // leading "v" so it matches firecracker's own output. func (s SemVer) String() string { if s.PreRelease == "" { return fmt.Sprintf("v%d.%d.%d", s.Major, s.Minor, s.Patch) } return fmt.Sprintf("v%d.%d.%d-%s", s.Major, s.Minor, s.Patch, s.PreRelease) } // Compare returns -1, 0, or +1 based on the (Major, Minor, Patch) // triple. Pre-release labels are ignored — banger doesn't // distinguish `v1.10.0` from `v1.10.0-rc1` for compatibility purposes. func (s SemVer) Compare(other SemVer) int { if s.Major != other.Major { return cmpInt(s.Major, other.Major) } if s.Minor != other.Minor { return cmpInt(s.Minor, other.Minor) } return cmpInt(s.Patch, other.Patch) } // ParseVersionOutput pulls the SemVer out of `firecracker --version` // stdout. firecracker historically prints the version line followed // by a freeform "exiting successfully" line; we match the first // occurrence of the pattern anywhere in the output to be tolerant of // future cosmetic changes. func ParseVersionOutput(out string) (SemVer, error) { m := versionPattern.FindStringSubmatch(out) if m == nil { return SemVer{}, fmt.Errorf("unrecognised firecracker version output: %q", strings.TrimSpace(out)) } major, _ := strconv.Atoi(m[1]) minor, _ := strconv.Atoi(m[2]) patch, _ := strconv.Atoi(m[3]) return SemVer{Major: major, Minor: minor, Patch: patch, PreRelease: m[4]}, nil } // MustParseSemVer parses a `MAJOR.MINOR.PATCH` (optionally `v`-prefixed) // constant. Panics on malformed input — only used for the package-level // constants above and in tests, so a malformed string is a developer // error rather than a runtime concern. func MustParseSemVer(s string) SemVer { parts := strings.SplitN(strings.TrimPrefix(s, "v"), ".", 3) if len(parts) != 3 { panic("MustParseSemVer: " + s) } major, err := strconv.Atoi(parts[0]) if err != nil { panic("MustParseSemVer: " + s) } minor, err := strconv.Atoi(parts[1]) if err != nil { panic("MustParseSemVer: " + s) } patch, err := strconv.Atoi(parts[2]) if err != nil { panic("MustParseSemVer: " + s) } return SemVer{Major: major, Minor: minor, Patch: patch} } // VersionRunner is the slim contract QueryVersion needs from a // command-runner. system.Runner satisfies it; defining the interface // inline keeps internal/firecracker free of cross-cutting imports. type VersionRunner interface { Run(ctx context.Context, name string, args ...string) ([]byte, error) } // QueryVersion runs ` --version` and parses the result. Returns // only the parsed SemVer — firecracker's stdout includes a trailing // "exiting successfully" log line that we have no use for; callers // render the result via SemVer.String() ("v1.14.1") for display. func QueryVersion(ctx context.Context, runner VersionRunner, bin string) (SemVer, error) { out, err := runner.Run(ctx, bin, "--version") if err != nil { return SemVer{}, err } return ParseVersionOutput(string(out)) } func cmpInt(a, b int) int { switch { case a < b: return -1 case a > b: return 1 default: return 0 } }