cos-customizer: Add -machine-type flag

This new flag allows users to select the machine type of the preload VM.

BUG=b/176990931
TEST=./run_tests.sh

Change-Id: I9e514e6e0eced8bf981067d3779c26369ea7574a
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/26021
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Tested-by: Robert Kolchmeyer <rkolchmeyer@google.com>
Reviewed-by: Roy Yang <royyang@google.com>
diff --git a/src/cmd/cos_customizer/README.md b/src/cmd/cos_customizer/README.md
index 821177a..f4b51ee 100644
--- a/src/cmd/cos_customizer/README.md
+++ b/src/cmd/cos_customizer/README.md
@@ -259,6 +259,11 @@
 overall Cloud Build workflow timeout expires, the task will be cancelled without
 any opportunity to clean up resources.
 
+`-machine-type`: The machine type to use for the COS Customizer preload VM.
+Defaults to `n1-standard-1`. Useful for optimizing costs. Note that this is
+separate from the Cloud Build machine type option, which sets the machine type
+of the Cloud Build VM, which is different from the COS Customizer preload VM.
+
 An example `finish-image-build` step looks like the following:
 
     - name: 'gcr.io/cos-cloud/cos-customizer'
diff --git a/src/cmd/cos_customizer/finish_image_build.go b/src/cmd/cos_customizer/finish_image_build.go
index 1707bac..e69870b 100644
--- a/src/cmd/cos_customizer/finish_image_build.go
+++ b/src/cmd/cos_customizer/finish_image_build.go
@@ -41,6 +41,7 @@
 	imageProject   string
 	zone           string
 	project        string
+	machineType    string
 	imageName      string
 	imageSuffix    string
 	imageFamily    string
@@ -86,6 +87,7 @@
 		"the deleted state).")
 	flags.StringVar(&f.zone, "zone", "", "Zone to make GCE resources in.")
 	flags.StringVar(&f.project, "project", "", "Project to make GCE resources in.")
+	flags.StringVar(&f.machineType, "machine-type", "n1-standard-1", "Machine type to use during the build.")
 	if f.labels == nil {
 		f.labels = newMapVar()
 	}
@@ -153,6 +155,7 @@
 	}
 	buildConfig.Project = f.project
 	buildConfig.Zone = f.zone
+	buildConfig.MachineType = f.machineType
 	buildConfig.DiskSize = f.diskSize
 	buildConfig.Timeout = f.timeout.String()
 	provConfig := &provisioner.Config{}
diff --git a/src/data/build_image.wf.json b/src/data/build_image.wf.json
index 7329148..8b280ae 100644
--- a/src/data/build_image.wf.json
+++ b/src/data/build_image.wf.json
@@ -7,7 +7,8 @@
     "output_image_project": {"Required": true, "Description": "Project of output image."},
     "cidata_img": {"Required": true, "Description": "Path to CIDATA vfat image containing cloud-init user-data and the provisioner program. Must be in .tar.gz format."},
     "disk_size_gb": {"Value": "10", "Description": "The disk size to use for preloading."},
-    "host_maintenance": {"Value": "MIGRATE", "Description": "VM behavior when there is maintenance."}
+    "host_maintenance": {"Value": "MIGRATE", "Description": "VM behavior when there is maintenance."},
+    "machine_type": {"Required": true, "Description": "Machine type of the preload VM."}
   },
   "Sources": {
     "cloud-config": "/data/startup.yaml",
@@ -50,6 +51,7 @@
         {
           "Name": "preload-vm",
           "Disks": [{"Source": "boot-disk"}, {"Source": "cidata-disk"}],
+          "MachineType": "${machine_type}",
           "guestAccelerators": {{.Accelerators}},
           "scheduling": {
             "onHostMaintenance": "${host_maintenance}"
diff --git a/src/pkg/config/config.go b/src/pkg/config/config.go
index d680a28..498882b 100644
--- a/src/pkg/config/config.go
+++ b/src/pkg/config/config.go
@@ -66,6 +66,7 @@
 	GCSDir      string
 	Project     string
 	Zone        string
+	MachineType string
 	DiskSize    int
 	GPUType     string
 	Timeout     string
diff --git a/src/pkg/preloader/preload.go b/src/pkg/preloader/preload.go
index e5aa5ab..f135031 100644
--- a/src/pkg/preloader/preload.go
+++ b/src/pkg/preloader/preload.go
@@ -306,6 +306,8 @@
 		output.Project,
 		"-var:cidata_img",
 		ciDataFile,
+		"-var:machine_type",
+		buildSpec.MachineType,
 		"-var:host_maintenance",
 		hostMaintenance,
 		"-gcs_path",
diff --git a/src/pkg/preloader/preload_test.go b/src/pkg/preloader/preload_test.go
index c6e015f..09510e7 100644
--- a/src/pkg/preloader/preload_test.go
+++ b/src/pkg/preloader/preload_test.go
@@ -344,6 +344,13 @@
 			want:        []string{"-zone", "zone"},
 		},
 		{
+			testName:    "MachineType",
+			inputImage:  config.NewImage("", ""),
+			outputImage: config.NewImage("", ""),
+			buildConfig: &config.Build{MachineType: "n1-standard-1", GCSBucket: "bucket", GCSDir: "dir"},
+			want:        []string{"-var:machine_type", "n1-standard-1"},
+		},
+		{
 			testName:    "Timeout",
 			inputImage:  config.NewImage("", ""),
 			outputImage: config.NewImage("", ""),
diff --git a/testing/machine_type_test.yaml b/testing/machine_type_test.yaml
new file mode 100644
index 0000000..a230666
--- /dev/null
+++ b/testing/machine_type_test.yaml
@@ -0,0 +1,45 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the License);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an AS IS BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+substitutions:
+  "_TEST": "machine_type_test"
+  "_INPUT_IMAGE": "cos-dev-69-10895-0-0"
+  "_INPUT_PROJECT": "cos-cloud"
+steps:
+- name: "gcr.io/cloud-builders/bazel"
+  args: ["run", "--spawn_strategy=standalone", ":cos_customizer", "--", "--norun"]
+- name: "bazel:cos_customizer"
+  args: ["start-image-build",
+         "-build-context=testing/${_TEST}",
+         "-image-name=${_INPUT_IMAGE}",
+         "-image-project=${_INPUT_PROJECT}",
+         "-gcs-bucket=${PROJECT_ID}_cloudbuild",
+         "-gcs-workdir=customizer-$BUILD_ID"]
+- name: "bazel:cos_customizer"
+  args: ["run-script",
+         "-script=preload.sh"]
+- name: "bazel:cos_customizer"
+  args: ["finish-image-build",
+         "-machine-type=n1-standard-8",
+         "-zone=us-west1-b",
+         "-project=$PROJECT_ID",
+         "-image-name=preload-test-$BUILD_ID",
+         "-image-project=$PROJECT_ID"]
+- name: "gcr.io/compute-image-tools/daisy"
+  args: ["-project=$PROJECT_ID", "-zone=us-west1-b", "-var:image_name",
+         "preload-test-$BUILD_ID", "-var:image_project", "$PROJECT_ID",
+         "-var:test_cfg", "../${_TEST}/preload_test.cfg", "testing/util/run_test.wf.json"]
+options:
+  machineType: "N1_HIGHCPU_8"
+timeout: "7200s"
diff --git a/testing/machine_type_test/preload.sh b/testing/machine_type_test/preload.sh
new file mode 100644
index 0000000..840e0ad
--- /dev/null
+++ b/testing/machine_type_test/preload.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+#
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+nproc > /var/lib/nproc
diff --git a/testing/machine_type_test/preload_test.cfg b/testing/machine_type_test/preload_test.cfg
new file mode 100644
index 0000000..3359ed6
--- /dev/null
+++ b/testing/machine_type_test/preload_test.cfg
@@ -0,0 +1,79 @@
+#cloud-config
+#
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+write_files:
+  - path: /tmp/preloader-test/test.sh
+    permissions: 0644
+    owner: root
+    content: |
+      set -o errexit
+      set -o pipefail
+
+      trap 'fail exiting due to errors' EXIT
+
+      fail() {
+        echo "TestFail: $@"
+      }
+
+      testNproc() {
+        if [[ ! -f "/var/lib/nproc" ]]; then
+          echo "/var/lib/nproc is missing"
+          echo "testNproc fail"
+          RESULT="fail"
+          return
+        fi
+        if [[ "$(cat "/var/lib/nproc")" != "8" ]]; then
+          echo "/var/lib/nproc: got $(cat "/var/lib/nproc"), want 8"
+          echo "testNproc fail"
+          RESULT="fail"
+          return
+        fi
+        echo "testNproc pass"
+      }
+
+      main() {
+        RESULT="pass"
+        testNproc
+        if [[ "${RESULT}" == "fail" ]]; then
+          exit 1
+        fi
+      }
+
+      main 2>&1 | sed "s/^/TestStatus: /"
+      trap - EXIT
+      echo "TestPass: all tests passed"
+
+  - path: /etc/systemd/system/preloader-test.service
+    permissions: 0644
+    owner: root
+    content: |
+      [Unit]
+      Description=Preloader test
+      Wants=network-online.target gcr-online.target docker.service
+      After=network-online.target gcr-online.target docker.service
+
+      [Service]
+      Type=oneshot
+      RemainAfterExit=yes
+      User=root
+      ExecStart=/bin/bash /tmp/preloader-test/test.sh
+      StandardOutput=tty
+      StandardError=tty
+      TTYPath=/dev/ttyS1
+
+runcmd:
+  - systemctl daemon-reload
+  - systemctl --no-block start preloader-test.service