[machine] Add versioning to the test_machine_monitor app.

Switchboard RPis don't run under kubernetes, so we don't have
a docker image name to display, so we add a version that
uses similar information: the USER, datetime, git hash,
and git state to construct a version string that is passed in from
main and is threaded through all the code to appear in the web UI.

The version string is built in the Makefile and passed to the
compiler via -ldflags. See:

  https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications

Change-Id: If337b1f32158a05e6e8886894bca9c6a8b7b133f
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/433004
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/machine/Makefile b/machine/Makefile
index 021d3ba..7977b32 100644
--- a/machine/Makefile
+++ b/machine/Makefile
@@ -75,21 +75,28 @@
 run-local-instance:
 	machineserver --local --logtostderr
 
-build_test_machine_monitor_rpi:
+export HASH=$(shell git rev-parse HEAD)
+export DATETIME=$(date --utc "+%Y-%m-%dT%H:%M:%SZ")
+export GIT_STATE=$(shell ../bash/gitstate.sh)
+export VERSION=$(DATETIME)-${USER}-${HASH}-${GIT_STATE}
+
+# The names of these targets can't change, as their form is dictated by ansible playbooks in //skolo/ansible/switchboard.
+build_test_machine_monitor_aarch64_Linux:
 	CGO_ENABLED=0 \
 	GOOS=linux \
 	GOARCH=arm64 \
-	go build -o ./build/linux/arm64/test_machine_monitor ./go/test_machine_monitor
+	go build \
+	  -o ./build/Linux/aarch64/test_machine_monitor \
+	  -ldflags="-X 'main.Version=${VERSION}'" \
+	  ./go/test_machine_monitor
 
-# Run the following command prefixed with the the IP address of the local machine
-# to be updated, for example:
-#
-#    $ TARGET=192.168.1.158 make push_test_machine_monitor_rpi
-#
-push_test_machine_monitor_rpi: build_test_machine_monitor_rpi
-	ANSIBLE_CONFIG=../skolo/ansible/ansible.cfg \
-	ANSIBLE_SSH_ARGS="-F ${HOME}/.ssh/config" \
-	ansible-playbook ../skolo/ansible/switchboard/install-test-machine-monitor.yml \
-	--extra-vars variable_hosts=${TARGET}
+build_test_machine_monitor_x86_64_Linux:
+	CGO_ENABLED=0 \
+	GOOS=linux \
+	GOARCH=amd64 \
+	go build \
+	 -o ./build/Linux/x86_64/test_machine_monitor \
+	  -ldflags="-X 'main.Version=${VERSION}'" \
+	 ./go/test_machine_monitor
 
 include ../make/npm.mk
diff --git a/machine/go/machine/machine.go b/machine/go/machine/machine.go
index 130066a..8bf5110 100644
--- a/machine/go/machine/machine.go
+++ b/machine/go/machine/machine.go
@@ -67,6 +67,9 @@
 	// KubernetesImage is the kubernetes image name.
 	KubernetesImage string
 
+	// Version of test_machine_monitor being run.
+	Version string
+
 	// ScheduledForDeletion will be a non-empty string and equal to PodName if
 	// the pod should be deleted.
 	ScheduledForDeletion string
@@ -135,6 +138,9 @@
 	// KubernetesImage is the container image being run.
 	KubernetesImage string `json:"image"`
 
+	// Version of test_machine_monitor being run.
+	Version string `json:"version"`
+
 	// StartTim is when the test_machine_monitor started running.
 	StartTime time.Time `json:"start_time"`
 }
diff --git a/machine/go/machine/machine_test.go b/machine/go/machine/machine_test.go
index 82f8e67..1e4ce93 100644
--- a/machine/go/machine/machine_test.go
+++ b/machine/go/machine/machine_test.go
@@ -32,6 +32,7 @@
 		},
 		PodName:              "rpi-swarming-1235-987",
 		KubernetesImage:      "gcr.io/skia-public/rpi-swarming-client:2020-05-09T19_28_20Z-jcgregorio-4fef3ca-clean",
+		Version:              "v1.2",
 		ScheduledForDeletion: "rpi-swarming-1235-987",
 		LastUpdated:          testTime,
 		Battery:              91,
diff --git a/machine/go/machine/processor/impl.go b/machine/go/machine/processor/impl.go
index 0e8ffa3..bf3101e 100644
--- a/machine/go/machine/processor/impl.go
+++ b/machine/go/machine/processor/impl.go
@@ -171,6 +171,8 @@
 	ret.DeviceUptime = int32(event.Android.Uptime.Seconds())
 
 	ret.KubernetesImage = sanitizeKubernetesImageName(event.Host.KubernetesImage)
+	ret.Version = event.Host.Version
+
 	for k, values := range dimensions {
 		ret.Dimensions[k] = values
 	}
diff --git a/machine/go/machine/processor/impl_test.go b/machine/go/machine/processor/impl_test.go
index 41d356f..b2e58fd 100644
--- a/machine/go/machine/processor/impl_test.go
+++ b/machine/go/machine/processor/impl_test.go
@@ -13,6 +13,10 @@
 	"go.skia.org/infra/machine/go/machine"
 )
 
+const (
+	myTestVersion = "2021-07-22-jcgregorio-78bcc725fef1e29b518291469b8ad8f0cc3b21e4"
+)
+
 func TestParseAndroidProperties_HappyPath(t *testing.T) {
 	unittest.SmallTest(t)
 
@@ -154,6 +158,7 @@
 			Name:            "skia-rpi2-0001",
 			PodName:         "rpi-swarming-12345-987",
 			KubernetesImage: "gcr.io/skia-public/rpi-swarming-client:2020-05-09T19_28_20Z-jcgregorio-4fef3ca-clean",
+			Version:         myTestVersion,
 		},
 	}
 
@@ -178,6 +183,7 @@
 	assert.Equal(t, event.Host.PodName, next.PodName)
 	assert.Equal(t, event.Host.KubernetesImage, next.KubernetesImage)
 	assert.Equal(t, uptime, next.DeviceUptime)
+	assert.Equal(t, myTestVersion, next.Version)
 }
 
 func TestProcess_DetectInsideDocker(t *testing.T) {
@@ -241,7 +247,7 @@
 	assert.Equal(t, machine.ModeAvailable, next.Mode)
 }
 
-func TestProcess_ClearScheduledForDelectionOnPodNameChange(t *testing.T) {
+func TestProcess_ClearScheduledForDeletionOnPodNameChange(t *testing.T) {
 	unittest.SmallTest(t)
 	ctx := context.Background()
 
diff --git a/machine/go/test_machine_monitor/machine/machine.go b/machine/go/test_machine_monitor/machine/machine.go
index 4cc7eb6..f0c79ec 100644
--- a/machine/go/test_machine_monitor/machine/machine.go
+++ b/machine/go/test_machine_monitor/machine/machine.go
@@ -44,6 +44,9 @@
 	// KubernetesImage is the container image being run.
 	KubernetesImage string
 
+	// Version of test_machine_monitor being run.
+	Version string
+
 	// startTime is the time when this machine started running.
 	startTime time.Time
 
@@ -67,7 +70,7 @@
 }
 
 // New return an instance of *Machine.
-func New(ctx context.Context, local bool, instanceConfig config.InstanceConfig, startTime time.Time) (*Machine, error) {
+func New(ctx context.Context, local bool, instanceConfig config.InstanceConfig, startTime time.Time, version string) (*Machine, error) {
 	store, err := store.New(ctx, false, instanceConfig)
 	if err != nil {
 		return nil, skerr.Wrapf(err, "Failed to build store instance.")
@@ -97,6 +100,7 @@
 		MachineID:                  machineID,
 		Hostname:                   hostname,
 		KubernetesImage:            kubernetesImage,
+		Version:                    version,
 		startTime:                  startTime,
 		interrogateTimer:           metrics2.GetFloat64SummaryMetric("bot_config_machine_interrogate_timer", map[string]string{"machine": machineID}),
 		interrogateAndSendFailures: metrics2.GetCounter("bot_config_machine_interrogate_and_send_errors", map[string]string{"machine": machineID}),
@@ -112,6 +116,7 @@
 	ret.Host.Name = m.MachineID
 	ret.Host.PodName = m.Hostname
 	ret.Host.KubernetesImage = m.KubernetesImage
+	ret.Host.Version = m.Version
 
 	if props, err := m.adb.RawProperties(ctx); err != nil {
 		sklog.Infof("Failed to read android properties: %s", err)
diff --git a/machine/go/test_machine_monitor/machine/machine_manual_test.go b/machine/go/test_machine_monitor/machine/machine_manual_test.go
index ea16c5a..b7e6325 100644
--- a/machine/go/test_machine_monitor/machine/machine_manual_test.go
+++ b/machine/go/test_machine_monitor/machine/machine_manual_test.go
@@ -25,6 +25,7 @@
 const (
 	adbShellGetPropSuccess = `[ro.product.manufacturer]: [asus]`
 	adbShellDumpSysBattery = `This is dumpsys output.`
+	versionForTest         = "some-version-string-for-testing-purposes"
 )
 
 func setupConfig(t *testing.T) (context.Context, *pubsub.Topic, config.InstanceConfig) {
@@ -94,7 +95,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 	require.NoError(t, err)
 	assert.Equal(t, "my-test-bot-001", m.MachineID)
 
@@ -196,7 +197,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 	require.NoError(t, err)
 	assert.Equal(t, "my-test-bot-001", m.MachineID)
 
@@ -279,7 +280,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 	require.NoError(t, err)
 
 	// Set up fakes for adb. We have two sets of 3 since Start calls
@@ -365,7 +366,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 	// We are running a task.
 	m.runningTask = true
 	require.NoError(t, err)
@@ -440,7 +441,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 	m.SetDimensionsForSwarming(machine.SwarmingDimensions{
 		machine.DimAndroidDevices: {"1"},
 	})
@@ -483,7 +484,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 
 	m.SetDimensionsForSwarming(machine.SwarmingDimensions{
 		machine.DimAndroidDevices: {"1"},
@@ -513,7 +514,7 @@
 
 	// Create a Machine instance.
 	start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
-	m, err := New(ctx, true, instanceConfig, start)
+	m, err := New(ctx, true, instanceConfig, start, versionForTest)
 
 	m.SetDimensionsForSwarming(machine.SwarmingDimensions{
 		machine.DimAndroidDevices: {},
diff --git a/machine/go/test_machine_monitor/main.go b/machine/go/test_machine_monitor/main.go
index adca5d0..fa2644a 100644
--- a/machine/go/test_machine_monitor/main.go
+++ b/machine/go/test_machine_monitor/main.go
@@ -38,12 +38,18 @@
 	swarmingBotZip   = flag.String("swarming_bot_zip", "/b/s/swarming_bot.zip", "Absolute path to where the swarming_bot.zip code should run from.")
 )
 
+var (
+	// Version can be changed via -ldflags.
+	Version = "development"
+)
+
 func main() {
 	common.InitWithMust(
 		"test_machine_monitor",
 		common.PrometheusOpt(promPort),
 		common.MetricsLoggingOpt(),
 	)
+	sklog.Infof("Version: %s", Version)
 	var instanceConfig config.InstanceConfig
 	b, err := fs.ReadFile(configs.Configs, *configFlag)
 	if err != nil {
@@ -59,7 +65,7 @@
 	}
 
 	ctx := context.Background()
-	machineState, err := machine.New(ctx, *local, instanceConfig, time.Now())
+	machineState, err := machine.New(ctx, *local, instanceConfig, time.Now(), Version)
 	if err != nil {
 		sklog.Fatal("Failed to create machine: %s", err)
 	}
diff --git a/machine/modules/json/index.ts b/machine/modules/json/index.ts
index 2ba0fbb..2664506 100644
--- a/machine/modules/json/index.ts
+++ b/machine/modules/json/index.ts
@@ -13,6 +13,7 @@
 	Dimensions: SwarmingDimensions;
 	PodName: string;
 	KubernetesImage: string;
+	Version: string;
 	ScheduledForDeletion: string;
 	PowerCycle: boolean;
 	LastUpdated: string;
diff --git a/machine/modules/machine-server-sk/demo_data.ts b/machine/modules/machine-server-sk/demo_data.ts
index 66d1507..95468e9 100644
--- a/machine/modules/machine-server-sk/demo_data.ts
+++ b/machine/modules/machine-server-sk/demo_data.ts
@@ -1,366 +1,376 @@
-import {Description} from '../json';
+import { Description } from '../json';
 
-export const fakeNow = Date.parse('2021-06-03T18:20:30.00000Z')
+export const fakeNow = Date.parse('2021-06-03T18:20:30.00000Z');
 
 // Based on a production response on 2021-06-03.
 export const descriptions: Description[] = [{
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Leaving recovery mode.",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:20:24.97453Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Leaving recovery mode.',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:20:24.97453Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["Q", "QP1A.190711.020", "QP1A.190711.020_G980FXXU1ATB3"],
-    "device_os_flavor": ["samsung"],
-    "device_os_type": ["user"],
-    "device_type": ["x1s", "exynos990"],
-    "id": ["skia-rpi2-rack4-shelf1-001"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['Q', 'QP1A.190711.020', 'QP1A.190711.020_G980FXXU1ATB3'],
+    device_os_flavor: ['samsung'],
+    device_os_type: ['user'],
+    device_type: ['x1s', 'exynos990'],
+    id: ['skia-rpi2-rack4-shelf1-001'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-jg6kz",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:24.974527Z",
-  "Battery": 100,
-  "Temperature": {
-    "TYPE_BATTERY": 24.2,
-    "TYPE_CPU": 29.1,
-    "TYPE_SKIN": 26.3,
-    "TYPE_USB_PORT": 23.2,
-    "dumpsys_battery": 24.3
+  PodName: 'rpi-swarming-jg6kz',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:24.974527Z',
+  Battery: 100,
+  Temperature: {
+    TYPE_BATTERY: 24.2,
+    TYPE_CPU: 29.1,
+    TYPE_SKIN: 26.3,
+    TYPE_USB_PORT: 23.2,
+    dumpsys_battery: 24.3,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "2021-06-03T18:20:09.386312Z",
-  "DeviceUptime": 167
+  RunningSwarmingTask: true,
+  RecoveryStart: '2021-06-03T18:20:09.386312Z',
+  DeviceUptime: 167,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-qdgf2\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:20:18.710419Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-qdgf2"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:20:18.710419Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["H", "HUAWEIELE-L29", "HUAWEIELE-L29_9.1.0.241C605"],
-    "device_os_flavor": ["huawei"],
-    "device_os_type": ["user"],
-    "device_type": ["HWELE", "ELE"],
-    "id": ["skia-rpi2-rack4-shelf1-002"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['H', 'HUAWEIELE-L29', 'HUAWEIELE-L29_9.1.0.241C605'],
+    device_os_flavor: ['huawei'],
+    device_os_type: ['user'],
+    device_type: ['HWELE', 'ELE'],
+    id: ['skia-rpi2-rack4-shelf1-002'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-qdgf2",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "rpi-swarming-qdgf2",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:18.710416Z",
-  "Battery": 100,
-  "Temperature": {
-    "dumpsys_battery": 23
+  PodName: 'rpi-swarming-qdgf2',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.3',
+  ScheduledForDeletion: 'rpi-swarming-qdgf2',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:18.710416Z',
+  Battery: 100,
+  Temperature: {
+    dumpsys_battery: 23,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "0001-01-01T00:00:00Z",
-  "DeviceUptime": 266
+  RunningSwarmingTask: true,
+  RecoveryStart: '0001-01-01T00:00:00Z',
+  DeviceUptime: 266,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-5hqvb\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:20:18.967714Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-5hqvb"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:20:18.967714Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["H", "HUAWEIELE-L29", "HUAWEIELE-L29_9.1.0.241C605"],
-    "device_os_flavor": ["huawei"],
-    "device_os_type": ["user"],
-    "device_type": ["HWELE", "ELE"],
-    "id": ["skia-rpi2-rack4-shelf1-003"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['H', 'HUAWEIELE-L29', 'HUAWEIELE-L29_9.1.0.241C605'],
+    device_os_flavor: ['huawei'],
+    device_os_type: ['user'],
+    device_type: ['HWELE', 'ELE'],
+    id: ['skia-rpi2-rack4-shelf1-003'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-pnp6w",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:20.87764Z",
-  "Battery": 100,
-  "Temperature": {
-    "dumpsys_battery": 22
+  PodName: 'rpi-swarming-pnp6w',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.4',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:20.87764Z',
+  Battery: 100,
+  Temperature: {
+    dumpsys_battery: 22,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "0001-01-01T00:00:00Z",
-  "DeviceUptime": 183
+  RunningSwarmingTask: true,
+  RecoveryStart: '0001-01-01T00:00:00Z',
+  DeviceUptime: 183,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-q2vpj\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:15:13.910199Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-q2vpj"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:15:13.910199Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["H", "HUAWEIELE-L29", "HUAWEIELE-L29_9.1.0.241C605"],
-    "device_os_flavor": ["huawei"],
-    "device_os_type": ["user"],
-    "device_type": ["HWELE", "ELE"],
-    "id": ["skia-rpi2-rack4-shelf1-004"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['H', 'HUAWEIELE-L29', 'HUAWEIELE-L29_9.1.0.241C605'],
+    device_os_flavor: ['huawei'],
+    device_os_type: ['user'],
+    device_type: ['HWELE', 'ELE'],
+    id: ['skia-rpi2-rack4-shelf1-004'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-b6brg",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:02.034149Z",
-  "Battery": 100,
-  "Temperature": {
-    "dumpsys_battery": 22
+  PodName: 'rpi-swarming-b6brg',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:02.034149Z',
+  Battery: 100,
+  Temperature: {
+    dumpsys_battery: 22,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "0001-01-01T00:00:00Z",
-  "DeviceUptime": 167
+  RunningSwarmingTask: true,
+  RecoveryStart: '0001-01-01T00:00:00Z',
+  DeviceUptime: 167,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-k8fdn\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:19:56.440311Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-k8fdn"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:19:56.440311Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["H", "HUAWEIELE-L29", "HUAWEIELE-L29_9.1.0.241C605"],
-    "device_os_flavor": ["huawei"],
-    "device_os_type": ["user"],
-    "device_type": ["HWELE", "ELE"],
-    "id": ["skia-rpi2-rack4-shelf1-005"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['H', 'HUAWEIELE-L29', 'HUAWEIELE-L29_9.1.0.241C605'],
+    device_os_flavor: ['huawei'],
+    device_os_type: ['user'],
+    device_type: ['HWELE', 'ELE'],
+    id: ['skia-rpi2-rack4-shelf1-005'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-mzg6v",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:21.471856Z",
-  "Battery": 100,
-  "Temperature": {
-    "dumpsys_battery": 23
+  PodName: 'rpi-swarming-mzg6v',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:21.471856Z',
+  Battery: 100,
+  Temperature: {
+    dumpsys_battery: 23,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "0001-01-01T00:00:00Z",
-  "DeviceUptime": 234
+  RunningSwarmingTask: true,
+  RecoveryStart: '0001-01-01T00:00:00Z',
+  DeviceUptime: 234,
 }, {
-  "Mode": "recovery",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-j9lzl\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:20:13.827511Z"
+  Mode: 'recovery',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-j9lzl"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:20:13.827511Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["Q", "QP1A.190711.020", "QP1A.190711.020_G980FXXU1ATBM"],
-    "device_os_flavor": ["samsung"],
-    "device_os_type": ["user"],
-    "device_type": ["x1s", "exynos990"],
-    "id": ["skia-rpi2-rack4-shelf1-006"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['Q', 'QP1A.190711.020', 'QP1A.190711.020_G980FXXU1ATBM'],
+    device_os_flavor: ['samsung'],
+    device_os_type: ['user'],
+    device_type: ['x1s', 'exynos990'],
+    id: ['skia-rpi2-rack4-shelf1-006'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-b5zk5",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:33.121421Z",
-  "Battery": 94,
-  "Temperature": {
-    "TYPE_BATTERY": 24.9,
-    "TYPE_CPU": 36.2,
-    "TYPE_SKIN": 28.8,
-    "TYPE_USB_PORT": 23.8,
-    "dumpsys_battery": 24.9
+  PodName: 'rpi-swarming-b5zk5',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:33.121421Z',
+  Battery: 94,
+  Temperature: {
+    TYPE_BATTERY: 24.9,
+    TYPE_CPU: 36.2,
+    TYPE_SKIN: 28.8,
+    TYPE_USB_PORT: 23.8,
+    dumpsys_battery: 24.9,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "2021-06-03T18:19:19.268204Z",
-  "DeviceUptime": 343
+  RunningSwarmingTask: true,
+  RecoveryStart: '2021-06-03T18:19:19.268204Z',
+  DeviceUptime: 343,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-d86nk\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:19:49.07976Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-d86nk"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:19:49.07976Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["P", "PQ1A.190105.004", "PQ1A.190105.004_5148680"],
-    "device_os_flavor": ["google"],
-    "device_os_type": ["userdebug"],
-    "device_type": ["blueline"],
-    "id": ["skia-rpi2-rack4-shelf1-007"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['P', 'PQ1A.190105.004', 'PQ1A.190105.004_5148680'],
+    device_os_flavor: ['google'],
+    device_os_type: ['userdebug'],
+    device_type: ['blueline'],
+    id: ['skia-rpi2-rack4-shelf1-007'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-92k5w",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:09.386348Z",
-  "Battery": 99,
-  "Temperature": {
-    "dumpsys_battery": 24.9
+  PodName: 'rpi-swarming-92k5w',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:09.386348Z',
+  Battery: 99,
+  Temperature: {
+    dumpsys_battery: 24.9,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "2021-01-12T18:33:24.063867Z",
-  "DeviceUptime": 657
+  RunningSwarmingTask: true,
+  RecoveryStart: '2021-01-12T18:33:24.063867Z',
+  DeviceUptime: 657,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Leaving recovery mode.",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:14:53.393161Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Leaving recovery mode.',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:14:53.393161Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["Q", "QP1A.190711.020", "QP1A.190711.020_G980FXXU1ATB3"],
-    "device_os_flavor": ["samsung"],
-    "device_os_type": ["user"],
-    "device_type": ["x1s", "exynos990"],
-    "id": ["skia-rpi2-rack4-shelf1-008"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['Q', 'QP1A.190711.020', 'QP1A.190711.020_G980FXXU1ATB3'],
+    device_os_flavor: ['samsung'],
+    device_os_type: ['user'],
+    device_type: ['x1s', 'exynos990'],
+    id: ['skia-rpi2-rack4-shelf1-008'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-cgzxt",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:35.74389Z",
-  "Battery": 100,
-  "Temperature": {
-    "TYPE_BATTERY": 24.2,
-    "TYPE_CPU": 25.2,
-    "TYPE_SKIN": 25.5,
-    "TYPE_USB_PORT": 24,
-    "dumpsys_battery": 24.2
+  PodName: 'rpi-swarming-cgzxt',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:35.74389Z',
+  Battery: 100,
+  Temperature: {
+    TYPE_BATTERY: 24.2,
+    TYPE_CPU: 25.2,
+    TYPE_SKIN: 25.5,
+    TYPE_USB_PORT: 24,
+    dumpsys_battery: 24.2,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "2021-06-03T18:12:59.603173Z",
-  "DeviceUptime": 660
+  RunningSwarmingTask: true,
+  RecoveryStart: '2021-06-03T18:12:59.603173Z',
+  DeviceUptime: 660,
 }, {
-  "Mode": "recovery",
-  "Annotation": {
-    "Message": "Too hot. ",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:20:05.427786Z"
+  Mode: 'recovery',
+  Annotation: {
+    Message: 'Too hot. ',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:20:05.427786Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["Q", "QP1A.190711.020", "QP1A.190711.020_G980FXXU1ATB3"],
-    "device_os_flavor": ["samsung"],
-    "device_os_type": ["user"],
-    "device_type": ["x1s", "exynos990"],
-    "id": ["skia-rpi2-rack4-shelf1-009"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['Q', 'QP1A.190711.020', 'QP1A.190711.020_G980FXXU1ATB3'],
+    device_os_flavor: ['samsung'],
+    device_os_type: ['user'],
+    device_type: ['x1s', 'exynos990'],
+    id: ['skia-rpi2-rack4-shelf1-009'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-7jx68",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:35.282608Z",
-  "Battery": 91,
-  "Temperature": {
-    "TYPE_BATTERY": 28.8,
-    "TYPE_CPU": 41.4,
-    "TYPE_SKIN": 32.3,
-    "TYPE_USB_PORT": 26.4,
-    "dumpsys_battery": 28.8
+  PodName: 'rpi-swarming-7jx68',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:35.282608Z',
+  Battery: 91,
+  Temperature: {
+    TYPE_BATTERY: 28.8,
+    TYPE_CPU: 41.4,
+    TYPE_SKIN: 32.3,
+    TYPE_USB_PORT: 26.4,
+    dumpsys_battery: 28.8,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "2021-06-03T18:20:05.427785Z",
-  "DeviceUptime": 891
+  RunningSwarmingTask: true,
+  RecoveryStart: '2021-06-03T18:20:05.427785Z',
+  DeviceUptime: 891,
 }, {
-  "Mode": "available",
-  "Annotation": {
-    "Message": "Pod too old, requested update for \"rpi-swarming-ghncz\"",
-    "User": "machines.skia.org",
-    "Timestamp": "2021-06-03T18:19:55.480856Z"
+  Mode: 'available',
+  Annotation: {
+    Message: 'Pod too old, requested update for "rpi-swarming-ghncz"',
+    User: 'machines.skia.org',
+    Timestamp: '2021-06-03T18:19:55.480856Z',
   },
-  "Note": {
-    "Message": "",
-    "User": "",
-    "Timestamp": "0001-01-01T00:00:00Z"
+  Note: {
+    Message: '',
+    User: '',
+    Timestamp: '0001-01-01T00:00:00Z',
   },
-  "Dimensions": {
-    "android_devices": ["1"],
-    "device_os": ["Q", "QP1A.190711.020", "QP1A.190711.020_G980FXXU1ATBM"],
-    "device_os_flavor": ["samsung"],
-    "device_os_type": ["user"],
-    "device_type": ["x1s", "exynos990"],
-    "id": ["skia-rpi2-rack4-shelf1-010"],
-    "inside_docker": ["1", "containerd"],
-    "os": ["Android"]
+  Dimensions: {
+    android_devices: ['1'],
+    device_os: ['Q', 'QP1A.190711.020', 'QP1A.190711.020_G980FXXU1ATBM'],
+    device_os_flavor: ['samsung'],
+    device_os_type: ['user'],
+    device_type: ['x1s', 'exynos990'],
+    id: ['skia-rpi2-rack4-shelf1-010'],
+    inside_docker: ['1', 'containerd'],
+    os: ['Android'],
   },
-  "PodName": "rpi-swarming-fptzl",
-  "KubernetesImage": "gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean",
-  "ScheduledForDeletion": "",
-  "PowerCycle": false,
-  "LastUpdated": "2021-06-03T18:20:23.312632Z",
-  "Battery": 100,
-  "Temperature": {
-    "TYPE_BATTERY": 25.1,
-    "TYPE_CPU": 26.2,
-    "TYPE_SKIN": 26.5,
-    "TYPE_USB_PORT": 25,
-    "dumpsys_battery": 25
+  PodName: 'rpi-swarming-fptzl',
+  KubernetesImage: 'gcr.io/skia-public/rpi-swarming-client:2020-08-18T17_53_11Z-jcgregorio-06c2067-clean',
+  Version: 'v1.2',
+  ScheduledForDeletion: '',
+  PowerCycle: false,
+  LastUpdated: '2021-06-03T18:20:23.312632Z',
+  Battery: 100,
+  Temperature: {
+    TYPE_BATTERY: 25.1,
+    TYPE_CPU: 26.2,
+    TYPE_SKIN: 26.5,
+    TYPE_USB_PORT: 25,
+    dumpsys_battery: 25,
   },
-  "RunningSwarmingTask": true,
-  "RecoveryStart": "2021-06-03T18:15:16.668578Z",
-  "DeviceUptime": 899
+  RunningSwarmingTask: true,
+  RecoveryStart: '2021-06-03T18:15:16.668578Z',
+  DeviceUptime: 899,
 }];
diff --git a/machine/modules/machine-server-sk/machine-server-sk-demo.ts b/machine/modules/machine-server-sk/machine-server-sk-demo.ts
index 53851b7..e964987 100644
--- a/machine/modules/machine-server-sk/machine-server-sk-demo.ts
+++ b/machine/modules/machine-server-sk/machine-server-sk-demo.ts
@@ -1,8 +1,8 @@
 import './index';
 
 import fetchMock from 'fetch-mock';
-import {descriptions, fakeNow} from './demo_data';
-import {MachineServerSk} from './machine-server-sk';
+import { descriptions, fakeNow } from './demo_data';
+import { MachineServerSk } from './machine-server-sk';
 
 Date.now = () => fakeNow;
 
diff --git a/machine/modules/machine-server-sk/machine-server-sk.ts b/machine/modules/machine-server-sk/machine-server-sk.ts
index aed5353..c875e7c 100644
--- a/machine/modules/machine-server-sk/machine-server-sk.ts
+++ b/machine/modules/machine-server-sk/machine-server-sk.ts
@@ -126,6 +126,10 @@
 };
 
 const imageName = (machine: Description): string => {
+  // Prefer displaying the Version over the KubernetesImage.
+  if (machine.Version) {
+    return machine.Version;
+  }
   // KubernetesImage looks like:
   // "gcr.io/skia-public/rpi-swarming-client:2020-05-09T19_28_20Z-jcgregorio-4fef3ca-clean".
   // We just need to display everything after the ":".