Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2a8570c
Start shared Machine installer and upgrade implementation (#822)
Sep 8, 2026
c13bcd8
feat(ployzd): install Machines through daemon
Sep 9, 2026
d18aed4
fix(ployzd): harden Machine installer
Sep 9, 2026
91b11a8
test(ployzd): execute installer contracts
Sep 9, 2026
a71057e
Merge feat/installer-823: execute installer contracts (#823)
Sep 9, 2026
aea92ce
feat(setup): use shared Machine installer
Sep 9, 2026
0da9905
fix(ployzd): tighten installer host boundaries
Sep 9, 2026
60ab5da
Merge fix/installer-review-822: tighten installer boundaries (#822)
Sep 9, 2026
5f672d3
feat: add bounded machine upgrades
Sep 9, 2026
24889e5
Merge commit '60ab5da2' into feat/setup-824
Sep 9, 2026
ca67e46
Merge commit '0da99056c1d5e9bf8a68c0ced8ff7d6c993592eb' into feat/upg…
Sep 9, 2026
bb4d30e
fix(setup): address adapter review findings
Sep 9, 2026
6c8e104
fix: preserve upgrade attempt evidence
Sep 9, 2026
4aa714a
Merge feat/setup-824 reviewed fixes into feat/upgrades-825
Sep 9, 2026
873c4fa
fix(setup): verify downloads before staging
Sep 9, 2026
21ce3b5
fix(setup): verify downloads before staging
Sep 9, 2026
7583526
test(compose): create ignored FIFOs portably
Sep 9, 2026
dacaf26
test(compose): create ignored FIFOs portably
Sep 9, 2026
54ffb63
test(compose): use POSIX mkfifo fixture
Sep 9, 2026
fd7eb84
test: qualify bounded machine upgrades
Sep 9, 2026
1381247
test(compose): use POSIX mkfifo fixture
Sep 9, 2026
22c3b57
fix(setup): refresh SSH groups after install
Sep 9, 2026
bae6331
fix(setup): refresh SSH groups after install
Sep 9, 2026
500deb0
Merge feat/setup-824: install setup workflow (#824)
Sep 9, 2026
840728a
Fix provisioned volume metadata registration
Sep 9, 2026
05b6222
Merge fix/volume-prepare-822: validate ZFS metadata prerequisite (#822)
Sep 9, 2026
7f7f9f9
fix(upgrade): own mutation admission and durable outcomes
Sep 9, 2026
98f7210
Fix provisioned volume metadata registration
Sep 9, 2026
0c0f02c
test(machine): await guarded local mutations
Sep 9, 2026
4d578c9
fix(installer): reject nonstandard system paths
Sep 9, 2026
de09dea
fix(upgrade): reject nonstandard activation paths
Sep 9, 2026
54e7d41
test(upgrade): isolate qualification from public DNS
Sep 9, 2026
16fd272
Let Docker own provisioned volume creation
Sep 9, 2026
8df1743
Let Docker own provisioned volume creation
Sep 9, 2026
dc88b31
Merge fix/volume-prepare-822: verify provisioned volume reuse (#822)
Sep 9, 2026
a5351d3
Merge feat/upgrades-825: upgrade workflow (#825)
Sep 9, 2026
f18f5f4
Merge origin/main into installer upgrades (#822)
Sep 9, 2026
d692ea3
fix(upgrade): close implementation review findings
Sep 9, 2026
70b9fa1
refactor(build): group execution admission
Sep 9, 2026
ad51005
fix(installer): own launches and retain live smoke backing
Sep 9, 2026
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
1 change: 0 additions & 1 deletion .github/workflows/release-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ on:
- .github/workflows/release-contracts.yml
- .goreleaser.yaml
- install.sh
- scripts/install.sh
- scripts/uninstall.sh
- scripts/verify-release.sh
- scripts/pack-release.sh
Expand Down
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ archives:

checksum:
name_template: checksums.txt
ids: [ployz]
ids: [ployz, ployzd]

changelog:
disable: true
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ployz-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ipnet = { version = "2.12.1", features = ["serde"] }
prost = "0.13.5"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
semver = "1.0.28"
thiserror = "2.0.20"
ts-rs.workspace = true
url = { version = "2.5.8", features = ["serde"] }
Expand Down
1 change: 1 addition & 0 deletions crates/ployz-core/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include!("src/rpc_catalog.rs");

fn main() {
println!("cargo:rerun-if-changed=src/rpc_catalog.rs");
macro_rules! build_service {
(
package $package:literal
Expand Down
4 changes: 4 additions & 0 deletions crates/ployz-core/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use std::{
};
use ts_rs::TS;

mod upgrade;
pub use upgrade::*;

use ipnet::Ipv4Net;
use prost::Message;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
Expand Down Expand Up @@ -839,6 +842,7 @@ define_responses! {
Domain(Domain) => "domain";
DomainRecords(DomainRecords) => "domain_records";
MachineUpdated(MachineUpdated) => "machine_updated";
MachineUpgradeAttempt(MachineUpgradeAttempt) => "machine_upgrade_attempt";
LocalMachineRemoved(LocalMachineRemoved) => "local_machine_removed";
MachineRemoved(MachineRemoved) => "machine_removed";
WireGuardInspected(WireGuardInspected) => "wireguard_inspected";
Expand Down
87 changes: 87 additions & 0 deletions crates/ployz-core/src/rpc/upgrade.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Typed requests and durable outcomes for one Machine upgrade.

use serde::{Deserialize, Serialize};
use ts_rs::TS;

/// Request one exact or channel-selected Machine release under a stable retry identity.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RequestMachineUpgradeRequest {
/// Identity reused by every retry of the same request.
pub attempt_id: crate::MachineUpgradeAttemptId,
/// Exact version or supported release channel to resolve once.
pub release: crate::MachineRelease,
}

/// Read the latest attempt, optionally requiring one exact retry identity.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct InspectMachineUpgradeRequest {
/// Required attempt identity, or the latest local attempt when absent.
#[serde(default)]
pub attempt_id: Option<crate::MachineUpgradeAttemptId>,
}

crate::value::open_string_enum!(MachineUpgradeStage, Unknown {
Launching => "launching",
Preparing => "preparing",
Acquiring => "acquiring",
Verifying => "verifying",
Activating => "activating",
Restarting => "restarting",
Readiness => "readiness",
});

/// Durable local evidence for one bounded Machine upgrade attempt.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
pub struct MachineUpgradeAttempt {
/// Stable identity assigned before the request is dispatched.
pub attempt_id: crate::MachineUpgradeAttemptId,
/// Exact version resolved before any host mutation.
pub target: crate::MachineVersion,
/// State-specific evidence for the attempt.
#[serde(flatten)]
#[ts(flatten)]
pub outcome: MachineUpgradeOutcome,
}

/// State-specific evidence for a Machine upgrade attempt.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum MachineUpgradeOutcome {
/// The durable receipt and active marker exist and worker launch was accepted.
Accepted,
/// The independent worker reached this installation stage.
Running {
/// Latest stage durably recorded by the worker.
stage: MachineUpgradeStage,
},
/// The target daemon was observed running and ready through its local API.
Succeeded {
/// Exact version reported by the ready, activated daemon.
version: crate::MachineVersion,
},
/// The worker recorded a terminal installation failure.
Failed {
/// First installation stage that did not complete.
stage: MachineUpgradeStage,
/// Operator-facing failure evidence.
error: String,
},
/// A positively stopped worker left no terminal result.
Interrupted {
/// Last durable stage before the worker stopped.
stage: MachineUpgradeStage,
},
}

impl MachineUpgradeAttempt {
/// Whether the attempt has durable terminal evidence.
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(
self.outcome,
MachineUpgradeOutcome::Succeeded { .. }
| MachineUpgradeOutcome::Failed { .. }
| MachineUpgradeOutcome::Interrupted { .. }
)
}
}
2 changes: 2 additions & 0 deletions crates/ployz-core/src/rpc_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ macro_rules! rpc_catalog {
SetCloudPairing: (set_cloud_pairing, "SetCloudPairing", SetCloudPairingRequest, "set_cloud_pairing", CloudPairingSet, SET_CLOUD_PAIRING_CAPABILITY, "ployz.machine.set-cloud-pairing.v1", Always),
ListMachines: (list_machines, "ListMachines", ListMachinesRequest, "list_machines", MachineList, LIST_MACHINES_CAPABILITY, "ployz.machine.list.v1", Always),
UpdateMachine: (update_machine, "UpdateMachine", UpdateMachineRequest, "update_machine", MachineUpdated, UPDATE_MACHINE_CAPABILITY, "ployz.machine.update.v1", Always),
RequestMachineUpgrade: (request_machine_upgrade, "RequestMachineUpgrade", RequestMachineUpgradeRequest, "request_machine_upgrade", MachineUpgradeAttempt, REQUEST_MACHINE_UPGRADE_CAPABILITY, "ployz.machine.upgrade.request.v1", Always),
InspectMachineUpgrade: (inspect_machine_upgrade, "InspectMachineUpgrade", InspectMachineUpgradeRequest, "inspect_machine_upgrade", MachineUpgradeAttempt, INSPECT_MACHINE_UPGRADE_CAPABILITY, "ployz.machine.upgrade.inspect.v1", Always),
RemoveLocalMachine: (remove_local_machine, "RemoveLocalMachine", RemoveLocalMachineRequest, "remove_local_machine", LocalMachineRemoved, REMOVE_LOCAL_MACHINE_CAPABILITY, "ployz.machine.remove-local.v1", Always),
RemoveMachine: (remove_machine, "RemoveMachine", RemoveMachineRequest, "remove_machine", MachineRemoved, REMOVE_MACHINE_CAPABILITY, "ployz.machine.remove.v1", Always),
InspectWireguard: (inspect_wireguard, "InspectWireguard", InspectWireGuardRequest, "inspect_wireguard", WireGuardInspected, INSPECT_WIREGUARD_CAPABILITY, "ployz.wireguard.inspect.v1", Always),
Expand Down
63 changes: 63 additions & 0 deletions crates/ployz-core/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ fn is_hostname(value: &str) -> bool {
(1..=253).contains(&value.len()) && value.split('.').all(is_dns_label)
}

fn is_machine_version(value: &str) -> bool {
let Ok(version) = semver::Version::parse(value) else {
return false;
};
let pre = version.pre.as_str();
version.build.is_empty()
&& (pre.is_empty()
|| pre.strip_prefix("beta.").is_some_and(|number| {
!number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit())
}))
}

macro_rules! hex_id_newtype {
($(#[$attribute:meta])* $name:ident, $label:literal, $len:expr, $expected:literal) => {
$(#[$attribute])*
Expand Down Expand Up @@ -208,6 +220,13 @@ hex_id_newtype!(
32,
"32 lowercase hexadecimal characters"
);
hex_id_newtype!(
/// Identity of one bounded Machine upgrade attempt.
MachineUpgradeAttemptId,
"Machine upgrade attempt ID",
32,
"32 lowercase hexadecimal characters"
);
hex_id_newtype!(
ServiceId,
"Service ID",
Expand Down Expand Up @@ -237,6 +256,16 @@ impl MachineId {
}
}

impl MachineUpgradeAttemptId {
/// Random 32-character lowercase hexadecimal attempt ID.
#[must_use]
pub fn random() -> Self {
let mut hex = [0_u8; 32];
uuid::Uuid::new_v4().simple().encode_lower(&mut hex);
Self(hex)
}
}

impl ServiceId {
#[must_use]
pub fn random() -> Self {
Expand Down Expand Up @@ -287,6 +316,20 @@ validated_string_newtype!(
"a 1-63 character lowercase DNS label",
|value| is_dns_label(value)
);
validated_string_newtype!(
/// A trusted release selector: stable, beta, or an exact supported version.
MachineRelease,
"Machine release",
"stable, beta, X.Y.Z, or X.Y.Z-beta.N",
|value| matches!(value, "stable" | "beta") || is_machine_version(value)
);
validated_string_newtype!(
/// One exact supported Machine release version.
MachineVersion,
"Machine version",
"X.Y.Z or X.Y.Z-beta.N",
|value| is_machine_version(value)
);
validated_string_newtype!(
DockerVolumeName,
"Docker Volume name",
Expand Down Expand Up @@ -878,4 +921,24 @@ mod tests {
assert_eq!(format!("{token:?}"), "CloudEnrollToken(..)");
assert!(!format!("{token:?}").contains("pmet_test"));
}

#[test]
fn machine_release_accepts_only_channels_and_supported_exact_versions() {
for release in ["stable", "beta", "1.2.3", "1.2.3-beta.4"] {
assert!(MachineRelease::parse(release).is_ok(), "{release}");
}
for release in [
"nightly",
"v1.2.3",
"1.2",
"1.2.3-alpha.1",
"1.2.3+build",
"1.2.3-beta.01",
"https://example.test/release",
] {
assert!(MachineRelease::parse(release).is_err(), "{release}");
assert!(MachineVersion::parse(release).is_err(), "{release}");
}
assert!(MachineVersion::parse("stable").is_err());
}
}
38 changes: 28 additions & 10 deletions crates/ployz-core/tests/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ use ployz_core::{
CapabilityName, DESCRIBE_CONTRACT_CAPABILITY, ENSURE_GLOBAL_SLOT_CAPABILITY,
ENSURE_IMAGE_INGEST_CAPABILITY, EXEC_CONTAINER_CAPABILITY, GET_DOMAIN_CAPABILITY,
GET_INGRESS_PROXY_CONFIG_CAPABILITY, INITIALIZE_MACHINE_CAPABILITY,
INSPECT_CONTAINER_CAPABILITY, INSPECT_MACHINE_CAPABILITY, INSPECT_STORAGE_CAPABILITY,
INSPECT_VOLUME_CAPABILITY, INSPECT_WIREGUARD_CAPABILITY, JOIN_MACHINE_CAPABILITY,
LIST_CONTAINERS_CAPABILITY, LIST_IMAGES_CAPABILITY, LIST_MACHINES_CAPABILITY,
LIST_VOLUMES_CAPABILITY, MACHINE_LOGS_CAPABILITY, MACHINE_STORAGE_OBSERVATION_CAPABILITY,
MACHINE_TOKEN_CAPABILITY, PREPARE_VOLUMES_CAPABILITY, PULL_IMAGE_FROM_MACHINE_CAPABILITY,
REGISTER_MACHINE_CAPABILITY, RELEASE_DOMAIN_CAPABILITY, REMOVE_CONTAINER_CAPABILITY,
REMOVE_LOCAL_MACHINE_CAPABILITY, REMOVE_MACHINE_CAPABILITY, REMOVE_VOLUME_CAPABILITY,
RESERVE_DOMAIN_CAPABILITY, RESET_MACHINE_CAPABILITY, RUNTIME_WATCH_CAPABILITY, Rpc,
SET_CLOUD_PAIRING_CAPABILITY, START_CONTAINER_CAPABILITY, STOP_CONTAINER_CAPABILITY,
UPDATE_MACHINE_CAPABILITY, op,
INSPECT_CONTAINER_CAPABILITY, INSPECT_MACHINE_CAPABILITY, INSPECT_MACHINE_UPGRADE_CAPABILITY,
INSPECT_STORAGE_CAPABILITY, INSPECT_VOLUME_CAPABILITY, INSPECT_WIREGUARD_CAPABILITY,
JOIN_MACHINE_CAPABILITY, LIST_CONTAINERS_CAPABILITY, LIST_IMAGES_CAPABILITY,
LIST_MACHINES_CAPABILITY, LIST_VOLUMES_CAPABILITY, MACHINE_LOGS_CAPABILITY,
MACHINE_STORAGE_OBSERVATION_CAPABILITY, MACHINE_TOKEN_CAPABILITY, PREPARE_VOLUMES_CAPABILITY,
PULL_IMAGE_FROM_MACHINE_CAPABILITY, REGISTER_MACHINE_CAPABILITY, RELEASE_DOMAIN_CAPABILITY,
REMOVE_CONTAINER_CAPABILITY, REMOVE_LOCAL_MACHINE_CAPABILITY, REMOVE_MACHINE_CAPABILITY,
REMOVE_VOLUME_CAPABILITY, REQUEST_MACHINE_UPGRADE_CAPABILITY, RESERVE_DOMAIN_CAPABILITY,
RESET_MACHINE_CAPABILITY, RUNTIME_WATCH_CAPABILITY, Rpc, SET_CLOUD_PAIRING_CAPABILITY,
START_CONTAINER_CAPABILITY, STOP_CONTAINER_CAPABILITY, UPDATE_MACHINE_CAPABILITY, op,
};

/// Capability constants are generated from the catalog, so a typo would stay
Expand All @@ -35,6 +35,14 @@ fn catalogued_capabilities_keep_stable_spellings() {
"ployz.machine.set-cloud-pairing.v1",
),
(LIST_MACHINES_CAPABILITY, "ployz.machine.list.v1"),
(
REQUEST_MACHINE_UPGRADE_CAPABILITY,
"ployz.machine.upgrade.request.v1",
),
(
INSPECT_MACHINE_UPGRADE_CAPABILITY,
"ployz.machine.upgrade.inspect.v1",
),
(
MACHINE_STORAGE_OBSERVATION_CAPABILITY,
"ployz.machine.storage-observation.v1",
Expand Down Expand Up @@ -112,6 +120,8 @@ fn advertised_capability_groups_match_the_frozen_catalog() {
"ployz.machine.set-cloud-pairing.v1",
"ployz.machine.list.v1",
"ployz.machine.update.v1",
"ployz.machine.upgrade.request.v1",
"ployz.machine.upgrade.inspect.v1",
"ployz.machine.remove-local.v1",
"ployz.machine.remove.v1",
"ployz.wireguard.inspect.v1",
Expand Down Expand Up @@ -200,4 +210,12 @@ fn unary_grpc_paths_stay_on_the_machine_rpc_service() {
op::EnsureGlobalSlot::PATH,
"/ployz.rpc.v1.MachineRpc/EnsureGlobalSlot"
);
assert_eq!(
op::RequestMachineUpgrade::PATH,
"/ployz.rpc.v1.MachineRpc/RequestMachineUpgrade"
);
assert_eq!(
op::InspectMachineUpgrade::PATH,
"/ployz.rpc.v1.MachineRpc/InspectMachineUpgrade"
);
}
Loading
Loading