Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 84 additions & 4 deletions libs/cln-version-manager/clnvm/cln_version_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
from pathlib import Path
import re
import subprocess
import sys
import tarfile
Expand Down Expand Up @@ -43,6 +44,40 @@ class VersionDescriptor:
logger = logging.getLogger(__name__)


# Matches tags like ``v25.12``, ``v25.12.``, ``v25.12gl1`` or ``v0.11.2gl2``.
# Group 1 is the dotted numeric base, group 2 the optional greenlight (``glN``)
# revision.
_VERSION_RE = re.compile(r"^v?(\d+(?:\.\d+)*)\.?(?:gl(\d+))?$")


def version_sort_key(tag: str) -> Optional[Tuple[Tuple[int, ...], int]]:
"""Return a deterministic sort key for a CLN version tag.

The key is ``(base, glrev)`` where ``base`` is the tuple of numeric
version components and ``glrev`` is the greenlight suffix revision
(``-1`` when there is no ``glN`` suffix, so a plain upstream build sorts
just below its greenlight counterpart). Returns ``None`` for tags that
are not numbered releases, e.g. ``main``, so callers can skip them.
"""
m = _VERSION_RE.match(tag)
if m is None:
return None
base = tuple(int(p) for p in m.group(1).split("."))
glrev = int(m.group(2)) if m.group(2) is not None else -1
return base, glrev


def version_base(tag: str) -> Optional[Tuple[int, ...]]:
"""Return the numeric base version, ignoring any ``glN`` suffix.

Used to compare against the supported-version bounds, which are
expressed without the greenlight suffix (the signer reports e.g.
``v25.12``, which must match both ``v25.12`` and ``v25.12gl1``).
"""
key = version_sort_key(tag)
return key[0] if key is not None else None


def _get_cache_dir() -> Path:
cln_cache_dir = os.environ.get("CLNVM_CACHE_DIR")
if cln_cache_dir is not None:
Expand Down Expand Up @@ -250,11 +285,56 @@ def get_descriptor_from_tag(self, tag: str) -> VersionDescriptor:

return descriptor

def supported_versions(
self, lowest: str, highest: str
) -> List[VersionDescriptor]:
"""Return the supported versions, sorted ascending.

A version is supported when its base version (ignoring the ``glN``
suffix) lies within ``[lowest, highest]`` inclusive. Tags that are
not numbered releases (e.g. ``main``) are dropped.
"""
low = version_base(lowest)
high = version_base(highest)
if low is None or high is None:
raise ValueError(
f"Invalid version bounds: lowest={lowest!r}, highest={highest!r}"
)

selected = []
for d in self.get_versions():
key = version_sort_key(d.tag)
if key is not None and low <= key[0] <= high:
selected.append((key, d))

selected.sort(key=lambda kd: kd[0])
return [d for _, d in selected]

def latest_supported(self, lowest: str, highest: str) -> NodeVersion:
"""Return the newest supported version within ``[lowest, highest]``.

Deterministic: versions are ordered by ``(base, glrev)`` so the
greenlight build wins over the plain upstream build of the same base
version. Use this rather than :meth:`latest` so that newer-but-
unsupported releases present in the manifest are never picked up.
"""
supported = self.supported_versions(lowest, highest)
if not supported:
raise ValueError(
f"No CLN version available in range [{lowest}, {highest}]"
)
return self.get(supported[-1])

def latest(self) -> NodeVersion:
vs = [d.tag for d in self.get_versions()]
latest = max(vs)
descriptor = self.get_descriptor_from_tag(latest)
return self.get(descriptor)
candidates = []
for d in self.get_versions():
key = version_sort_key(d.tag)
if key is not None:
candidates.append((key, d))
if not candidates:
raise ValueError("No numbered CLN version available in the manifest")
_, latest = max(candidates, key=lambda kd: kd[0])
return self.get(latest)

def get(self, cln_version: VersionDescriptor, force: bool = False) -> NodeVersion:
"""
Expand Down
77 changes: 76 additions & 1 deletion libs/cln-version-manager/tests/test_version_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,30 @@

import requests

from clnvm.cln_version_manager import ClnVersionManager, VersionDescriptor
from clnvm.cln_version_manager import (
ClnVersionManager,
VersionDescriptor,
version_base,
version_sort_key,
)


def _descriptor(tag: str) -> VersionDescriptor:
return VersionDescriptor(tag=tag, url="", checksum=tag)


# Mirrors the kind of tags found in the production manifest, deliberately out
# of order.
_MANIFEST_TAGS = [
"main",
"v0.11.2gl2",
"v23.08.",
"v23.08gl1",
"v24.11gl1",
"v25.12.",
"v25.12gl1",
"v26.06gl1",
]


def get_tmp_dir(name: str) -> str:
Expand All @@ -32,3 +55,55 @@ def test_download_cln_version() -> None:
with mock.patch("requests.get") as request_mock:
vm_test.get_all()
assert not request_mock.get.called


def test_version_sort_key() -> None:
# ``main`` and other non-numbered tags are not orderable.
assert version_sort_key("main") is None

# The glN suffix is captured separately, and absent suffixes sort below
# their greenlight counterpart of the same base.
assert version_sort_key("v25.12.") == ((25, 12), -1)
assert version_sort_key("v25.12gl1") == ((25, 12), 1)
assert version_sort_key("v0.11.2gl2") == ((0, 11, 2), 2)
assert version_sort_key("v25.12.") < version_sort_key("v25.12gl1")
assert version_sort_key("v25.12gl1") < version_sort_key("v26.06gl1")


def test_version_base_ignores_suffix() -> None:
# Base comparison ignores the glN suffix, so the signer reporting
# ``v25.12`` matches both the plain and greenlight builds.
assert version_base("v25.12") == version_base("v25.12gl1") == (25, 12)
assert version_base("v25.12.") == (25, 12)
assert version_base("main") is None


def test_supported_versions_filters_and_sorts() -> None:
vm = ClnVersionManager(
cln_versions=[_descriptor(t) for t in _MANIFEST_TAGS]
)

supported = vm.supported_versions("v23.08", "v25.12")
tags = [d.tag for d in supported]

# ``main`` dropped (not numbered), ``v0.11.2gl2`` below the lower bound,
# ``v26.06gl1`` above the signer-supported upper bound. Result is sorted
# ascending with the greenlight build after the plain build of the same
# base.
assert tags == [
"v23.08.",
"v23.08gl1",
"v24.11gl1",
"v25.12.",
"v25.12gl1",
]


def test_supported_versions_latest_is_greenlight_build() -> None:
vm = ClnVersionManager(
cln_versions=[_descriptor(t) for t in _MANIFEST_TAGS]
)

# The newest supported version is the greenlight build at the upper
# bound, never the newer-but-unsupported v26.06gl1.
assert vm.supported_versions("v23.08", "v25.12")[-1].tag == "v25.12gl1"
2 changes: 1 addition & 1 deletion libs/gl-plugin/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
//! sign off actually match the authentic commands by a valid
//! caller.

use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Request {
Expand Down
10 changes: 6 additions & 4 deletions libs/gl-plugin/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ where
/// `peer_connected` hook.
#[derive(Serialize, Deserialize, Debug)]
pub struct PeerConnectedCall {
pub peer: Peer
pub peer: Peer,
}

#[derive(Serialize, Deserialize, Debug)]
Expand All @@ -295,10 +295,9 @@ pub struct Peer {
#[serde(rename_all = "snake_case")]
pub enum Direction {
In,
Out
Out,
}


#[cfg(test)]
mod test {
use super::*;
Expand All @@ -315,7 +314,10 @@ mod test {
});

let call = serde_json::from_str::<PeerConnectedCall>(&msg.to_string()).unwrap();
assert_eq!(call.peer.id, "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f");
assert_eq!(
call.peer.id,
"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"
);
assert_eq!(call.peer.direction, Direction::In);
assert_eq!(call.peer.addr, "34.239.230.56:9735");
assert_eq!(call.peer.features, "");
Expand Down
Loading
Loading