Skip to content
Merged
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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions internal/flaps/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading