go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/model/model_test.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package model
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  
    21  	"go.chromium.org/luci/gae/impl/memory"
    22  	"go.chromium.org/luci/gae/service/datastore"
    23  	computealpha "google.golang.org/api/compute/v0.alpha"
    24  	"google.golang.org/api/compute/v1"
    25  	"google.golang.org/protobuf/testing/protocmp"
    26  
    27  	"go.chromium.org/luci/gce/api/config/v1"
    28  	"go.chromium.org/luci/gce/api/projects/v1"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/google/go-cmp/cmp/cmpopts"
    32  	. "github.com/smartystreets/goconvey/convey"
    33  	. "go.chromium.org/luci/common/testing/assertions"
    34  )
    35  
    36  func TestConfig(t *testing.T) {
    37  	t.Parallel()
    38  
    39  	Convey("Config", t, func() {
    40  		c := memory.Use(context.Background())
    41  		cfg := &Config{ID: "id"}
    42  		err := datastore.Get(c, cfg)
    43  		So(err, ShouldEqual, datastore.ErrNoSuchEntity)
    44  
    45  		err = datastore.Put(c, &Config{
    46  			ID: "id",
    47  			Config: &config.Config{
    48  				Attributes: &config.VM{
    49  					Disk: []*config.Disk{
    50  						{
    51  							Image: "image",
    52  						},
    53  					},
    54  					Project: "project",
    55  				},
    56  				Prefix: "prefix",
    57  			},
    58  		})
    59  		So(err, ShouldBeNil)
    60  
    61  		err = datastore.Get(c, cfg)
    62  		So(err, ShouldBeNil)
    63  		So(cmp.Diff(cfg, &Config{
    64  			ID: "id",
    65  			Config: &config.Config{
    66  				Attributes: &config.VM{
    67  					Disk: []*config.Disk{
    68  						{
    69  							Image: "image",
    70  						},
    71  					},
    72  					Project: "project",
    73  				},
    74  				Prefix: "prefix",
    75  			},
    76  		}, cmpopts.IgnoreUnexported(*cfg), protocmp.Transform()), ShouldBeEmpty)
    77  	})
    78  }
    79  
    80  func TestProject(t *testing.T) {
    81  	t.Parallel()
    82  
    83  	Convey("Project", t, func() {
    84  		c := memory.Use(context.Background())
    85  		p := &Project{ID: "id"}
    86  		err := datastore.Get(c, p)
    87  		So(err, ShouldEqual, datastore.ErrNoSuchEntity)
    88  
    89  		err = datastore.Put(c, &Project{
    90  			ID: "id",
    91  			Config: &projects.Config{
    92  				Metric: []string{
    93  					"metric-1",
    94  					"metric-2",
    95  				},
    96  				Project: "project",
    97  				Region: []string{
    98  					"region-1",
    99  					"region-2",
   100  				},
   101  			},
   102  		})
   103  		So(err, ShouldBeNil)
   104  
   105  		err = datastore.Get(c, p)
   106  		So(err, ShouldBeNil)
   107  
   108  		So(p.Config, ShouldResembleProto, &projects.Config{
   109  			Metric: []string{
   110  				"metric-1",
   111  				"metric-2",
   112  			},
   113  			Project: "project",
   114  			Region: []string{
   115  				"region-1",
   116  				"region-2",
   117  			},
   118  		})
   119  	})
   120  }
   121  
   122  func TestVM(t *testing.T) {
   123  	t.Parallel()
   124  
   125  	Convey("VM", t, func() {
   126  		c := memory.Use(context.Background())
   127  		v := &VM{
   128  			ID: "id",
   129  		}
   130  		err := datastore.Get(c, v)
   131  		So(err, ShouldEqual, datastore.ErrNoSuchEntity)
   132  
   133  		err = datastore.Put(c, &VM{
   134  			ID: "id",
   135  			Attributes: config.VM{
   136  				Project: "project",
   137  			},
   138  		})
   139  		So(err, ShouldBeNil)
   140  
   141  		So(datastore.Get(c, v), ShouldBeNil)
   142  
   143  		So(cmp.Diff(v, &VM{
   144  			ID: "id",
   145  			Attributes: config.VM{
   146  				Project: "project",
   147  			},
   148  		}, cmpopts.IgnoreUnexported(VM{}), protocmp.Transform()), ShouldBeEmpty)
   149  
   150  		Convey("IndexAttributes", func() {
   151  			v.IndexAttributes()
   152  			So(v.AttributesIndexed, ShouldBeEmpty)
   153  
   154  			v := &VM{
   155  				ID: "id",
   156  				Attributes: config.VM{
   157  					Disk: []*config.Disk{
   158  						{
   159  							Image: "global/images/image-1",
   160  						},
   161  						{
   162  							Image: "projects/project/global/images/image-2",
   163  						},
   164  					},
   165  				},
   166  			}
   167  			v.IndexAttributes()
   168  			So(v.AttributesIndexed, ShouldResemble, []string{"disk.image:image-1", "disk.image:image-2"})
   169  		})
   170  	})
   171  
   172  	Convey("getConfidentialInstanceConfig", t, func() {
   173  		Convey("zero", func() {
   174  			Convey("empty", func() {
   175  				v := &VM{}
   176  				c := v.getConfidentialInstanceConfig()
   177  				So(c, ShouldBeNil)
   178  				s := v.getScheduling()
   179  				So(s, ShouldBeNil)
   180  			})
   181  		})
   182  		Convey("EnableConfidentialCompute", func() {
   183  			v := &VM{
   184  				Attributes: config.VM{
   185  					EnableConfidentialCompute: true,
   186  				},
   187  			}
   188  			c := v.getConfidentialInstanceConfig()
   189  			So(c, ShouldResemble, &compute.ConfidentialInstanceConfig{
   190  				EnableConfidentialCompute: true,
   191  			})
   192  			s := v.getScheduling()
   193  			So(s, ShouldResemble, &compute.Scheduling{
   194  				NodeAffinities:    []*compute.SchedulingNodeAffinity{},
   195  				OnHostMaintenance: "TERMINATE",
   196  			})
   197  		})
   198  	})
   199  
   200  	Convey("getDisks", t, func() {
   201  		Convey("zero", func() {
   202  			Convey("nil", func() {
   203  				v := &VM{}
   204  				d := v.getDisks()
   205  				So(d, ShouldHaveLength, 0)
   206  			})
   207  
   208  			Convey("empty", func() {
   209  				v := &VM{
   210  					Attributes: config.VM{
   211  						Disk: []*config.Disk{},
   212  					},
   213  				}
   214  				d := v.getDisks()
   215  				So(d, ShouldHaveLength, 0)
   216  			})
   217  		})
   218  
   219  		Convey("non-zero", func() {
   220  			Convey("empty", func() {
   221  				v := &VM{
   222  					Attributes: config.VM{
   223  						Disk: []*config.Disk{
   224  							{},
   225  						},
   226  					},
   227  				}
   228  				d := v.getDisks()
   229  				So(d, ShouldHaveLength, 1)
   230  				So(d[0].AutoDelete, ShouldBeTrue)
   231  				So(d[0].Boot, ShouldBeTrue)
   232  				So(d[0].InitializeParams.DiskSizeGb, ShouldEqual, 0)
   233  			})
   234  
   235  			Convey("non-empty", func() {
   236  				v := &VM{
   237  					Attributes: config.VM{
   238  						Disk: []*config.Disk{
   239  							{
   240  								Image: "image",
   241  							},
   242  						},
   243  					},
   244  				}
   245  				d := v.getDisks()
   246  				So(d, ShouldHaveLength, 1)
   247  				So(d[0].InitializeParams.SourceImage, ShouldEqual, "image")
   248  			})
   249  
   250  			Convey("multi", func() {
   251  				v := &VM{
   252  					Attributes: config.VM{
   253  						Disk: []*config.Disk{
   254  							{},
   255  							{},
   256  						},
   257  					},
   258  				}
   259  				d := v.getDisks()
   260  				So(d, ShouldHaveLength, 2)
   261  				So(d[0].Boot, ShouldBeTrue)
   262  				So(d[1].Boot, ShouldBeFalse)
   263  			})
   264  
   265  			Convey("scratch", func() {
   266  				v := &VM{
   267  					Attributes: config.VM{
   268  						Disk: []*config.Disk{
   269  							{
   270  								Type: "zones/zone/diskTypes/pd-ssd",
   271  							},
   272  							{
   273  								Type: "zones/zone/diskTypes/local-ssd",
   274  							},
   275  							{
   276  								Type: "zones/zone/diskTypes/pd-standard",
   277  							},
   278  						},
   279  					},
   280  				}
   281  				d := v.getDisks()
   282  				So(d, ShouldHaveLength, 3)
   283  				So(d[0].Type, ShouldEqual, "")
   284  				So(d[1].Type, ShouldEqual, "SCRATCH")
   285  				So(d[2].Type, ShouldEqual, "")
   286  			})
   287  		})
   288  	})
   289  
   290  	Convey("getMetadata", t, func() {
   291  		Convey("nil", func() {
   292  			v := &VM{}
   293  			m := v.getMetadata()
   294  			So(m, ShouldBeNil)
   295  		})
   296  
   297  		Convey("empty", func() {
   298  			v := &VM{
   299  				Attributes: config.VM{
   300  					Metadata: []*config.Metadata{},
   301  				},
   302  			}
   303  			m := v.getMetadata()
   304  			So(m, ShouldBeNil)
   305  		})
   306  
   307  		Convey("non-empty", func() {
   308  			Convey("empty-nil", func() {
   309  				v := &VM{
   310  					Attributes: config.VM{
   311  						Metadata: []*config.Metadata{
   312  							{},
   313  						},
   314  					},
   315  				}
   316  				m := v.getMetadata()
   317  				So(m.Items, ShouldHaveLength, 1)
   318  				So(m.Items[0].Key, ShouldEqual, "")
   319  				So(m.Items[0].Value, ShouldBeNil)
   320  			})
   321  
   322  			Convey("key-nil", func() {
   323  				v := &VM{
   324  					Attributes: config.VM{
   325  						Metadata: []*config.Metadata{
   326  							{
   327  								Metadata: &config.Metadata_FromText{
   328  									FromText: "key",
   329  								},
   330  							},
   331  						},
   332  					},
   333  				}
   334  				m := v.getMetadata()
   335  				So(m.Items, ShouldHaveLength, 1)
   336  				So(m.Items[0].Key, ShouldEqual, "key")
   337  				So(m.Items[0].Value, ShouldBeNil)
   338  			})
   339  
   340  			Convey("key-empty", func() {
   341  				v := &VM{
   342  					Attributes: config.VM{
   343  						Metadata: []*config.Metadata{
   344  							{
   345  								Metadata: &config.Metadata_FromText{
   346  									FromText: "key:",
   347  								},
   348  							},
   349  						},
   350  					},
   351  				}
   352  				m := v.getMetadata()
   353  				So(m.Items, ShouldHaveLength, 1)
   354  				So(m.Items[0].Key, ShouldEqual, "key")
   355  				So(*m.Items[0].Value, ShouldEqual, "")
   356  			})
   357  
   358  			Convey("key-value", func() {
   359  				v := &VM{
   360  					Attributes: config.VM{
   361  						Metadata: []*config.Metadata{
   362  							{
   363  								Metadata: &config.Metadata_FromText{
   364  									FromText: "key:value",
   365  								},
   366  							},
   367  						},
   368  					},
   369  				}
   370  				m := v.getMetadata()
   371  				So(m.Items, ShouldHaveLength, 1)
   372  				So(m.Items[0].Key, ShouldEqual, "key")
   373  				So(*m.Items[0].Value, ShouldEqual, "value")
   374  			})
   375  
   376  			Convey("empty-value", func() {
   377  				v := &VM{
   378  					Attributes: config.VM{
   379  						Metadata: []*config.Metadata{
   380  							{
   381  								Metadata: &config.Metadata_FromText{
   382  									FromText: ":value",
   383  								},
   384  							},
   385  						},
   386  					},
   387  				}
   388  				m := v.getMetadata()
   389  				So(m.Items, ShouldHaveLength, 1)
   390  				So(m.Items[0].Key, ShouldEqual, "")
   391  				So(*m.Items[0].Value, ShouldEqual, "value")
   392  			})
   393  
   394  			Convey("from file", func() {
   395  				v := &VM{
   396  					Attributes: config.VM{
   397  						Metadata: []*config.Metadata{
   398  							{
   399  								Metadata: &config.Metadata_FromFile{
   400  									FromFile: "key:file",
   401  								},
   402  							},
   403  						},
   404  					},
   405  				}
   406  				m := v.getMetadata()
   407  				So(m.Items, ShouldHaveLength, 1)
   408  				So(m.Items[0].Key, ShouldEqual, "")
   409  				So(m.Items[0].Value, ShouldBeNil)
   410  			})
   411  
   412  			Convey("empty with dut", func() {
   413  				v := &VM{
   414  					Attributes: config.VM{
   415  						Metadata: []*config.Metadata{},
   416  					},
   417  					DUT: "dut-1",
   418  				}
   419  				m := v.getMetadata()
   420  				So(m.Items, ShouldHaveLength, 1)
   421  				So(m.Items[0].Key, ShouldEqual, "dut")
   422  				So(*m.Items[0].Value, ShouldEqual, "dut-1")
   423  			})
   424  
   425  			Convey("non-empty with dut", func() {
   426  				v := &VM{
   427  					Attributes: config.VM{
   428  						Metadata: []*config.Metadata{
   429  							{
   430  								Metadata: &config.Metadata_FromText{
   431  									FromText: "key:file",
   432  								},
   433  							},
   434  						},
   435  					},
   436  					DUT: "dut-1",
   437  				}
   438  				m := v.getMetadata()
   439  				So(m.Items, ShouldHaveLength, 2)
   440  				So(m.Items[0].Key, ShouldEqual, "key")
   441  				So(*m.Items[0].Value, ShouldEqual, "file")
   442  				So(m.Items[1].Key, ShouldEqual, "dut")
   443  				So(*m.Items[1].Value, ShouldEqual, "dut-1")
   444  			})
   445  		})
   446  	})
   447  
   448  	Convey("getNetworkInterfaces", t, func() {
   449  		Convey("zero", func() {
   450  			Convey("nil", func() {
   451  				v := &VM{}
   452  				n := v.getNetworkInterfaces()
   453  				So(n, ShouldHaveLength, 0)
   454  			})
   455  
   456  			Convey("empty", func() {
   457  				v := &VM{
   458  					Attributes: config.VM{
   459  						NetworkInterface: []*config.NetworkInterface{},
   460  					},
   461  				}
   462  				n := v.getNetworkInterfaces()
   463  				So(n, ShouldHaveLength, 0)
   464  			})
   465  		})
   466  
   467  		Convey("non-zero", func() {
   468  			Convey("empty", func() {
   469  				v := &VM{
   470  					Attributes: config.VM{
   471  						NetworkInterface: []*config.NetworkInterface{
   472  							{},
   473  						},
   474  					},
   475  				}
   476  				n := v.getNetworkInterfaces()
   477  				So(n, ShouldHaveLength, 1)
   478  				So(n[0].AccessConfigs, ShouldHaveLength, 0)
   479  				So(n[0].Network, ShouldEqual, "")
   480  			})
   481  
   482  			Convey("non-empty", func() {
   483  				Convey("network and subnetwork", func() {
   484  					v := &VM{
   485  						Attributes: config.VM{
   486  							NetworkInterface: []*config.NetworkInterface{
   487  								{
   488  									AccessConfig: []*config.AccessConfig{},
   489  									Network:      "network",
   490  									Subnetwork:   "subnetwork",
   491  								},
   492  							},
   493  						},
   494  					}
   495  					n := v.getNetworkInterfaces()
   496  					So(n, ShouldHaveLength, 1)
   497  					So(n[0].AccessConfigs, ShouldBeNil)
   498  					So(n[0].Network, ShouldEqual, "network")
   499  					So(n[0].Subnetwork, ShouldEqual, "subnetwork")
   500  				})
   501  
   502  				Convey("network", func() {
   503  					v := &VM{
   504  						Attributes: config.VM{
   505  							NetworkInterface: []*config.NetworkInterface{
   506  								{
   507  									AccessConfig: []*config.AccessConfig{},
   508  									Network:      "network",
   509  								},
   510  							},
   511  						},
   512  					}
   513  					n := v.getNetworkInterfaces()
   514  					So(n, ShouldHaveLength, 1)
   515  					So(n[0].AccessConfigs, ShouldBeNil)
   516  					So(n[0].Network, ShouldEqual, "network")
   517  				})
   518  
   519  				Convey("subnetwork", func() {
   520  					v := &VM{
   521  						Attributes: config.VM{
   522  							NetworkInterface: []*config.NetworkInterface{
   523  								{
   524  									AccessConfig: []*config.AccessConfig{},
   525  									Subnetwork:   "subnetwork",
   526  								},
   527  							},
   528  						},
   529  					}
   530  					n := v.getNetworkInterfaces()
   531  					So(n, ShouldHaveLength, 1)
   532  					So(n[0].AccessConfigs, ShouldBeNil)
   533  					So(n[0].Subnetwork, ShouldEqual, "subnetwork")
   534  				})
   535  
   536  				Convey("access configs", func() {
   537  					v := &VM{
   538  						Attributes: config.VM{
   539  							NetworkInterface: []*config.NetworkInterface{
   540  								{
   541  									AccessConfig: []*config.AccessConfig{
   542  										{
   543  											Type: config.AccessConfigType_ONE_TO_ONE_NAT,
   544  										},
   545  									},
   546  								},
   547  							},
   548  						},
   549  					}
   550  					n := v.getNetworkInterfaces()
   551  					So(n, ShouldHaveLength, 1)
   552  					So(n[0].AccessConfigs, ShouldHaveLength, 1)
   553  					So(n[0].AccessConfigs[0].Type, ShouldEqual, "ONE_TO_ONE_NAT")
   554  				})
   555  			})
   556  		})
   557  	})
   558  
   559  	Convey("getServiceAccounts", t, func() {
   560  		Convey("zero", func() {
   561  			Convey("nil", func() {
   562  				v := &VM{}
   563  				s := v.getServiceAccounts()
   564  				So(s, ShouldHaveLength, 0)
   565  			})
   566  
   567  			Convey("empty", func() {
   568  				v := &VM{
   569  					Attributes: config.VM{
   570  						ServiceAccount: []*config.ServiceAccount{},
   571  					},
   572  				}
   573  				s := v.getServiceAccounts()
   574  				So(s, ShouldHaveLength, 0)
   575  			})
   576  		})
   577  
   578  		Convey("non-zero", func() {
   579  			Convey("empty", func() {
   580  				v := &VM{
   581  					Attributes: config.VM{
   582  						ServiceAccount: []*config.ServiceAccount{
   583  							{},
   584  						},
   585  					},
   586  				}
   587  				s := v.getServiceAccounts()
   588  				So(s, ShouldHaveLength, 1)
   589  				So(s[0].Email, ShouldEqual, "")
   590  				So(s[0].Scopes, ShouldHaveLength, 0)
   591  			})
   592  
   593  			Convey("non-empty", func() {
   594  				Convey("email", func() {
   595  					v := &VM{
   596  						Attributes: config.VM{
   597  							ServiceAccount: []*config.ServiceAccount{
   598  								{
   599  									Email: "email",
   600  									Scope: []string{},
   601  								},
   602  							},
   603  						},
   604  					}
   605  					s := v.getServiceAccounts()
   606  					So(s, ShouldHaveLength, 1)
   607  					So(s[0].Email, ShouldEqual, "email")
   608  					So(s[0].Scopes, ShouldHaveLength, 0)
   609  				})
   610  
   611  				Convey("scopes", func() {
   612  					v := &VM{
   613  						Attributes: config.VM{
   614  							ServiceAccount: []*config.ServiceAccount{
   615  								{
   616  									Scope: []string{
   617  										"scope",
   618  									},
   619  								},
   620  							},
   621  						},
   622  					}
   623  					s := v.getServiceAccounts()
   624  					So(s, ShouldHaveLength, 1)
   625  					So(s[0].Email, ShouldEqual, "")
   626  					So(s[0].Scopes, ShouldHaveLength, 1)
   627  					So(s[0].Scopes[0], ShouldEqual, "scope")
   628  				})
   629  			})
   630  		})
   631  
   632  	})
   633  
   634  	Convey("getScheduling", t, func() {
   635  		Convey("zero", func() {
   636  			Convey("nil", func() {
   637  				v := &VM{}
   638  				s := v.getScheduling()
   639  				So(s, ShouldBeNil)
   640  			})
   641  
   642  			Convey("empty", func() {
   643  				v := &VM{
   644  					Attributes: config.VM{
   645  						Scheduling: &config.Scheduling{
   646  							NodeAffinity: []*config.NodeAffinity{},
   647  						},
   648  					},
   649  				}
   650  				s := v.getScheduling()
   651  				So(s, ShouldBeNil)
   652  			})
   653  			Convey("empty & Confidential", func() {
   654  				v := &VM{
   655  					Attributes: config.VM{
   656  						EnableConfidentialCompute: true,
   657  					},
   658  				}
   659  				s := v.getScheduling()
   660  				So(s, ShouldResemble, &compute.Scheduling{
   661  					NodeAffinities:    []*compute.SchedulingNodeAffinity{},
   662  					OnHostMaintenance: "TERMINATE",
   663  				})
   664  			})
   665  			Convey("empty & Terminatable", func() {
   666  				v := &VM{
   667  					Attributes: config.VM{
   668  						TerminateOnMaintenance: true,
   669  					},
   670  				}
   671  				s := v.getScheduling()
   672  				So(s, ShouldResemble, &compute.Scheduling{
   673  					NodeAffinities:    []*compute.SchedulingNodeAffinity{},
   674  					OnHostMaintenance: "TERMINATE",
   675  				})
   676  			})
   677  			Convey("empty & Confidential & Terminatable", func() {
   678  				v := &VM{
   679  					Attributes: config.VM{
   680  						EnableConfidentialCompute: true,
   681  						TerminateOnMaintenance:    true,
   682  					},
   683  				}
   684  				s := v.getScheduling()
   685  				So(s, ShouldResemble, &compute.Scheduling{
   686  					NodeAffinities:    []*compute.SchedulingNodeAffinity{},
   687  					OnHostMaintenance: "TERMINATE",
   688  				})
   689  			})
   690  		})
   691  
   692  		Convey("non-zero", func() {
   693  			Convey("empty", func() {
   694  				v := &VM{
   695  					Attributes: config.VM{
   696  						Scheduling: &config.Scheduling{
   697  							NodeAffinity: []*config.NodeAffinity{
   698  								{},
   699  							},
   700  						},
   701  					},
   702  				}
   703  				s := v.getScheduling()
   704  				So(s, ShouldNotBeNil)
   705  				So(s.NodeAffinities, ShouldHaveLength, 1)
   706  				So(s.NodeAffinities[0].Key, ShouldEqual, "")
   707  				So(s.NodeAffinities[0].Operator, ShouldEqual, "OPERATOR_UNSPECIFIED")
   708  				So(s.NodeAffinities[0].Values, ShouldHaveLength, 0)
   709  			})
   710  
   711  			Convey("non-empty", func() {
   712  				Convey("key", func() {
   713  					v := &VM{
   714  						Attributes: config.VM{
   715  							Scheduling: &config.Scheduling{
   716  								NodeAffinity: []*config.NodeAffinity{
   717  									{
   718  										Key: "node-affinity-key",
   719  									},
   720  								},
   721  							},
   722  						},
   723  					}
   724  					s := v.getScheduling()
   725  					So(s.NodeAffinities, ShouldHaveLength, 1)
   726  					So(s.NodeAffinities[0].Key, ShouldEqual, "node-affinity-key")
   727  				})
   728  				Convey("operator", func() {
   729  					v := &VM{
   730  						Attributes: config.VM{
   731  							Scheduling: &config.Scheduling{
   732  								NodeAffinity: []*config.NodeAffinity{
   733  									{
   734  										Operator: config.NodeAffinityOperator_IN,
   735  									},
   736  								},
   737  							},
   738  						},
   739  					}
   740  					s := v.getScheduling()
   741  					So(s.NodeAffinities, ShouldHaveLength, 1)
   742  					So(s.NodeAffinities[0].Operator, ShouldEqual, "IN")
   743  				})
   744  				Convey("values", func() {
   745  					v := &VM{
   746  						Attributes: config.VM{
   747  							Scheduling: &config.Scheduling{
   748  								NodeAffinity: []*config.NodeAffinity{
   749  									{
   750  										Values: []string{"node-affinity-value"},
   751  									},
   752  								},
   753  							},
   754  						},
   755  					}
   756  					s := v.getScheduling()
   757  					So(s.NodeAffinities, ShouldHaveLength, 1)
   758  					So(s.NodeAffinities[0].Values, ShouldHaveLength, 1)
   759  					So(s.NodeAffinities[0].Values[0], ShouldEqual, "node-affinity-value")
   760  				})
   761  				Convey("not-empty other cases", func() {
   762  					inScheduling := &config.Scheduling{
   763  						NodeAffinity: []*config.NodeAffinity{{Key: "node-affinity-key"}},
   764  					}
   765  					Convey("Confidential", func() {
   766  						v := &VM{
   767  							Attributes: config.VM{
   768  								Scheduling:                inScheduling,
   769  								EnableConfidentialCompute: true,
   770  							},
   771  						}
   772  						s := v.getScheduling()
   773  						So(s.NodeAffinities, ShouldHaveLength, 1)
   774  						So(s.NodeAffinities[0].Key, ShouldEqual, "node-affinity-key")
   775  						So(s.OnHostMaintenance, ShouldEqual, "TERMINATE")
   776  					})
   777  					Convey("Terminatable", func() {
   778  						v := &VM{
   779  							Attributes: config.VM{
   780  								Scheduling:             inScheduling,
   781  								TerminateOnMaintenance: true,
   782  							},
   783  						}
   784  						s := v.getScheduling()
   785  						So(s.NodeAffinities, ShouldHaveLength, 1)
   786  						So(s.NodeAffinities[0].Key, ShouldEqual, "node-affinity-key")
   787  						So(s.OnHostMaintenance, ShouldEqual, "TERMINATE")
   788  					})
   789  					Convey("Confidential & Terminatable", func() {
   790  						v := &VM{
   791  							Attributes: config.VM{
   792  								Scheduling:                inScheduling,
   793  								EnableConfidentialCompute: true,
   794  								TerminateOnMaintenance:    true,
   795  							},
   796  						}
   797  						s := v.getScheduling()
   798  						So(s.NodeAffinities, ShouldHaveLength, 1)
   799  						So(s.NodeAffinities[0].Key, ShouldEqual, "node-affinity-key")
   800  						So(s.OnHostMaintenance, ShouldEqual, "TERMINATE")
   801  					})
   802  				})
   803  			})
   804  		})
   805  	})
   806  
   807  	Convey("getShieldedInstanceConfig", t, func() {
   808  		Convey("zero", func() {
   809  			Convey("empty", func() {
   810  				v := &VM{}
   811  				c := v.getShieldedInstanceConfig()
   812  				So(c, ShouldBeNil)
   813  			})
   814  		})
   815  		Convey("non-zero", func() {
   816  			Convey("disableIntegrityMonitoring", func() {
   817  				v := &VM{
   818  					Attributes: config.VM{
   819  						DisableIntegrityMonitoring: true,
   820  					},
   821  				}
   822  				c := v.getShieldedInstanceConfig()
   823  				So(c, ShouldResemble, &compute.ShieldedInstanceConfig{
   824  					EnableIntegrityMonitoring: false,
   825  					EnableSecureBoot:          false,
   826  					EnableVtpm:                true,
   827  				})
   828  			})
   829  			Convey("enableSecureBoot", func() {
   830  				v := &VM{
   831  					Attributes: config.VM{
   832  						EnableSecureBoot: true,
   833  					},
   834  				}
   835  				c := v.getShieldedInstanceConfig()
   836  				So(c, ShouldResemble, &compute.ShieldedInstanceConfig{
   837  					EnableIntegrityMonitoring: true,
   838  					EnableSecureBoot:          true,
   839  					EnableVtpm:                true,
   840  				})
   841  			})
   842  			Convey("disablevTPM", func() {
   843  				v := &VM{
   844  					Attributes: config.VM{
   845  						DisableVtpm: true,
   846  					},
   847  				}
   848  				c := v.getShieldedInstanceConfig()
   849  				So(c, ShouldResemble, &compute.ShieldedInstanceConfig{
   850  					EnableIntegrityMonitoring: true,
   851  					EnableSecureBoot:          false,
   852  					EnableVtpm:                false,
   853  				})
   854  			})
   855  		})
   856  	})
   857  
   858  	Convey("getTags", t, func() {
   859  		Convey("zero", func() {
   860  			Convey("nil", func() {
   861  				v := &VM{}
   862  				t := v.getTags()
   863  				So(t, ShouldBeNil)
   864  			})
   865  
   866  			Convey("empty", func() {
   867  				v := &VM{
   868  					Attributes: config.VM{
   869  						Tag: []string{},
   870  					},
   871  				}
   872  				t := v.getTags()
   873  				So(t, ShouldBeNil)
   874  			})
   875  		})
   876  
   877  		Convey("non-zero", func() {
   878  			v := &VM{
   879  				Attributes: config.VM{
   880  					Tag: []string{
   881  						"tag",
   882  					},
   883  				},
   884  			}
   885  			t := v.getTags()
   886  			So(t.Items, ShouldHaveLength, 1)
   887  			So(t.Items[0], ShouldEqual, "tag")
   888  		})
   889  	})
   890  
   891  	Convey("GetLabels", t, func() {
   892  		Convey("zero", func() {
   893  			Convey("nil", func() {
   894  				v := &VM{}
   895  				l := v.getLabels()
   896  				So(l, ShouldBeEmpty)
   897  			})
   898  
   899  			Convey("empty", func() {
   900  				v := &VM{
   901  					Attributes: config.VM{
   902  						Label: map[string]string{},
   903  					},
   904  				}
   905  				l := v.getLabels()
   906  				So(l, ShouldBeEmpty)
   907  			})
   908  		})
   909  
   910  		Convey("non-zero", func() {
   911  			v := &VM{
   912  				Attributes: config.VM{
   913  					Label: map[string]string{"key1": "value1"},
   914  				},
   915  			}
   916  			l := v.getLabels()
   917  			So(l, ShouldHaveLength, 1)
   918  			So(l, ShouldContainKey, "key1")
   919  			So(l["key1"], ShouldEqual, "value1")
   920  		})
   921  	})
   922  
   923  	Convey("GetInstance", t, func() {
   924  		Convey("empty", func() {
   925  			v := &VM{}
   926  			i := v.GetInstance().Stable
   927  			So(i.Disks, ShouldHaveLength, 0)
   928  			So(i.MachineType, ShouldEqual, "")
   929  			So(i.Metadata, ShouldBeNil)
   930  			So(i.MinCpuPlatform, ShouldEqual, "")
   931  			So(i.NetworkInterfaces, ShouldHaveLength, 0)
   932  			So(i.Scheduling, ShouldBeNil)
   933  			So(i.ServiceAccounts, ShouldBeNil)
   934  			So(i.ShieldedInstanceConfig, ShouldBeNil)
   935  			So(i.Tags, ShouldBeNil)
   936  			So(i.Labels, ShouldBeNil)
   937  			So(i.ForceSendFields, ShouldBeNil)
   938  		})
   939  
   940  		Convey("non-empty", func() {
   941  			v := &VM{
   942  				Attributes: config.VM{
   943  					Disk: []*config.Disk{
   944  						{
   945  							Image: "image",
   946  							Size:  100,
   947  						},
   948  					},
   949  					EnableSecureBoot: true,
   950  					MachineType:      "type",
   951  					MinCpuPlatform:   "plat",
   952  					NetworkInterface: []*config.NetworkInterface{
   953  						{
   954  							AccessConfig: []*config.AccessConfig{
   955  								{},
   956  							},
   957  							Network: "network",
   958  						},
   959  					},
   960  					Scheduling: &config.Scheduling{
   961  						NodeAffinity: []*config.NodeAffinity{
   962  							{},
   963  						},
   964  					},
   965  					ForceSendFields: []string{
   966  						"a",
   967  						"b",
   968  						"c",
   969  					},
   970  					NullFields: []string{
   971  						"e",
   972  					},
   973  				},
   974  			}
   975  			i := v.GetInstance().Stable
   976  			So(i.Disks, ShouldHaveLength, 1)
   977  			So(i.MachineType, ShouldEqual, "type")
   978  			So(i.Metadata, ShouldBeNil)
   979  			So(i.MinCpuPlatform, ShouldEqual, "plat")
   980  			So(i.NetworkInterfaces, ShouldHaveLength, 1)
   981  			So(i.ServiceAccounts, ShouldBeNil)
   982  			So(i.Scheduling, ShouldNotBeNil)
   983  			So(i.Scheduling.NodeAffinities, ShouldHaveLength, 1)
   984  			So(i.ShieldedInstanceConfig, ShouldNotBeNil)
   985  			So(i.Tags, ShouldBeNil)
   986  			So(i.Labels, ShouldBeNil)
   987  			So(i.ForceSendFields, ShouldNotBeNil)
   988  			So(i.NullFields, ShouldNotBeNil)
   989  		})
   990  	})
   991  }
   992  
   993  // TestGetInstanceReturnsAlpha tests the conversion of various input VMs to the
   994  // alpha GCP API.
   995  func TestGetInstanceReturnsAlpha(t *testing.T) {
   996  	t.Parallel()
   997  
   998  	cases := []struct {
   999  		name   string
  1000  		input  *VM
  1001  		output *computealpha.Instance
  1002  	}{
  1003  		{
  1004  			name: "only alpha",
  1005  			input: &VM{
  1006  				Attributes: config.VM{
  1007  					GcpChannel: config.GCPChannel_GCP_CHANNEL_ALPHA,
  1008  				},
  1009  			},
  1010  			output: &computealpha.Instance{},
  1011  		},
  1012  		{
  1013  			name: "hostname",
  1014  			input: &VM{
  1015  				Hostname: "awesome-host",
  1016  				Attributes: config.VM{
  1017  					GcpChannel: config.GCPChannel_GCP_CHANNEL_ALPHA,
  1018  				},
  1019  			},
  1020  			output: &computealpha.Instance{
  1021  				Name: "awesome-host",
  1022  			},
  1023  		},
  1024  	}
  1025  
  1026  	for _, tt := range cases {
  1027  		tt := tt
  1028  		t.Run(tt.name, func(t *testing.T) {
  1029  			t.Parallel()
  1030  			expected := tt.output
  1031  			actual := tt.input.GetInstance().Alpha
  1032  
  1033  			if diff := cmp.Diff(expected, actual, protocmp.Transform()); diff != "" {
  1034  				t.Errorf("unexpected diff (-want +got): %s", diff)
  1035  			}
  1036  		})
  1037  	}
  1038  }
  1039  
  1040  // LegacyConfigV1 is the type of an old Config struct.
  1041  // This type is used to ensure that the current (as of 2023-09-25) struct and
  1042  // the former version are compatible within datastore, i.e. records can be written
  1043  // or read with the old or new schema.
  1044  type LegacyConfigV1 struct {
  1045  	_extra datastore.PropertyMap `gae:"-,extra"`
  1046  	_kind  string                `gae:"$kind,Config"`
  1047  	ID     string                `gae:"$id"`
  1048  	Config config.Config         `gae:"binary_config,noindex"`
  1049  }
  1050  
  1051  // TestReadLegacyConfigV1AsConfig tests writing a record to in-memory datastore as a
  1052  // LegacyConfigV1 and reading it out as a Config.
  1053  //
  1054  // This test is more valuable than just being a leaf test.
  1055  // Basically, the old Config field, which is a config.Config not a *config.Config,
  1056  // copies mutex fields inside a proto. We are getting rid of this by changing the type
  1057  // of the field to be a pointer and changing some of the annotations.
  1058  //
  1059  // This kind of schema change should be transparent and harmless, but I don't know that for sure.
  1060  //
  1061  // This test's purpose is to catch problems with updating datastore schemas in go projects, so
  1062  // if we encounter a problem in prod, we will go back and update this test in such a way that it
  1063  // would have caught the issue.
  1064  func TestReadLegacyConfigV1AsConfig(t *testing.T) {
  1065  	t.Parallel()
  1066  	ctx := memory.Use(context.Background())
  1067  	input := &LegacyConfigV1{
  1068  		ID: "a",
  1069  		Config: config.Config{
  1070  			Swarming: "ed18ba13-bfe7-46bc-ae77-77ec70f2c22c",
  1071  		},
  1072  	}
  1073  	output := &Config{
  1074  		ID: "a",
  1075  		Config: &config.Config{
  1076  			Swarming: "ed18ba13-bfe7-46bc-ae77-77ec70f2c22c",
  1077  		},
  1078  	}
  1079  
  1080  	if err := datastore.Put(ctx, input); err != nil {
  1081  		t.Errorf("error when inserting record to datastore: %s", err)
  1082  	}
  1083  
  1084  	expected := output
  1085  	actual := &Config{
  1086  		ID: "a",
  1087  	}
  1088  	if err := datastore.Get(ctx, actual); err != nil {
  1089  		t.Errorf("error when retrieving record from datastore: %s", err)
  1090  	}
  1091  
  1092  	if diff := cmp.Diff(expected, actual, protocmp.Transform(), cmpopts.IgnoreUnexported(Config{})); diff != "" {
  1093  		t.Errorf("unexpected diff (-want +got): %s", diff)
  1094  	}
  1095  }