VirtualMachine: Make VPN optional

Change-Id: I22c8235a651969665fc50e6b2841d710ca137109
diff --git a/core/installer/derived.go b/core/installer/derived.go
index d99f02b..0351b21 100644
--- a/core/installer/derived.go
+++ b/core/installer/derived.go
@@ -90,6 +90,17 @@
 				}
 			}
 			if def.Kind() == KindVPNAuthKey {
+				enabled := true
+				if v, ok := def.Meta()["enabledField"]; ok {
+					// TODO(gio): Improve getField
+					enabled, ok = getField(root, v).(bool)
+					if !ok {
+						return nil, fmt.Errorf("could not resolve enabled: %+v %s %+v", def.Meta(), v, root)
+					}
+				}
+				if !enabled {
+					continue
+				}
 				var username string
 				if v, ok := def.Meta()["username"]; ok {
 					username = v
diff --git a/core/installer/derived_test.go b/core/installer/derived_test.go
index 8f638b3..83e07ec 100644
--- a/core/installer/derived_test.go
+++ b/core/installer/derived_test.go
@@ -44,3 +44,55 @@
 		t.Fatal(v)
 	}
 }
+
+func TestDeriveVPNAuthKeyDisabled(t *testing.T) {
+	schema := structSchema{
+		"input",
+		[]Field{
+			Field{"username", basicSchema{"username", KindString, false, nil}},
+			Field{"enabled", basicSchema{"enabled", KindBoolean, false, nil}},
+			Field{"authKey", basicSchema{"authKey", KindVPNAuthKey, false, map[string]string{
+				"usernameField": "username",
+				"enabledField":  "enabled",
+			}}},
+		},
+		false,
+	}
+	input := map[string]any{
+		"username": "foo",
+		"enabled":  false,
+	}
+	v, err := deriveValues(input, input, schema, nil, testKeyGen{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, ok := v["authKey"].(string); ok {
+		t.Fatal(v)
+	}
+}
+
+func TestDeriveVPNAuthKeyEnabledExplicitly(t *testing.T) {
+	schema := structSchema{
+		"input",
+		[]Field{
+			Field{"username", basicSchema{"username", KindString, false, nil}},
+			Field{"enabled", basicSchema{"enabled", KindBoolean, false, nil}},
+			Field{"authKey", basicSchema{"authKey", KindVPNAuthKey, false, map[string]string{
+				"usernameField": "username",
+				"enabledField":  "enabled",
+			}}},
+		},
+		false,
+	}
+	input := map[string]any{
+		"username": "foo",
+		"enabled":  true,
+	}
+	v, err := deriveValues(input, input, schema, nil, testKeyGen{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if key, ok := v["authKey"].(string); !ok || key != "foo" {
+		t.Fatal(v)
+	}
+}
diff --git a/core/installer/schema.go b/core/installer/schema.go
index 04955b1..fcdebd4 100644
--- a/core/installer/schema.go
+++ b/core/installer/schema.go
@@ -242,6 +242,10 @@
 			if len(meta) != 1 {
 				return nil, fmt.Errorf("invalid vpn auth key field meta: %+v", meta)
 			}
+			enabledFieldAttr := v.Attribute("enabledField")
+			if enabledFieldAttr.Err() == nil {
+				meta["enabledField"] = strings.ToLower(enabledFieldAttr.Contents())
+			}
 			return basicSchema{name, KindVPNAuthKey, true, meta}, nil
 		} else {
 			return basicSchema{name, KindString, false, nil}, nil
diff --git a/core/installer/values-tmpl/virtual-machine.cue b/core/installer/values-tmpl/virtual-machine.cue
index 5b08788..6f311d9 100644
--- a/core/installer/values-tmpl/virtual-machine.cue
+++ b/core/installer/values-tmpl/virtual-machine.cue
@@ -1,9 +1,10 @@
 input: {
 	name: string @name(Hostname)
 	username: string @name(Username)
-	authKey: string @name(Auth Key) @role(VPNAuthKey) @usernameField(username)
+	authKey?: string @name(Auth Key) @role(VPNAuthKey) @usernameField(username) @enabledField(vpnEnabled)
 	cpuCores: int | *1 @name(CPU Cores)
 	memory: string | *"2Gi" @name(Memory)
+	vpnEnabled: bool @name(Enable VPN)
 }
 
 name: "Virutal Machine"
@@ -20,10 +21,15 @@
 			domain: global.domain
 			cpuCores: input.cpuCores
 			memory: input.memory
-			vpn: {
-				enabled: true
-				loginServer: "https://headscale.\(global.domain)"
-				authKey: input.authKey
+			if !input.vpnEnabled {
+				vpn: enabled: false
+			}
+			if input.vpnEnabled {
+				vpn: {
+					enabled: true
+					loginServer: "https://headscale.\(global.domain)"
+					authKey: input.authKey
+				}
 			}
 		}
 	}