Skip to content

Commit 154fb98

Browse files
Configure scheduled_snapshots via fly.toml and flyctl launch (#4586)
* Configure scheduled_snapshots via fly.toml * Configure scheduled_snapshots via flyctl launch
1 parent cd0314d commit 154fb98

File tree

7 files changed

+149
-16
lines changed

7 files changed

+149
-16
lines changed

internal/appconfig/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ type Mount struct {
158158
Destination string `toml:"destination,omitempty" json:"destination,omitempty"`
159159
InitialSize string `toml:"initial_size,omitempty" json:"initial_size,omitempty"`
160160
SnapshotRetention *int `toml:"snapshot_retention,omitempty" json:"snapshot_retention,omitempty"`
161+
ScheduledSnapshots *bool `toml:"scheduled_snapshots,omitempty" json:"scheduled_snapshots,omitempty"`
161162
AutoExtendSizeThreshold int `toml:"auto_extend_size_threshold,omitempty" json:"auto_extend_size_threshold,omitempty"`
162163
AutoExtendSizeIncrement string `toml:"auto_extend_size_increment,omitempty" json:"auto_extend_size_increment,omitempty"`
163164
AutoExtendSizeLimit string `toml:"auto_extend_size_limit,omitempty" json:"auto_extend_size_limit,omitempty"`

internal/appconfig/definition_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,11 @@ func TestToDefinition(t *testing.T) {
304304
},
305305
},
306306
"mounts": []any{map[string]any{
307-
"source": "data",
308-
"destination": "/data",
309-
"initial_size": "30gb",
310-
"snapshot_retention": int64(17),
307+
"source": "data",
308+
"destination": "/data",
309+
"initial_size": "30gb",
310+
"snapshot_retention": int64(17),
311+
"scheduled_snapshots": true,
311312
}},
312313
"processes": map[string]any{
313314
"web": "run web",

internal/appconfig/serde_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,11 @@ func TestLoadTOMLAppConfigReferenceFormat(t *testing.T) {
592592
},
593593

594594
Mounts: []Mount{{
595-
Source: "data",
596-
Destination: "/data",
597-
InitialSize: "30gb",
598-
SnapshotRetention: fly.Pointer(17),
595+
Source: "data",
596+
Destination: "/data",
597+
InitialSize: "30gb",
598+
SnapshotRetention: fly.Pointer(17),
599+
ScheduledSnapshots: fly.BoolPointer(true),
599600
}},
600601

601602
Processes: map[string]string{

internal/appconfig/testdata/full-reference.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ host_dedication_id = "06031957"
128128
initial_size = "30gb"
129129
destination = "/data"
130130
snapshot_retention = 17
131+
scheduled_snapshots = true
131132

132133
[[vm]]
133134
size = "shared-cpu-1x"

internal/command/deploy/deploy_first.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func (md *machineDeployment) provisionVolumesOnFirstDeploy(ctx context.Context)
154154
ComputeRequirements: guest,
155155
ComputeImage: md.img,
156156
SnapshotRetention: m.SnapshotRetention,
157+
AutoBackupEnabled: m.ScheduledSnapshots,
157158
}
158159

159160
vol, err := md.flapsClient.CreateVolume(ctx, input)

internal/command/launch/cmd_test.go

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package launch
22

33
import (
44
"context"
5+
"strings"
56
"testing"
67

78
"github.com/spf13/pflag"
9+
"github.com/stretchr/testify/assert"
10+
fly "github.com/superfly/fly-go"
11+
"github.com/superfly/flyctl/internal/appconfig"
812
"github.com/superfly/flyctl/internal/flag/flagctx"
913
"github.com/superfly/flyctl/iostreams"
1014
)
@@ -89,7 +93,7 @@ func TestValidatePostgresFlags(t *testing.T) {
8993
t.Errorf("expected error but got none")
9094
return
9195
}
92-
if tt.errorMsg != "" && !containsString(err.Error(), tt.errorMsg) {
96+
if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
9397
t.Errorf("expected error message to contain '%s', got '%s'", tt.errorMsg, err.Error())
9498
}
9599
} else {
@@ -101,12 +105,130 @@ func TestValidatePostgresFlags(t *testing.T) {
101105
}
102106
}
103107

104-
// Helper function to check if a string contains a substring
105-
func containsString(s, substr string) bool {
106-
for i := 0; i <= len(s)-len(substr); i++ {
107-
if s[i:i+len(substr)] == substr {
108-
return true
109-
}
108+
func TestParseMountOptions(t *testing.T) {
109+
tests := []struct {
110+
name string
111+
options string
112+
expectedMount appconfig.Mount
113+
expectError bool
114+
errMsg string
115+
}{
116+
{
117+
name: "empty options",
118+
options: "",
119+
expectedMount: appconfig.Mount{},
120+
},
121+
{
122+
name: "scheduled_snapshots true",
123+
options: "scheduled_snapshots=true",
124+
expectedMount: appconfig.Mount{
125+
ScheduledSnapshots: fly.Pointer(true),
126+
},
127+
},
128+
{
129+
name: "scheduled_snapshots false",
130+
options: "scheduled_snapshots=false",
131+
expectedMount: appconfig.Mount{
132+
ScheduledSnapshots: fly.Pointer(false),
133+
},
134+
},
135+
{
136+
name: "scheduled_snapshots invalid value",
137+
options: "scheduled_snapshots=invalid",
138+
expectError: true,
139+
errMsg: "invalid value for scheduled_snapshots",
140+
},
141+
{
142+
name: "snapshot_retention",
143+
options: "snapshot_retention=7",
144+
expectedMount: appconfig.Mount{
145+
SnapshotRetention: fly.Pointer(7),
146+
},
147+
},
148+
{
149+
name: "snapshot_retention invalid",
150+
options: "snapshot_retention=invalid",
151+
expectError: true,
152+
errMsg: "invalid value for snapshot_retention",
153+
},
154+
{
155+
name: "initial_size",
156+
options: "initial_size=10GB",
157+
expectedMount: appconfig.Mount{
158+
InitialSize: "10GB",
159+
},
160+
},
161+
{
162+
name: "auto_extend_size_threshold",
163+
options: "auto_extend_size_threshold=80",
164+
expectedMount: appconfig.Mount{
165+
AutoExtendSizeThreshold: 80,
166+
},
167+
},
168+
{
169+
name: "auto_extend_size_threshold invalid",
170+
options: "auto_extend_size_threshold=invalid",
171+
expectError: true,
172+
errMsg: "invalid value for auto_extend_size_threshold",
173+
},
174+
{
175+
name: "auto_extend_size_increment",
176+
options: "auto_extend_size_increment=1GB",
177+
expectedMount: appconfig.Mount{
178+
AutoExtendSizeIncrement: "1GB",
179+
},
180+
},
181+
{
182+
name: "auto_extend_size_limit",
183+
options: "auto_extend_size_limit=100GB",
184+
expectedMount: appconfig.Mount{
185+
AutoExtendSizeLimit: "100GB",
186+
},
187+
},
188+
{
189+
name: "multiple options",
190+
options: "initial_size=10GB,scheduled_snapshots=true,snapshot_retention=14",
191+
expectedMount: appconfig.Mount{
192+
InitialSize: "10GB",
193+
ScheduledSnapshots: fly.Pointer(true),
194+
SnapshotRetention: fly.Pointer(14),
195+
},
196+
},
197+
{
198+
name: "unknown option",
199+
options: "unknown_option=value",
200+
expectError: true,
201+
errMsg: "unknown mount option",
202+
},
203+
{
204+
name: "invalid format",
205+
options: "invalid_format",
206+
expectError: true,
207+
errMsg: "invalid mount option",
208+
},
209+
}
210+
211+
for _, tt := range tests {
212+
t.Run(tt.name, func(t *testing.T) {
213+
mount := &appconfig.Mount{}
214+
err := ParseMountOptions(mount, tt.options)
215+
216+
if tt.expectError {
217+
if err == nil {
218+
t.Errorf("expected error but got none")
219+
return
220+
}
221+
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
222+
t.Errorf("expected error message to contain '%s', got '%s'", tt.errMsg, err.Error())
223+
}
224+
} else {
225+
if err != nil {
226+
t.Errorf("expected no error but got: %v", err)
227+
return
228+
}
229+
230+
assert.Equal(t, tt.expectedMount, *mount)
231+
}
232+
})
110233
}
111-
return false
112234
}

internal/command/launch/launch.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ func ParseMountOptions(mount *appconfig.Mount, options string) error {
235235
return fmt.Errorf("invalid value for snapshot_retention: %s", value)
236236
}
237237
mount.SnapshotRetention = &ret
238+
case "scheduled_snapshots":
239+
ret, err := strconv.ParseBool(value)
240+
if err != nil {
241+
return fmt.Errorf("invalid value for scheduled_snapshots: %s", value)
242+
}
243+
mount.ScheduledSnapshots = &ret
238244
case "auto_extend_size_threshold":
239245
threshold, err := strconv.Atoi(value)
240246
if err != nil {

0 commit comments

Comments
 (0)