diff --git a/CHANGELOG.md b/CHANGELOG.md index 690c052..a2c87b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.4.1] - 2026-07-14 + +### Fixed + +- Machine `config.mounts` is now persisted and echoed back. Previously + `MachineConfig` had no `mounts` field, so a create body's mounts were dropped + on unmarshal and `GET .../machines` returned `config.mounts: null` — making a + mounting machine look drifted on every reconcile (it never no-op'd). The field + now mirrors fly-go's `MachineMount` and round-trips through create → GET + (INTENTIUS/chant#868, #59). + ## [0.4.0] - 2026-07-12 ### Added @@ -111,7 +122,9 @@ Fidelity and correctness pass from an adversarial audit against `superfly/fly-go - Distroless container image, GoReleaser configuration, mkdocs-material doc site, and CI. -[Unreleased]: https://github.com/intentius/mudflaps/compare/v0.3.1...HEAD +[Unreleased]: https://github.com/intentius/mudflaps/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/intentius/mudflaps/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/intentius/mudflaps/compare/v0.3.1...v0.4.0 [0.3.1]: https://github.com/intentius/mudflaps/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/intentius/mudflaps/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/intentius/mudflaps/compare/v0.1.0...v0.2.0 diff --git a/internal/flaps/types.go b/internal/flaps/types.go index e0736c0..9adf377 100644 --- a/internal/flaps/types.go +++ b/internal/flaps/types.go @@ -85,6 +85,22 @@ type MachineConfig struct { Guest *MachineGuest `json:"guest,omitempty"` Services []Service `json:"services,omitempty"` Restart *Restart `json:"restart,omitempty"` + Mounts []MachineMount `json:"mounts,omitempty"` +} + +// MachineMount attaches a volume to a machine at a path. Field names mirror +// fly-go's MachineMount so a create body round-trips through GET unchanged — +// without this, mounts would be dropped on unmarshal and a mounting machine +// would look drifted on every reconcile. +type MachineMount struct { + Volume string `json:"volume,omitempty"` + Path string `json:"path,omitempty"` + Name string `json:"name,omitempty"` + Encrypted bool `json:"encrypted,omitempty"` + SizeGb int `json:"size_gb,omitempty"` + SizeGbLimit int `json:"size_gb_limit,omitempty"` + AddSizeGb int `json:"add_size_gb,omitempty"` + ExtendThresholdPercent int `json:"extend_threshold_percent,omitempty"` } // MachineGuest is the resource allocation for a machine. diff --git a/internal/store/store.go b/internal/store/store.go index e0ea795..36f7f55 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -205,6 +205,11 @@ func cloneMachine(m *flaps.Machine) *flaps.Machine { } cfg.Services = svcs } + if m.Config.Mounts != nil { + mounts := make([]flaps.MachineMount, len(m.Config.Mounts)) + copy(mounts, m.Config.Mounts) + cfg.Mounts = mounts + } c.Config = &cfg } if m.ImageRef != nil { diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 5313e18..1785729 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -169,3 +169,35 @@ func TestClonedMachinePortsAreIsolated(t *testing.T) { t.Fatalf("clone aliased stored handlers: got %q, want tls", got) } } + +func TestMachineMountsRoundTrip(t *testing.T) { + s := New() + if _, err := s.CreateApp(flaps.App{Name: "demo"}); err != nil { + t.Fatalf("CreateApp: %v", err) + } + m := flaps.Machine{ + ID: "m1", + State: flaps.StateCreating, + Config: &flaps.MachineConfig{Image: "nginx", Mounts: []flaps.MachineMount{{Volume: "data", Path: "/data"}}}, + } + if _, err := s.CreateMachine("demo", m); err != nil { + t.Fatalf("CreateMachine: %v", err) + } + + // The mount survives create → GET, so a mounting machine reconciles to a + // no-op instead of looking drifted every apply (the bug this fixes). + got, err := s.GetMachine("demo", "m1") + if err != nil { + t.Fatalf("GetMachine: %v", err) + } + if len(got.Config.Mounts) != 1 || got.Config.Mounts[0].Volume != "data" || got.Config.Mounts[0].Path != "/data" { + t.Fatalf("mounts not persisted: %+v", got.Config.Mounts) + } + + // Clone isolation: mutating a returned clone must not affect stored state. + got.Config.Mounts[0].Path = "MUTATED" + again, _ := s.GetMachine("demo", "m1") + if again.Config.Mounts[0].Path != "/data" { + t.Fatalf("clone aliased stored mounts: got %q, want /data", again.Config.Mounts[0].Path) + } +}