Repository: superfly/flyctl
Pull request #3720 (2024-07-09 15:33:03)

Allow machine checks without a service
### Change Summary

What and Why:
Some customers want to be able to use machine checks for apps that only use private communications, which is a very fair use case.

How:
 In order to allow for this, we just allow putting machine_checks at the top level of a fly.toml

Related to:
https://github.com/superfly/customer-help/issues/55

---

### Documentation

- [x] Fresh Produce
- [ ] In superfly/docs, or asked for help from docs team
- [ ] n/a



Patch:
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index ad5af59db..89cb4ed98 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -58,6 +58,8 @@ type Config struct {
 	Files            []File                    `toml:"files,omitempty" json:"files,omitempty"`
 	HostDedicationID string                    `toml:"host_dedication_id,omitempty" json:"host_dedication_id,omitempty"`
 
+	MachineChecks []*ServiceMachineCheck `toml:"machine_checks,omitempty" json:"machine_checks,omitempty"`
+
 	Restart []Restart `toml:"restart,omitempty" json:"restart,omitempty"`
 
 	Compute []*Compute `toml:"vm,omitempty" json:"vm,omitempty"`


Test patch:
diff --git a/internal/appconfig/definition_test.go b/internal/appconfig/definition_test.go
index cb447704a..e075092d3 100644
--- a/internal/appconfig/definition_test.go
+++ b/internal/appconfig/definition_test.go
@@ -237,6 +237,15 @@ func TestToDefinition(t *testing.T) {
 				},
 			},
 		},
+		"machine_checks": []any{
+			map[string]any{
+				"command":      []any{"curl", "https://fly.io"},
+				"image":        "curlimages/curl",
+				"entrypoint":   []any{"/bin/sh"},
+				"kill_signal":  "SIGKILL",
+				"kill_timeout": "5s",
+			},
+		},
 		"experimental": map[string]any{
 			"cmd":           []any{"cmd"},
 			"entrypoint":    []any{"entrypoint"},
diff --git a/internal/appconfig/serde_test.go b/internal/appconfig/serde_test.go
index 18bfea1fa..d36eaea35 100644
--- a/internal/appconfig/serde_test.go
+++ b/internal/appconfig/serde_test.go
@@ -464,6 +464,16 @@ func TestLoadTOMLAppConfigReferenceFormat(t *testing.T) {
 			},
 		},
 
+		MachineChecks: []*ServiceMachineCheck{
+			{
+				Command:     []string{"curl", "https://fly.io"},
+				Entrypoint:  []string{"/bin/sh"},
+				Image:       "curlimages/curl",
+				KillSignal:  fly.StringPointer("SIGKILL"),
+				KillTimeout: fly.MustParseDuration("5s"),
+			},
+		},
+
 		Statics: []Static{
 			{
 				GuestPath:     "/path/to/statics",
diff --git a/internal/appconfig/testdata/full-reference.toml b/internal/appconfig/testdata/full-reference.toml
index 67be359a7..7e9aa9730 100644
--- a/internal/appconfig/testdata/full-reference.toml
+++ b/internal/appconfig/testdata/full-reference.toml
@@ -162,6 +162,13 @@ host_dedication_id = "06031957"
     Content-Type = "application/json"
     Authorization = "super-duper-secret"
 
+[[machine_checks]]
+   command = ["curl", "https://fly.io"]
+   image = "curlimages/curl"
+   entrypoint = ["/bin/sh"]
+   kill_timeout = "5s"
+   kill_signal = "SIGKILL"
+
 [[services]]
   internal_port = 8081
   protocol = "tcp"
diff --git a/internal/command/deploy/machinebasedtest.go b/internal/command/deploy/machinebasedtest.go
index fe9992d77..caa4af56e 100644
--- a/internal/command/deploy/machinebasedtest.go
+++ b/internal/command/deploy/machinebasedtest.go
@@ -43,6 +43,7 @@ func (md *machineDeployment) runTestMachines(ctx context.Context, machineToTest
 			return nil
 		}
 	})
+	machineChecks = append(machineChecks, md.appConfig.MachineChecks...)
 
 	if len(machineChecks) == 0 {
 		span.AddEvent("no machine checks")
diff --git a/test/preflight/fly_deploy_test.go b/test/preflight/fly_deploy_test.go
index 690aeef6d..79be98b52 100644
--- a/test/preflight/fly_deploy_test.go
+++ b/test/preflight/fly_deploy_test.go
@@ -250,6 +250,24 @@ func TestFlyDeploy_DeployMachinesCheck(t *testing.T) {
 	require.Contains(f, output, "Test Machine")
 }
 
+func TestFlyDeploy_NoServiceDeployMachinesCheck(t *testing.T) {
+	f := testlib.NewTestEnvFromEnv(t)
+	appName := f.CreateRandomAppName()
+	f.Fly("launch --org %s --name %s --region %s --image nginx --internal-port 80 --ha=false", f.OrgSlug(), appName, f.PrimaryRegion())
+	appConfig := f.ReadFile("fly.toml")
+	appConfig += `
+		[[machine_checks]]
+			image = "curlimages/curl"
+			entrypoint = ["/bin/sh", "-c"]
+			command = ["curl http://[$FLY_TEST_MACHINE_IP]:80"]
+		`
+	f.WriteFlyToml(appConfig)
+	f.OverrideAuthAccessToken(f.Fly("tokens deploy").StdOut().String())
+	deployRes := f.Fly("deploy")
+	output := deployRes.StdOutString()
+	require.Contains(f, output, "Test Machine")
+}
+
 func TestFlyDeploy_DeployMachinesCheckCanary(t *testing.T) {
 	f := testlib.NewTestEnvFromEnv(t)
 	appName := f.CreateRandomAppName()