go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/op_begin_test.go (about)

     1  // Copyright 2022 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  	"time"
    21  
    22  	statuspb "google.golang.org/genproto/googleapis/rpc/status"
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/protobuf/types/known/durationpb"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/common/clock/testclock"
    28  	"go.chromium.org/luci/gae/impl/memory"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  
    31  	"go.chromium.org/luci/deploy/api/modelpb"
    32  	"go.chromium.org/luci/deploy/api/rpcpb"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  	. "go.chromium.org/luci/common/testing/assertions"
    36  )
    37  
    38  var (
    39  	mockedDeployment = &modelpb.Deployment{
    40  		RepoRev: "mocked-deployment",
    41  		Config: &modelpb.DeploymentConfig{
    42  			ActuationTimeout: durationpb.New(3 * time.Minute),
    43  		},
    44  	}
    45  	mockedActuator = &modelpb.ActuatorInfo{Identity: "mocked-actuator"}
    46  	mockedTriggers = []*modelpb.ActuationTrigger{{}, {}}
    47  )
    48  
    49  func mockedIntendedState(payload string, traffic int32) *modelpb.AppengineState {
    50  	return &modelpb.AppengineState{
    51  		IntendedState: &modelpb.AppengineState_IntendedState{
    52  			DeployableYamls: []*modelpb.AppengineState_IntendedState_DeployableYaml{
    53  				{YamlPath: payload},
    54  			},
    55  		},
    56  		Services: []*modelpb.AppengineState_Service{
    57  			{
    58  				Name:             "default",
    59  				TrafficSplitting: modelpb.AppengineState_Service_COOKIE,
    60  				TrafficAllocation: map[string]int32{
    61  					"ver1": traffic,
    62  					"ver2": 1000 - traffic,
    63  				},
    64  				Versions: []*modelpb.AppengineState_Service_Version{
    65  					{
    66  						Name:          "ver1",
    67  						IntendedState: &modelpb.AppengineState_Service_Version_IntendedState{},
    68  					},
    69  					{
    70  						Name:          "ver2",
    71  						IntendedState: &modelpb.AppengineState_Service_Version_IntendedState{},
    72  					},
    73  				},
    74  			},
    75  		},
    76  	}
    77  }
    78  
    79  func mockedReportedState(payload string, traffic int32) *modelpb.AppengineState {
    80  	return &modelpb.AppengineState{
    81  		CapturedState: &modelpb.AppengineState_CapturedState{
    82  			LocationId: payload,
    83  		},
    84  		Services: []*modelpb.AppengineState_Service{
    85  			{
    86  				Name: "default",
    87  				TrafficAllocation: map[string]int32{
    88  					"ver1": traffic,
    89  					"ver2": 1000 - traffic,
    90  				},
    91  				Versions: []*modelpb.AppengineState_Service_Version{
    92  					{
    93  						Name:          "ver1",
    94  						CapturedState: &modelpb.AppengineState_Service_Version_CapturedState{},
    95  					},
    96  					{
    97  						Name:          "ver2",
    98  						CapturedState: &modelpb.AppengineState_Service_Version_CapturedState{},
    99  					},
   100  				},
   101  			},
   102  		},
   103  	}
   104  }
   105  
   106  func TestActuationBeginOp(t *testing.T) {
   107  	t.Parallel()
   108  
   109  	Convey("With datastore", t, func() {
   110  		now := testclock.TestRecentTimeUTC.Round(time.Millisecond)
   111  		ctx, _ := testclock.UseTime(context.Background(), now)
   112  		ctx = memory.Use(ctx)
   113  
   114  		So(datastore.Put(ctx, &Asset{
   115  			ID:                  "apps/app1",
   116  			Asset:               &modelpb.Asset{Id: "apps/app1"},
   117  			ConsecutiveFailures: 111,
   118  		}, &Asset{
   119  			ID:                  "apps/app2",
   120  			Asset:               &modelpb.Asset{Id: "apps/app2"},
   121  			ConsecutiveFailures: 222,
   122  		}), ShouldBeNil)
   123  
   124  		Convey("Executing", func() {
   125  			op, err := NewActuationBeginOp(ctx, []string{"apps/app1", "apps/app2"}, &modelpb.Actuation{
   126  				Id:         "actuation-id",
   127  				Deployment: mockedDeployment,
   128  				Actuator:   mockedActuator,
   129  				Triggers:   mockedTriggers,
   130  			})
   131  			So(err, ShouldBeNil)
   132  
   133  			app1Call := &rpcpb.AssetToActuate{
   134  				Config: &modelpb.AssetConfig{EnableAutomation: false},
   135  				IntendedState: &modelpb.AssetState{
   136  					State: &modelpb.AssetState_Appengine{
   137  						Appengine: mockedIntendedState("app1", 0),
   138  					},
   139  				},
   140  				ReportedState: &modelpb.AssetState{
   141  					State: &modelpb.AssetState_Appengine{
   142  						Appengine: mockedReportedState("app1", 200),
   143  					},
   144  				},
   145  			}
   146  			op.MakeDecision(ctx, "apps/app1", app1Call)
   147  
   148  			app2Call := &rpcpb.AssetToActuate{
   149  				Config: &modelpb.AssetConfig{EnableAutomation: true},
   150  				IntendedState: &modelpb.AssetState{
   151  					State: &modelpb.AssetState_Appengine{
   152  						Appengine: mockedIntendedState("app2", 0),
   153  					},
   154  				},
   155  				ReportedState: &modelpb.AssetState{
   156  					State: &modelpb.AssetState_Appengine{
   157  						Appengine: mockedReportedState("app2", 200),
   158  					},
   159  				},
   160  			}
   161  			op.MakeDecision(ctx, "apps/app2", app2Call)
   162  
   163  			decisions, err := op.Apply(ctx)
   164  			So(err, ShouldBeNil)
   165  
   166  			// Returned decisions are correct.
   167  			So(decisions, ShouldHaveLength, 2)
   168  			So(decisions["apps/app1"], ShouldResembleProto, &modelpb.ActuationDecision{
   169  				Decision: modelpb.ActuationDecision_SKIP_DISABLED,
   170  			})
   171  			So(decisions["apps/app2"], ShouldResembleProto, &modelpb.ActuationDecision{
   172  				Decision: modelpb.ActuationDecision_ACTUATE_STALE,
   173  			})
   174  
   175  			// Stored Actuation entity is correct.
   176  			storedActuation := &Actuation{ID: "actuation-id"}
   177  			So(datastore.Get(ctx, storedActuation), ShouldBeNil)
   178  			So(storedActuation.Actuation, ShouldResembleProto, &modelpb.Actuation{
   179  				Id:         "actuation-id",
   180  				State:      modelpb.Actuation_EXECUTING,
   181  				Deployment: mockedDeployment,
   182  				Actuator:   mockedActuator,
   183  				Triggers:   mockedTriggers,
   184  				Created:    timestamppb.New(now),
   185  				Expiry:     timestamppb.New(now.Add(3 * time.Minute)),
   186  			})
   187  			So(storedActuation.Decisions, ShouldResembleProto, &modelpb.ActuationDecisions{
   188  				Decisions: map[string]*modelpb.ActuationDecision{
   189  					"apps/app1": {Decision: modelpb.ActuationDecision_SKIP_DISABLED},
   190  					"apps/app2": {Decision: modelpb.ActuationDecision_ACTUATE_STALE},
   191  				},
   192  			})
   193  			So(storedActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING)
   194  			So(storedActuation.Created.Equal(now), ShouldBeTrue)
   195  			So(storedActuation.Expiry.Equal(now.Add(3*time.Minute)), ShouldBeTrue)
   196  
   197  			// Stored Asset entities are correct.
   198  			assets, err := fetchAssets(ctx, []string{"apps/app1", "apps/app2"}, true)
   199  			So(err, ShouldBeNil)
   200  			So(assets["apps/app1"].Asset, ShouldResembleProto, &modelpb.Asset{
   201  				Id:            "apps/app1",
   202  				LastActuation: storedActuation.Actuation,
   203  				LastDecision:  decisions["apps/app1"],
   204  				Config:        &modelpb.AssetConfig{EnableAutomation: false},
   205  				IntendedState: &modelpb.AssetState{
   206  					Timestamp:  timestamppb.New(now),
   207  					Deployment: storedActuation.Actuation.Deployment,
   208  					Actuator:   storedActuation.Actuation.Actuator,
   209  					State: &modelpb.AssetState_Appengine{
   210  						Appengine: mockedIntendedState("app1", 0),
   211  					},
   212  				},
   213  				ReportedState: &modelpb.AssetState{
   214  					Timestamp:  timestamppb.New(now),
   215  					Deployment: storedActuation.Actuation.Deployment,
   216  					Actuator:   storedActuation.Actuation.Actuator,
   217  					State: &modelpb.AssetState_Appengine{
   218  						Appengine: mockedReportedState("app1", 200),
   219  					},
   220  				},
   221  			})
   222  			So(assets["apps/app1"].ConsecutiveFailures, ShouldEqual, 0) // was reset
   223  
   224  			So(assets["apps/app2"].Asset, ShouldResembleProto, &modelpb.Asset{
   225  				Id:                   "apps/app2",
   226  				LastActuation:        storedActuation.Actuation,
   227  				LastDecision:         decisions["apps/app2"],
   228  				LastActuateActuation: storedActuation.Actuation,
   229  				LastActuateDecision:  decisions["apps/app2"],
   230  				Config:               &modelpb.AssetConfig{EnableAutomation: true},
   231  				IntendedState: &modelpb.AssetState{
   232  					Timestamp:  timestamppb.New(now),
   233  					Deployment: storedActuation.Actuation.Deployment,
   234  					Actuator:   storedActuation.Actuation.Actuator,
   235  					State: &modelpb.AssetState_Appengine{
   236  						Appengine: mockedIntendedState("app2", 0),
   237  					},
   238  				},
   239  				ReportedState: &modelpb.AssetState{
   240  					Timestamp:  timestamppb.New(now),
   241  					Deployment: storedActuation.Actuation.Deployment,
   242  					Actuator:   storedActuation.Actuation.Actuator,
   243  					State: &modelpb.AssetState_Appengine{
   244  						Appengine: mockedReportedState("app2", 200),
   245  					},
   246  				},
   247  			})
   248  			So(assets["apps/app2"].ConsecutiveFailures, ShouldEqual, 222) // unchanged
   249  
   250  			// Made correct history records.
   251  			So(assets["apps/app1"].LastHistoryID, ShouldEqual, 1)
   252  			So(assets["apps/app1"].HistoryEntry, ShouldResembleProto, &modelpb.AssetHistory{
   253  				AssetId:                  "apps/app1",
   254  				HistoryId:                1,
   255  				Decision:                 decisions["apps/app1"],
   256  				Actuation:                storedActuation.Actuation,
   257  				Config:                   app1Call.Config,
   258  				IntendedState:            app1Call.IntendedState,
   259  				ReportedState:            app1Call.ReportedState,
   260  				PriorConsecutiveFailures: 111,
   261  			})
   262  			rec := AssetHistory{ID: 1, Parent: datastore.KeyForObj(ctx, assets["apps/app1"])}
   263  			So(datastore.Get(ctx, &rec), ShouldBeNil)
   264  			So(rec.Entry, ShouldResembleProto, assets["apps/app1"].HistoryEntry)
   265  
   266  			So(assets["apps/app2"].LastHistoryID, ShouldEqual, 0)
   267  			So(assets["apps/app2"].HistoryEntry, ShouldResembleProto, &modelpb.AssetHistory{
   268  				AssetId:                  "apps/app2",
   269  				HistoryId:                1,
   270  				Decision:                 decisions["apps/app2"],
   271  				Actuation:                storedActuation.Actuation,
   272  				Config:                   app2Call.Config,
   273  				IntendedState:            app2Call.IntendedState,
   274  				ReportedState:            app2Call.ReportedState,
   275  				PriorConsecutiveFailures: 222,
   276  			})
   277  			rec = AssetHistory{ID: 1, Parent: datastore.KeyForObj(ctx, assets["apps/app2"])}
   278  			So(datastore.Get(ctx, &rec), ShouldEqual, datastore.ErrNoSuchEntity)
   279  		})
   280  
   281  		Convey("Skipping disabled", func() {
   282  			op, err := NewActuationBeginOp(ctx, []string{"apps/app1"}, &modelpb.Actuation{
   283  				Id:         "actuation-id",
   284  				Deployment: mockedDeployment,
   285  				Actuator:   mockedActuator,
   286  				Triggers:   mockedTriggers,
   287  			})
   288  			So(err, ShouldBeNil)
   289  
   290  			op.MakeDecision(ctx, "apps/app1", &rpcpb.AssetToActuate{
   291  				Config: &modelpb.AssetConfig{EnableAutomation: false},
   292  				IntendedState: &modelpb.AssetState{
   293  					State: &modelpb.AssetState_Appengine{
   294  						Appengine: mockedIntendedState("app1", 0),
   295  					},
   296  				},
   297  				ReportedState: &modelpb.AssetState{
   298  					State: &modelpb.AssetState_Appengine{
   299  						Appengine: mockedReportedState("app1", 200),
   300  					},
   301  				},
   302  			})
   303  
   304  			_, err = op.Apply(ctx)
   305  			So(err, ShouldBeNil)
   306  
   307  			// Stored Actuation entity is correct.
   308  			storedActuation := &Actuation{ID: "actuation-id"}
   309  			So(datastore.Get(ctx, storedActuation), ShouldBeNil)
   310  			So(storedActuation.Actuation, ShouldResembleProto, &modelpb.Actuation{
   311  				Id:         "actuation-id",
   312  				State:      modelpb.Actuation_SUCCEEDED,
   313  				Deployment: mockedDeployment,
   314  				Actuator:   mockedActuator,
   315  				Triggers:   mockedTriggers,
   316  				Created:    timestamppb.New(now),
   317  				Finished:   timestamppb.New(now),
   318  			})
   319  			So(storedActuation.Decisions, ShouldResembleProto, &modelpb.ActuationDecisions{
   320  				Decisions: map[string]*modelpb.ActuationDecision{
   321  					"apps/app1": {Decision: modelpb.ActuationDecision_SKIP_DISABLED},
   322  				},
   323  			})
   324  			So(storedActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   325  			So(storedActuation.Created.Equal(now), ShouldBeTrue)
   326  			So(storedActuation.Expiry.IsZero(), ShouldBeTrue)
   327  
   328  			// Reset ConsecutiveFailures counter.
   329  			assets, _ := fetchAssets(ctx, []string{"apps/app1"}, true)
   330  			So(assets["apps/app1"].ConsecutiveFailures, ShouldEqual, 0)
   331  		})
   332  
   333  		Convey("Skipping up-to-date", func() {
   334  			datastore.Put(ctx, &Asset{
   335  				ID: "apps/app1",
   336  				Asset: &modelpb.Asset{
   337  					Id: "apps/app1",
   338  					AppliedState: &modelpb.AssetState{
   339  						State: &modelpb.AssetState_Appengine{
   340  							Appengine: mockedIntendedState("app1", 0),
   341  						},
   342  					},
   343  				},
   344  			})
   345  
   346  			op, err := NewActuationBeginOp(ctx, []string{"apps/app1"}, &modelpb.Actuation{
   347  				Id:         "actuation-id",
   348  				Deployment: mockedDeployment,
   349  				Actuator:   mockedActuator,
   350  				Triggers:   mockedTriggers,
   351  			})
   352  			So(err, ShouldBeNil)
   353  
   354  			op.MakeDecision(ctx, "apps/app1", &rpcpb.AssetToActuate{
   355  				Config: &modelpb.AssetConfig{EnableAutomation: true},
   356  				IntendedState: &modelpb.AssetState{
   357  					State: &modelpb.AssetState_Appengine{
   358  						Appengine: mockedIntendedState("app1", 0),
   359  					},
   360  				},
   361  				ReportedState: &modelpb.AssetState{
   362  					State: &modelpb.AssetState_Appengine{
   363  						Appengine: mockedReportedState("app1", 0),
   364  					},
   365  				},
   366  			})
   367  
   368  			_, err = op.Apply(ctx)
   369  			So(err, ShouldBeNil)
   370  
   371  			// Stored Actuation entity is correct.
   372  			storedActuation := &Actuation{ID: "actuation-id"}
   373  			So(datastore.Get(ctx, storedActuation), ShouldBeNil)
   374  			So(storedActuation.Actuation, ShouldResembleProto, &modelpb.Actuation{
   375  				Id:         "actuation-id",
   376  				State:      modelpb.Actuation_SUCCEEDED,
   377  				Deployment: mockedDeployment,
   378  				Actuator:   mockedActuator,
   379  				Triggers:   mockedTriggers,
   380  				Created:    timestamppb.New(now),
   381  				Finished:   timestamppb.New(now),
   382  			})
   383  			So(storedActuation.Decisions, ShouldResembleProto, &modelpb.ActuationDecisions{
   384  				Decisions: map[string]*modelpb.ActuationDecision{
   385  					"apps/app1": {Decision: modelpb.ActuationDecision_SKIP_UPTODATE},
   386  				},
   387  			})
   388  			So(storedActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   389  			So(storedActuation.Created.Equal(now), ShouldBeTrue)
   390  			So(storedActuation.Expiry.IsZero(), ShouldBeTrue)
   391  
   392  			// Stored Asset entity is correct.
   393  			assets, err := fetchAssets(ctx, []string{"apps/app1"}, true)
   394  			So(err, ShouldBeNil)
   395  			So(assets["apps/app1"].Asset, ShouldResembleProto, &modelpb.Asset{
   396  				Id:            "apps/app1",
   397  				LastActuation: storedActuation.Actuation,
   398  				LastDecision:  storedActuation.Decisions.Decisions["apps/app1"],
   399  				Config:        &modelpb.AssetConfig{EnableAutomation: true},
   400  				IntendedState: &modelpb.AssetState{
   401  					Timestamp:  timestamppb.New(now),
   402  					Deployment: storedActuation.Actuation.Deployment,
   403  					Actuator:   storedActuation.Actuation.Actuator,
   404  					State: &modelpb.AssetState_Appengine{
   405  						Appengine: mockedIntendedState("app1", 0),
   406  					},
   407  				},
   408  				ReportedState: &modelpb.AssetState{
   409  					Timestamp:  timestamppb.New(now),
   410  					Deployment: storedActuation.Actuation.Deployment,
   411  					Actuator:   storedActuation.Actuation.Actuator,
   412  					State: &modelpb.AssetState_Appengine{
   413  						Appengine: mockedReportedState("app1", 0),
   414  					},
   415  				},
   416  				AppliedState: &modelpb.AssetState{
   417  					Timestamp:  timestamppb.New(now),
   418  					Deployment: storedActuation.Actuation.Deployment,
   419  					Actuator:   storedActuation.Actuation.Actuator,
   420  					State: &modelpb.AssetState_Appengine{
   421  						Appengine: mockedIntendedState("app1", 0),
   422  					},
   423  				},
   424  			})
   425  			So(assets["apps/app1"].ConsecutiveFailures, ShouldEqual, 0)
   426  		})
   427  
   428  		Convey("Broken", func() {
   429  			op, err := NewActuationBeginOp(ctx, []string{"apps/app1"}, &modelpb.Actuation{
   430  				Id:         "actuation-id",
   431  				Deployment: mockedDeployment,
   432  				Actuator:   mockedActuator,
   433  				Triggers:   mockedTriggers,
   434  			})
   435  			So(err, ShouldBeNil)
   436  
   437  			op.MakeDecision(ctx, "apps/app1", &rpcpb.AssetToActuate{
   438  				Config: &modelpb.AssetConfig{EnableAutomation: true},
   439  				IntendedState: &modelpb.AssetState{
   440  					Status: &statuspb.Status{
   441  						Code:    int32(codes.FailedPrecondition),
   442  						Message: "intended broken",
   443  					},
   444  				},
   445  				ReportedState: &modelpb.AssetState{
   446  					Status: &statuspb.Status{
   447  						Code:    int32(codes.FailedPrecondition),
   448  						Message: "reported broken",
   449  					},
   450  				},
   451  			})
   452  
   453  			_, err = op.Apply(ctx)
   454  			So(err, ShouldBeNil)
   455  
   456  			// Stored Actuation entity is correct.
   457  			storedActuation := &Actuation{ID: "actuation-id"}
   458  			So(datastore.Get(ctx, storedActuation), ShouldBeNil)
   459  			So(storedActuation.Actuation, ShouldResembleProto, &modelpb.Actuation{
   460  				Id:         "actuation-id",
   461  				State:      modelpb.Actuation_FAILED,
   462  				Deployment: mockedDeployment,
   463  				Actuator:   mockedActuator,
   464  				Triggers:   mockedTriggers,
   465  				Created:    timestamppb.New(now),
   466  				Finished:   timestamppb.New(now),
   467  				Status: &statuspb.Status{
   468  					Code: int32(codes.Internal),
   469  					Message: "asset \"apps/app1\": failed to collect intended state: " +
   470  						"rpc error: code = FailedPrecondition desc = intended broken; " +
   471  						"asset \"apps/app1\": failed to collect reported state: rpc error: " +
   472  						"code = FailedPrecondition desc = reported broken",
   473  				},
   474  			})
   475  			So(storedActuation.Decisions, ShouldResembleProto, &modelpb.ActuationDecisions{
   476  				Decisions: map[string]*modelpb.ActuationDecision{
   477  					"apps/app1": {
   478  						Decision: modelpb.ActuationDecision_SKIP_BROKEN,
   479  						Status: &statuspb.Status{
   480  							Code:    int32(codes.FailedPrecondition),
   481  							Message: "reported broken",
   482  						},
   483  					},
   484  				},
   485  			})
   486  			So(storedActuation.State, ShouldEqual, modelpb.Actuation_FAILED)
   487  			So(storedActuation.Created.Equal(now), ShouldBeTrue)
   488  			So(storedActuation.Expiry.IsZero(), ShouldBeTrue)
   489  
   490  			// Incremented ConsecutiveFailures counter.
   491  			assets, _ := fetchAssets(ctx, []string{"apps/app1"}, true)
   492  			So(assets["apps/app1"].ConsecutiveFailures, ShouldEqual, 112)
   493  
   494  			// Stored the historical record with correct ConsecutiveFailures counter.
   495  			So(assets["apps/app1"].LastHistoryID, ShouldEqual, 1)
   496  			So(assets["apps/app1"].HistoryEntry.PriorConsecutiveFailures, ShouldEqual, 111)
   497  			rec := AssetHistory{ID: 1, Parent: datastore.KeyForObj(ctx, assets["apps/app1"])}
   498  			So(datastore.Get(ctx, &rec), ShouldBeNil)
   499  			So(rec.Entry, ShouldResembleProto, assets["apps/app1"].HistoryEntry)
   500  		})
   501  	})
   502  }