github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/gce_image_publish/publish/publish_test.go (about)

     1  //  Copyright 2017 Google Inc. All Rights Reserved.
     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 publish
    16  
    17  import (
    18  	"context"
    19  	"reflect"
    20  	"testing"
    21  	"time"
    22  
    23  	daisy "github.com/GoogleCloudPlatform/compute-daisy"
    24  	"github.com/kylelemons/godebug/pretty"
    25  	computeAlpha "google.golang.org/api/compute/v0.alpha"
    26  	"google.golang.org/api/compute/v1"
    27  )
    28  
    29  func TestPublishImage(t *testing.T) {
    30  	now := time.Now()
    31  	fakeInitialState := &computeAlpha.InitialStateConfig{
    32  		Dbs: []*computeAlpha.FileContentBuffer{
    33  			{
    34  				Content:  "abc",
    35  				FileType: "BIN",
    36  			},
    37  		},
    38  		Dbxs: []*computeAlpha.FileContentBuffer{
    39  			{
    40  				Content:  "abc",
    41  				FileType: "X509",
    42  			},
    43  		},
    44  		NullFields: []string{"Keks", "Pk"},
    45  	}
    46  
    47  	tests := []struct {
    48  		desc    string
    49  		p       *Publish
    50  		img     *Image
    51  		pubImgs []*computeAlpha.Image
    52  		skipDup bool
    53  		replace bool
    54  		noRoot  bool
    55  		wantCI  *daisy.CreateImages
    56  		wantDI  *daisy.DeprecateImages
    57  		wantErr bool
    58  	}{
    59  		{
    60  			desc: "normal case",
    61  			p:    &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
    62  			img: &Image{
    63  				Prefix:          "foo",
    64  				Family:          "foo-family",
    65  				GuestOsFeatures: []string{"foo-feature", "bar-feature"},
    66  				ObsoleteDate:    &now,
    67  				RolloutPolicy: &computeAlpha.RolloutPolicy{
    68  					DefaultRolloutTime: now.Format(time.RFC3339),
    69  				},
    70  			},
    71  			pubImgs: []*computeAlpha.Image{
    72  				{Name: "bar-2", Family: "bar-family"},
    73  				{Name: "foo-2", Family: "foo-family"},
    74  				{
    75  					Name:   "foo-1",
    76  					Family: "foo-family",
    77  					Deprecated: &computeAlpha.DeprecationStatus{
    78  						State: "DEPRECATED",
    79  						StateOverride: &computeAlpha.RolloutPolicy{
    80  							DefaultRolloutTime: now.Format(time.RFC3339),
    81  						},
    82  					},
    83  				},
    84  				{
    85  					Name:   "bar-1",
    86  					Family: "bar-family",
    87  					Deprecated: &computeAlpha.DeprecationStatus{
    88  						State: "DEPRECATED",
    89  						StateOverride: &computeAlpha.RolloutPolicy{
    90  							DefaultRolloutTime: now.Format(time.RFC3339),
    91  						},
    92  					},
    93  				},
    94  			},
    95  			wantCI: &daisy.CreateImages{
    96  				ImagesAlpha: []*daisy.ImageAlpha{
    97  					{
    98  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"}},
    99  						Image: computeAlpha.Image{
   100  							Name: "foo-3", Family: "foo-family",
   101  							SourceImage: "projects/bar-project/global/images/foo-3",
   102  							RolloutOverride: &computeAlpha.RolloutPolicy{
   103  								DefaultRolloutTime: now.Format(time.RFC3339),
   104  							},
   105  							Deprecated: &computeAlpha.DeprecationStatus{
   106  								State:    "ACTIVE",
   107  								Obsolete: now.Format(time.RFC3339),
   108  							},
   109  						},
   110  						GuestOsFeatures: []string{"foo-feature", "bar-feature"},
   111  					},
   112  				},
   113  			},
   114  			wantDI: &daisy.DeprecateImages{
   115  				{
   116  					Image:   "foo-2",
   117  					Project: "foo-project",
   118  					DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   119  						State:       "DEPRECATED",
   120  						Replacement: "https://www.googleapis.com/compute/v1/projects/foo-project/global/images/foo-3",
   121  						StateOverride: &computeAlpha.RolloutPolicy{
   122  							DefaultRolloutTime: now.Format(time.RFC3339),
   123  						},
   124  					},
   125  				},
   126  			},
   127  		},
   128  		{
   129  			desc: "multiple images to deprecate",
   130  			p:    &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   131  			img: &Image{
   132  				Prefix: "foo",
   133  				Family: "foo-family",
   134  				RolloutPolicy: &computeAlpha.RolloutPolicy{
   135  					DefaultRolloutTime: now.Format(time.RFC3339),
   136  				},
   137  			},
   138  			pubImgs: []*computeAlpha.Image{
   139  				{Name: "bar-2", Family: "bar-family"},
   140  				{Name: "foo-2", Family: "foo-family"},
   141  				{Name: "foo-1", Family: "foo-family"},
   142  				{Name: "bar-1", Family: "bar-family"},
   143  			},
   144  			wantCI: &daisy.CreateImages{
   145  				ImagesAlpha: []*daisy.ImageAlpha{
   146  					{
   147  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"}},
   148  						Image: computeAlpha.Image{
   149  							Name:        "foo-3",
   150  							Family:      "foo-family",
   151  							SourceImage: "projects/bar-project/global/images/foo-3",
   152  							RolloutOverride: &computeAlpha.RolloutPolicy{
   153  								DefaultRolloutTime: now.Format(time.RFC3339),
   154  							},
   155  						},
   156  					},
   157  				},
   158  			},
   159  			wantDI: &daisy.DeprecateImages{
   160  				{
   161  					Image:   "foo-2",
   162  					Project: "foo-project",
   163  					DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   164  						State:       "DEPRECATED",
   165  						Replacement: "https://www.googleapis.com/compute/v1/projects/foo-project/global/images/foo-3",
   166  						StateOverride: &computeAlpha.RolloutPolicy{
   167  							DefaultRolloutTime: now.Format(time.RFC3339),
   168  						},
   169  					},
   170  				},
   171  				{
   172  					Image:   "foo-1",
   173  					Project: "foo-project",
   174  					DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   175  						State:       "DEPRECATED",
   176  						Replacement: "https://www.googleapis.com/compute/v1/projects/foo-project/global/images/foo-3",
   177  						StateOverride: &computeAlpha.RolloutPolicy{
   178  							DefaultRolloutTime: now.Format(time.RFC3339),
   179  						},
   180  					},
   181  				},
   182  			},
   183  		},
   184  		{
   185  			desc:    "GCSPath case",
   186  			p:       &Publish{SourceGCSPath: "gs://bar-project-path", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   187  			img:     &Image{Prefix: "foo", Family: "foo-family"},
   188  			pubImgs: []*computeAlpha.Image{},
   189  			wantCI: &daisy.CreateImages{
   190  				ImagesAlpha: []*daisy.ImageAlpha{
   191  					{
   192  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"}},
   193  						Image: computeAlpha.Image{
   194  							Name:    "foo-3",
   195  							Family:  "foo-family",
   196  							RawDisk: &computeAlpha.ImageRawDisk{Source: "gs://bar-project-path/foo-3/root.tar.gz"},
   197  						},
   198  					},
   199  				},
   200  			},
   201  		},
   202  		{
   203  			desc:    "GCSPath with noRoot case",
   204  			p:       &Publish{SourceGCSPath: "gs://bar-project-path", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   205  			img:     &Image{Prefix: "foo", Family: "foo-family"},
   206  			pubImgs: []*computeAlpha.Image{},
   207  			noRoot:  true,
   208  			wantCI: &daisy.CreateImages{
   209  				ImagesAlpha: []*daisy.ImageAlpha{
   210  					{
   211  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"}},
   212  						Image: computeAlpha.Image{
   213  							Name:    "foo-3",
   214  							Family:  "foo-family",
   215  							RawDisk: &computeAlpha.ImageRawDisk{Source: "gs://bar-project-path/foo-3.tar.gz"},
   216  						},
   217  					},
   218  				},
   219  			},
   220  		},
   221  		{
   222  			desc:    "both SourceGCSPath and SourceProject set",
   223  			p:       &Publish{SourceGCSPath: "gs://bar-project-path", SourceProject: "bar-project"},
   224  			img:     &Image{},
   225  			wantErr: true,
   226  		},
   227  		{
   228  			desc:    "neither SourceGCSPath and SourceProject set",
   229  			p:       &Publish{},
   230  			img:     &Image{},
   231  			wantErr: true,
   232  		},
   233  		{
   234  			desc:    "image already exists",
   235  			p:       &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   236  			img:     &Image{Prefix: "foo", Family: "foo-family", GuestOsFeatures: []string{"foo-feature"}},
   237  			pubImgs: []*computeAlpha.Image{{Name: "foo-3", Family: "foo-family"}},
   238  			wantErr: true,
   239  		},
   240  		{
   241  			desc: "image already exists, skipDup set",
   242  			p:    &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   243  			img: &Image{
   244  				Prefix:          "foo",
   245  				Family:          "foo-family",
   246  				GuestOsFeatures: []string{"foo-feature"},
   247  			},
   248  			pubImgs: []*computeAlpha.Image{
   249  				{Name: "foo-3", Family: "bar-family"},
   250  				{Name: "foo-2", Family: "foo-family"},
   251  			},
   252  			skipDup: true,
   253  			wantDI: &daisy.DeprecateImages{
   254  				{
   255  					Image: "foo-2", Project: "foo-project",
   256  					DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   257  						State:       "DEPRECATED",
   258  						Replacement: "https://www.googleapis.com/compute/v1/projects/foo-project/global/images/foo-3",
   259  					},
   260  				},
   261  			},
   262  		},
   263  		{
   264  			desc: "image already exists, replace set",
   265  			p:    &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   266  			img: &Image{
   267  				Prefix: "foo",
   268  				Family: "foo-family",
   269  				RolloutPolicy: &computeAlpha.RolloutPolicy{
   270  					DefaultRolloutTime: now.Format(time.RFC3339),
   271  				},
   272  			},
   273  			pubImgs: []*computeAlpha.Image{
   274  				{Name: "foo-3", Family: "bar-family"},
   275  				{Name: "foo-2", Family: "foo-family"},
   276  			},
   277  			replace: true,
   278  			wantCI: &daisy.CreateImages{
   279  				ImagesAlpha: []*daisy.ImageAlpha{
   280  					{
   281  						ImageBase: daisy.ImageBase{
   282  							OverWrite: true,
   283  							Resource:  daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"},
   284  						},
   285  						Image: computeAlpha.Image{
   286  							Name:        "foo-3",
   287  							Family:      "foo-family",
   288  							SourceImage: "projects/bar-project/global/images/foo-3",
   289  							RolloutOverride: &computeAlpha.RolloutPolicy{
   290  								DefaultRolloutTime: now.Format(time.RFC3339),
   291  							},
   292  						},
   293  					},
   294  				},
   295  			},
   296  			wantDI: &daisy.DeprecateImages{
   297  				{
   298  					Image:   "foo-2",
   299  					Project: "foo-project",
   300  					DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   301  						State:       "DEPRECATED",
   302  						Replacement: "https://www.googleapis.com/compute/v1/projects/foo-project/global/images/foo-3",
   303  						StateOverride: &computeAlpha.RolloutPolicy{
   304  							DefaultRolloutTime: now.Format(time.RFC3339),
   305  						},
   306  					},
   307  				},
   308  			},
   309  		},
   310  		{
   311  			desc: "new image from src, without version",
   312  			p:    &Publish{SourceProject: "bar-project", PublishProject: "foo-project"},
   313  			img:  &Image{Prefix: "foo-x", Family: "foo-family", GuestOsFeatures: []string{"foo-feature", "bar-feature"}},
   314  			pubImgs: []*computeAlpha.Image{
   315  				{Name: "bar-x", Family: "bar-family"},
   316  			},
   317  			wantCI: &daisy.CreateImages{
   318  				ImagesAlpha: []*daisy.ImageAlpha{
   319  					{
   320  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-x"}},
   321  						Image: computeAlpha.Image{
   322  							Name:        "foo-x",
   323  							Family:      "foo-family",
   324  							SourceImage: "projects/bar-project/global/images/foo-x",
   325  						},
   326  						GuestOsFeatures: []string{"foo-feature", "bar-feature"}},
   327  				},
   328  			},
   329  		},
   330  		{
   331  			desc: "no image family, don't deprecate",
   332  			p:    &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   333  			img:  &Image{Prefix: "foo", Family: "foo-family"},
   334  			pubImgs: []*computeAlpha.Image{
   335  				{Name: "foo-2", Family: ""},
   336  				{Name: "foo-1", Family: "", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   337  			},
   338  			wantCI: &daisy.CreateImages{
   339  				ImagesAlpha: []*daisy.ImageAlpha{
   340  					{
   341  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"}},
   342  						Image: computeAlpha.Image{
   343  							Name:        "foo-3",
   344  							Family:      "foo-family",
   345  							SourceImage: "projects/bar-project/global/images/foo-3",
   346  						},
   347  					},
   348  				},
   349  			},
   350  		},
   351  		{
   352  			desc:    "ignore license validation if forbidden",
   353  			p:       &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   354  			img:     &Image{Prefix: "foo", Family: "foo-family", GuestOsFeatures: []string{"foo-feature"}, IgnoreLicenseValidationIfForbidden: true},
   355  			pubImgs: []*computeAlpha.Image{},
   356  			wantCI: &daisy.CreateImages{
   357  				ImagesAlpha: []*daisy.ImageAlpha{
   358  					{
   359  						ImageBase: daisy.ImageBase{
   360  							Resource:                           daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"},
   361  							IgnoreLicenseValidationIfForbidden: true,
   362  						},
   363  						Image: computeAlpha.Image{
   364  							Name:        "foo-3",
   365  							Family:      "foo-family",
   366  							SourceImage: "projects/bar-project/global/images/foo-3",
   367  						},
   368  						GuestOsFeatures: []string{"foo-feature"},
   369  					},
   370  				},
   371  			},
   372  		},
   373  		{
   374  			desc:    "don't ignore license validation if forbidden",
   375  			p:       &Publish{SourceProject: "bar-project", PublishProject: "foo-project", sourceVersion: "3", publishVersion: "3"},
   376  			img:     &Image{Prefix: "foo", Family: "foo-family", GuestOsFeatures: []string{"foo-feature"}, IgnoreLicenseValidationIfForbidden: false},
   377  			pubImgs: []*computeAlpha.Image{},
   378  			wantCI: &daisy.CreateImages{
   379  				ImagesAlpha: []*daisy.ImageAlpha{
   380  					{
   381  						ImageBase: daisy.ImageBase{
   382  							Resource:                           daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-3"},
   383  							IgnoreLicenseValidationIfForbidden: false,
   384  						},
   385  						Image: computeAlpha.Image{
   386  							Name:        "foo-3",
   387  							Family:      "foo-family",
   388  							SourceImage: "projects/bar-project/global/images/foo-3",
   389  						},
   390  						GuestOsFeatures: []string{"foo-feature"},
   391  					},
   392  				},
   393  			},
   394  		},
   395  		{
   396  			desc:    "new image from src, with ShieldedInstanceInitialState",
   397  			p:       &Publish{SourceProject: "bar-project", PublishProject: "foo-project"},
   398  			img:     &Image{Prefix: "foo-x", Family: "foo-family", ShieldedInstanceInitialState: fakeInitialState},
   399  			pubImgs: []*computeAlpha.Image{},
   400  			wantCI: &daisy.CreateImages{
   401  				ImagesAlpha: []*daisy.ImageAlpha{
   402  					{
   403  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "foo-x"}},
   404  						Image: computeAlpha.Image{
   405  							Name:                         "foo-x",
   406  							Family:                       "foo-family",
   407  							SourceImage:                  "projects/bar-project/global/images/foo-x",
   408  							ShieldedInstanceInitialState: fakeInitialState,
   409  						},
   410  					},
   411  				},
   412  			},
   413  		},
   414  	}
   415  
   416  	for _, tt := range tests {
   417  		dr, di, _, err := publishImage(tt.p, tt.img, tt.pubImgs, tt.skipDup, tt.replace, tt.noRoot)
   418  		if tt.wantErr && err != nil {
   419  			continue
   420  		}
   421  		if !tt.wantErr && err != nil {
   422  			t.Errorf("%s: error from publishImage(): %v", tt.desc, err)
   423  			continue
   424  		} else if tt.wantErr && err == nil {
   425  			t.Errorf("%s: did not get expected error from publishImage()", tt.desc)
   426  		}
   427  
   428  		if diff := pretty.Compare(dr, tt.wantCI); diff != "" {
   429  			t.Errorf("%s: returned CreateImages does not match expectation: (-got +want)\n%s", tt.desc, diff)
   430  		}
   431  		if diff := pretty.Compare(di, tt.wantDI); diff != "" {
   432  			t.Errorf("%s: returned DeprecateImages does not match expectation: (-got +want)\n%s", tt.desc, diff)
   433  		}
   434  	}
   435  }
   436  
   437  func TestRollbackImage(t *testing.T) {
   438  	tests := []struct {
   439  		desc    string
   440  		p       *Publish
   441  		img     *Image
   442  		pubImgs []*computeAlpha.Image
   443  		wantDR  *daisy.DeleteResources
   444  		wantDI  *daisy.DeprecateImages
   445  	}{
   446  		{
   447  			"normal case",
   448  			&Publish{PublishProject: "foo-project", publishVersion: "3"},
   449  			&Image{Prefix: "foo", Family: "foo-family"},
   450  			[]*computeAlpha.Image{
   451  				{Name: "bar-3", Family: "bar-family"},
   452  				{Name: "foo-3", Family: "foo-family"},
   453  				{Name: "bar-2", Family: "bar-family", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   454  				{Name: "foo-2", Family: "foo-family", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   455  				{Name: "foo-1", Family: "foo-family", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   456  				{Name: "bar-1", Family: "bar-family", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   457  			},
   458  			&daisy.DeleteResources{Images: []string{"projects/foo-project/global/images/foo-3"}},
   459  			&daisy.DeprecateImages{{Image: "foo-2", Project: "foo-project", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "ACTIVE"}}},
   460  		},
   461  		{
   462  			"no image to undeprecate",
   463  			&Publish{PublishProject: "foo-project", publishVersion: "3"},
   464  			&Image{Prefix: "foo", Family: "foo-family"},
   465  			[]*computeAlpha.Image{
   466  				{Name: "bar-3", Family: "bar-family"},
   467  				{Name: "foo-3", Family: "foo-family"},
   468  				{Name: "bar-2", Family: "bar-family", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   469  				{Name: "bar-1", Family: "bar-family", Deprecated: &computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   470  			},
   471  			&daisy.DeleteResources{Images: []string{"projects/foo-project/global/images/foo-3"}},
   472  			&daisy.DeprecateImages{},
   473  		},
   474  		{
   475  			"image DNE",
   476  			&Publish{PublishProject: "foo-project", publishVersion: "1"},
   477  			&Image{Prefix: "foo", Family: "foo-family"},
   478  			[]*computeAlpha.Image{
   479  				{Name: "bar-1", Family: "bar-family"},
   480  			},
   481  			nil,
   482  			nil,
   483  		},
   484  	}
   485  	for _, tt := range tests {
   486  		dr, di := rollbackImage(tt.p, tt.img, tt.pubImgs)
   487  		if diff := pretty.Compare(dr, tt.wantDR); diff != "" {
   488  			t.Errorf("%s: returned DeleteResources does not match expectation: (-got +want)\n%s", tt.desc, diff)
   489  		}
   490  		if diff := pretty.Compare(di, tt.wantDI); diff != "" {
   491  			t.Errorf("%s: returned DeprecateImages does not match expectation: (-got +want)\n%s", tt.desc, diff)
   492  		}
   493  	}
   494  
   495  }
   496  
   497  func TestPopulateSteps(t *testing.T) {
   498  	// This scenario is a bit contrived as there's no way you will get
   499  	// DeleteResources steps and CreateImages steps in the same workflow,
   500  	// but this simplifies the test data.
   501  	got := daisy.New()
   502  	err := populateSteps(
   503  		got,
   504  		"foo",
   505  		//&daisy.CreateImages{{Image: computeAlpha.Image{Name: "create-image"}}},
   506  		&daisy.CreateImages{ImagesAlpha: []*daisy.ImageAlpha{{Image: computeAlpha.Image{Name: "create-image"}}}},
   507  
   508  		&daisy.DeprecateImages{{Image: "deprecate-image"}},
   509  		&daisy.DeleteResources{Images: []string{"delete-image"}},
   510  	)
   511  	if err != nil {
   512  		t.Fatal(err)
   513  	}
   514  	got.Cancel = nil
   515  
   516  	want := &daisy.Workflow{
   517  		Steps: map[string]*daisy.Step{
   518  			"delete-foo":    {DeleteResources: &daisy.DeleteResources{Images: []string{"delete-image"}}},
   519  			"deprecate-foo": {DeprecateImages: &daisy.DeprecateImages{{Image: "deprecate-image"}}},
   520  			"publish-foo":   {Timeout: "1h", CreateImages: &daisy.CreateImages{ImagesAlpha: []*daisy.ImageAlpha{{Image: computeAlpha.Image{Name: "create-image"}}}}},
   521  		},
   522  		Dependencies: map[string][]string{
   523  			"delete-foo":    {"publish-foo", "deprecate-foo"},
   524  			"deprecate-foo": {"publish-foo"},
   525  		},
   526  		DefaultTimeout: "10m",
   527  	}
   528  
   529  	if diff := (&pretty.Config{Diffable: true, Formatter: pretty.DefaultFormatter}).Compare(got, want); diff != "" {
   530  		t.Errorf("-got +want\n%s", diff)
   531  	}
   532  
   533  }
   534  
   535  func TestPopulateWorkflow(t *testing.T) {
   536  	now := time.Now()
   537  	got := daisy.New()
   538  	p := &Publish{
   539  		SourceProject:  "foo-project",
   540  		PublishProject: "foo-project",
   541  		publishVersion: "pv",
   542  		sourceVersion:  "sv",
   543  		Images: []*Image{
   544  			{
   545  				Prefix: "test",
   546  				Family: "test-family",
   547  				RolloutPolicy: createRollOut([]*compute.Zone{
   548  					{Name: "us-central1-a", Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central1"},
   549  					{Name: "us-central1-b", Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central1"},
   550  					{Name: "us-central1-c", Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central1"},
   551  				}, now, 1),
   552  			},
   553  		},
   554  	}
   555  	err := p.populateWorkflow(
   556  		context.Background(),
   557  		got,
   558  		[]*computeAlpha.Image{
   559  			{Name: "test-old", Family: "test-family"},
   560  		},
   561  		p.Images[0],
   562  		false,
   563  		false,
   564  		false,
   565  		false,
   566  	)
   567  	if err != nil {
   568  		t.Fatal(err)
   569  	}
   570  	got.Cancel = nil
   571  
   572  	wantrp := computeAlpha.RolloutPolicy{DefaultRolloutTime: now.Add(time.Minute * 2).Format(time.RFC3339)}
   573  	wantrp.LocationRolloutPolicies = make(map[string]string)
   574  	wantrp.LocationRolloutPolicies["zones/us-central1-a"] = now.Format(time.RFC3339)
   575  	wantrp.LocationRolloutPolicies["zones/us-central1-b"] = now.Add(time.Minute).Format(time.RFC3339)
   576  	wantrp.LocationRolloutPolicies["zones/us-central1-c"] = now.Add(time.Minute * 2).Format(time.RFC3339)
   577  
   578  	want := &daisy.Workflow{
   579  		Steps: map[string]*daisy.Step{
   580  			"publish-test": {Timeout: "1h", CreateImages: &daisy.CreateImages{
   581  				ImagesAlpha: []*daisy.ImageAlpha{
   582  					{
   583  						ImageBase: daisy.ImageBase{Resource: daisy.Resource{Project: "foo-project", NoCleanup: true, RealName: "test-pv"}},
   584  						Image: computeAlpha.Image{
   585  							Name:            "test-pv",
   586  							Family:          "test-family",
   587  							SourceImage:     "projects/foo-project/global/images/test-sv",
   588  							RolloutOverride: &wantrp,
   589  						},
   590  					},
   591  				},
   592  			}},
   593  			"deprecate-test": {DeprecateImages: &daisy.DeprecateImages{
   594  				{Project: "foo-project", Image: "test-old", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "DEPRECATED", Replacement: "https://www.googleapis.com/compute/v1/projects/foo-project/global/images/test-pv", StateOverride: &wantrp}}},
   595  			},
   596  		},
   597  		Dependencies: map[string][]string{
   598  			"deprecate-test": {"publish-test"},
   599  		},
   600  		DefaultTimeout: "10m",
   601  	}
   602  
   603  	if diff := (&pretty.Config{Diffable: true, Formatter: pretty.DefaultFormatter}).Compare(got, want); diff != "" {
   604  		t.Errorf("-got +want\n%s", diff)
   605  	}
   606  
   607  }
   608  
   609  func TestCreatePrintOut(t *testing.T) {
   610  	tests := []struct {
   611  		name string
   612  		args *daisy.CreateImages
   613  		want []string
   614  	}{
   615  		{"empty", nil, nil},
   616  		{
   617  			"one image",
   618  			&daisy.CreateImages{ImagesAlpha: []*daisy.ImageAlpha{{Image: computeAlpha.Image{Name: "foo", Description: "bar"}}}},
   619  			[]string{"foo: (bar)"},
   620  		},
   621  		{"two images", &daisy.CreateImages{ImagesAlpha: []*daisy.ImageAlpha{
   622  			{Image: computeAlpha.Image{Name: "foo1", Description: "bar1"}},
   623  			{Image: computeAlpha.Image{Name: "foo2", Description: "bar2"}},
   624  		},
   625  		},
   626  			[]string{"foo1: (bar1)", "foo2: (bar2)"},
   627  		},
   628  	}
   629  	for _, tt := range tests {
   630  		t.Run(tt.name, func(t *testing.T) {
   631  			p := &Publish{}
   632  			p.createPrintOut(tt.args)
   633  			if !reflect.DeepEqual(p.toCreate, tt.want) {
   634  				t.Errorf("createPrintOut() got = %v, want %v", p.toCreate, tt.want)
   635  			}
   636  		})
   637  	}
   638  }
   639  
   640  func TestDeletePrintOut(t *testing.T) {
   641  	tests := []struct {
   642  		name string
   643  		args *daisy.DeleteResources
   644  		want []string
   645  	}{
   646  		{"empty", nil, nil},
   647  		{"not an image", &daisy.DeleteResources{Disks: []string{"foo"}}, nil},
   648  		{"one image", &daisy.DeleteResources{Images: []string{"foo"}}, []string{"foo"}},
   649  		{"two images", &daisy.DeleteResources{Images: []string{"foo", "bar"}}, []string{"foo", "bar"}},
   650  	}
   651  	for _, tt := range tests {
   652  		t.Run(tt.name, func(t *testing.T) {
   653  			p := &Publish{}
   654  			p.deletePrintOut(tt.args)
   655  			if !reflect.DeepEqual(p.toDelete, tt.want) {
   656  				t.Errorf("deletePrintOut() got = %v, want %v", p.toDelete, tt.want)
   657  			}
   658  		})
   659  	}
   660  }
   661  
   662  func TestDeprecatePrintOut(t *testing.T) {
   663  	tests := []struct {
   664  		name          string
   665  		args          *daisy.DeprecateImages
   666  		toDeprecate   []string
   667  		toObsolete    []string
   668  		toUndeprecate []string
   669  	}{
   670  		{"empty", nil, nil, nil, nil},
   671  		{"unknown state", &daisy.DeprecateImages{&daisy.DeprecateImage{Image: "foo", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "foo"}}}, nil, nil, nil},
   672  		{"only DEPRECATED", &daisy.DeprecateImages{&daisy.DeprecateImage{Image: "foo", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "DEPRECATED", StateOverride: &computeAlpha.RolloutPolicy{DefaultRolloutTime: time.Now().Format(time.RFC3339)}}}}, []string{"foo"}, nil, nil},
   673  		{"only OBSOLETE", &daisy.DeprecateImages{&daisy.DeprecateImage{Image: "foo", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "OBSOLETE"}}}, nil, []string{"foo"}, nil},
   674  		{"only un-deprecated", &daisy.DeprecateImages{&daisy.DeprecateImage{Image: "foo", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "ACTIVE"}}}, nil, nil, []string{"foo"}},
   675  		{"all three", &daisy.DeprecateImages{
   676  			&daisy.DeprecateImage{Image: "foo", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "DEPRECATED"}},
   677  			&daisy.DeprecateImage{Image: "bar", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "OBSOLETE"}},
   678  			&daisy.DeprecateImage{Image: "baz", DeprecationStatusAlpha: computeAlpha.DeprecationStatus{State: "ACTIVE"}}},
   679  			[]string{"foo"}, []string{"bar"}, []string{"baz"},
   680  		},
   681  	}
   682  	for _, tt := range tests {
   683  		t.Run(tt.name, func(t *testing.T) {
   684  			p := &Publish{}
   685  			p.deprecatePrintOut(tt.args)
   686  			if !reflect.DeepEqual(p.toDeprecate, tt.toDeprecate) {
   687  				t.Errorf("deprecatePrintOut() toDeprecate got = %v, want %v", p.toDeprecate, tt.toDeprecate)
   688  			}
   689  			if !reflect.DeepEqual(p.toObsolete, tt.toObsolete) {
   690  				t.Errorf("deprecatePrintOut() toObsolete got = %v, want %v", p.toObsolete, tt.toObsolete)
   691  			}
   692  			if !reflect.DeepEqual(p.toUndeprecate, tt.toUndeprecate) {
   693  				t.Errorf("deprecatePrintOut() toUndeprecate got = %v, want %v", p.toUndeprecate, tt.toUndeprecate)
   694  			}
   695  		})
   696  	}
   697  }
   698  
   699  func TestCreateRollOut(t *testing.T) {
   700  	startTime := time.Now().Round(time.Second)
   701  	tests := []struct {
   702  		desc             string
   703  		zones            []*compute.Zone
   704  		rolloutStartTime time.Time
   705  		rolloutRate      int
   706  		wantRollout      computeAlpha.RolloutPolicy
   707  	}{
   708  		{
   709  			desc: "3 regions, each region has a different number of zones.",
   710  			zones: []*compute.Zone{
   711  				{
   712  					Name:   "us-central1-a",
   713  					Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central1",
   714  				},
   715  				{
   716  					Name:   "us-central1-b",
   717  					Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central1",
   718  				},
   719  				{
   720  					Name:   "us-central2-a",
   721  					Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central2",
   722  				},
   723  				{
   724  					Name:   "us-central2-c",
   725  					Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central2",
   726  				},
   727  				{
   728  					Name:   "us-central2-b",
   729  					Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central2",
   730  				},
   731  				{
   732  					Name:   "us-central3-a",
   733  					Region: "https://www.googleapis.com/compute/v1/projects/projectname/regions/us-central2",
   734  				},
   735  			},
   736  			rolloutStartTime: startTime,
   737  			rolloutRate:      5,
   738  			wantRollout: computeAlpha.RolloutPolicy{
   739  				DefaultRolloutTime: startTime.Format(time.RFC3339),
   740  				LocationRolloutPolicies: map[string]string{
   741  					"us-central1-a": startTime.Format(time.RFC3339),
   742  					"us-central2-a": startTime.Add(5 * time.Minute).Format(time.RFC3339),
   743  					"us-central3-a": startTime.Add(10 * time.Minute).Format(time.RFC3339),
   744  					"us-central1-b": startTime.Add(15 * time.Minute).Format(time.RFC3339),
   745  					"us-central2-b": startTime.Add(20 * time.Minute).Format(time.RFC3339),
   746  					"us-central2-c": startTime.Add(25 * time.Minute).Format(time.RFC3339),
   747  				},
   748  			},
   749  		},
   750  	}
   751  	for _, tt := range tests {
   752  		t.Run(tt.desc, func(t *testing.T) {
   753  			rollout := createRollOut(tt.zones, tt.rolloutStartTime, tt.rolloutRate)
   754  
   755  			if reflect.DeepEqual(rollout, tt.wantRollout) {
   756  				t.Errorf("unexpected rollout got = %s, want = %s", rollout, tt.wantRollout)
   757  			}
   758  		})
   759  	}
   760  }
   761  
   762  func TestCreatePublishWithFile(t *testing.T) {
   763  	tests := []struct {
   764  		name    string
   765  		path    string
   766  		wantErr bool
   767  	}{
   768  		{"no valid path", "", true},
   769  		{"pass with valid path", "../../../daisy_workflows/build-publish/debian/debian_10.publish.json", false},
   770  	}
   771  	for _, tt := range tests {
   772  		t.Run(tt.name, func(t *testing.T) {
   773  			_, err := CreatePublish("", "", "", "", "", "", "", tt.path, map[string]string{}, map[string][]*computeAlpha.Image{})
   774  			if err != nil && !tt.wantErr {
   775  				t.Errorf("CreatePublish() called with path %s: got error %v", tt.path, err)
   776  			}
   777  			if tt.wantErr && err == nil {
   778  				t.Errorf("CreatePublish() called with path %s: did not get expected error", tt.path)
   779  			}
   780  		})
   781  	}
   782  }
   783  
   784  func TestCreatePublishWithTemplate(t *testing.T) {
   785  	tests := []struct {
   786  		name     string
   787  		template string
   788  		wantErr  bool
   789  	}{
   790  		{"pass template", `{"WorkProject": "blah"}`, false},
   791  		{"pass with invalid template", "{", true},
   792  	}
   793  	for _, tt := range tests {
   794  		t.Run(tt.name, func(t *testing.T) {
   795  			_, err := CreatePublishWithTemplate("", "", "", "", "", "", "", tt.template, map[string]string{}, map[string][]*computeAlpha.Image{})
   796  			if err != nil && !tt.wantErr {
   797  				t.Errorf("CreatePublishWithTemplate() called with template %s: got error %v", tt.template, err)
   798  			}
   799  			if tt.wantErr && err == nil {
   800  				t.Errorf("CreatePublishWithTemplate() called with template %s: did not get expected error", tt.template)
   801  			}
   802  		})
   803  	}
   804  }