go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/op_end_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/timestamppb"
    25  
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  	"go.chromium.org/luci/gae/impl/memory"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  
    30  	"go.chromium.org/luci/deploy/api/modelpb"
    31  	"go.chromium.org/luci/deploy/api/rpcpb"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func TestActuationEndOp(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	Convey("With datastore", t, func() {
    41  		now := testclock.TestRecentTimeUTC.Round(time.Millisecond)
    42  		ctx, _ := testclock.UseTime(context.Background(), now)
    43  		ctx = memory.Use(ctx)
    44  
    45  		Convey("Missing assets", func() {
    46  			_, err := NewActuationEndOp(ctx, &Actuation{
    47  				Decisions: &modelpb.ActuationDecisions{
    48  					Decisions: map[string]*modelpb.ActuationDecision{
    49  						"apps/missing": {Decision: modelpb.ActuationDecision_ACTUATE_STALE},
    50  					},
    51  				},
    52  			})
    53  			So(err, ShouldErrLike, "assets entities unexpectedly missing: apps/missing")
    54  		})
    55  
    56  		Convey("Works", func() {
    57  			assets := []*Asset{
    58  				{
    59  					ID: "apps/app1",
    60  					Asset: &modelpb.Asset{
    61  						Id: "apps/app1",
    62  						LastActuation: &modelpb.Actuation{
    63  							Id:    "actuation-id",
    64  							State: modelpb.Actuation_EXECUTING,
    65  						},
    66  						LastActuateActuation: &modelpb.Actuation{
    67  							Id:    "actuation-id",
    68  							State: modelpb.Actuation_EXECUTING,
    69  						},
    70  						IntendedState: &modelpb.AssetState{
    71  							State: &modelpb.AssetState_Appengine{
    72  								Appengine: mockedReportedState("intended", 0),
    73  							},
    74  						},
    75  						ReportedState: &modelpb.AssetState{
    76  							State: &modelpb.AssetState_Appengine{
    77  								Appengine: mockedReportedState("old reported", 0),
    78  							},
    79  						},
    80  						ActuatedState: &modelpb.AssetState{
    81  							State: &modelpb.AssetState_Appengine{
    82  								Appengine: mockedReportedState("old actuated", 0),
    83  							},
    84  						},
    85  					},
    86  					LastHistoryID: 123,
    87  					HistoryEntry: &modelpb.AssetHistory{
    88  						AssetId:   "apps/app1",
    89  						HistoryId: 124, // i.e. being recorded now
    90  						Actuation: &modelpb.Actuation{
    91  							Id: "phony-to-be-overridden",
    92  						},
    93  						PriorConsecutiveFailures: 111,
    94  					},
    95  					ConsecutiveFailures: 111,
    96  				},
    97  				{
    98  					ID: "apps/app2",
    99  					Asset: &modelpb.Asset{
   100  						Id: "apps/app2",
   101  						IntendedState: &modelpb.AssetState{
   102  							State: &modelpb.AssetState_Appengine{
   103  								Appengine: mockedReportedState("intended", 0),
   104  							},
   105  						},
   106  						LastActuation: &modelpb.Actuation{
   107  							Id: "another-actuation",
   108  						},
   109  						LastActuateActuation: &modelpb.Actuation{
   110  							Id: "another-actuation",
   111  						},
   112  					},
   113  					LastHistoryID: 123,
   114  					HistoryEntry: &modelpb.AssetHistory{
   115  						AssetId:   "apps/app2",
   116  						HistoryId: 123,
   117  						Actuation: &modelpb.Actuation{
   118  							Id: "phony-to-be-untouched",
   119  						},
   120  					},
   121  					ConsecutiveFailures: 222,
   122  				},
   123  			}
   124  			So(datastore.Put(ctx, assets), ShouldBeNil)
   125  
   126  			op, err := NewActuationEndOp(ctx, &Actuation{
   127  				ID: "actuation-id",
   128  				Actuation: &modelpb.Actuation{
   129  					Id:         "actuation-id",
   130  					Deployment: mockedDeployment,
   131  					Actuator:   mockedActuator,
   132  					State:      modelpb.Actuation_EXECUTING,
   133  					LogUrl:     "old-log-url",
   134  				},
   135  				Decisions: &modelpb.ActuationDecisions{
   136  					Decisions: map[string]*modelpb.ActuationDecision{
   137  						"apps/app1": {Decision: modelpb.ActuationDecision_ACTUATE_STALE},
   138  						"apps/app2": {Decision: modelpb.ActuationDecision_ACTUATE_STALE},
   139  					},
   140  				},
   141  			})
   142  			So(err, ShouldBeNil)
   143  
   144  			Convey("Success", func() {
   145  				op.UpdateActuationStatus(ctx, nil, "new-log-url")
   146  				op.HandleActuatedState(ctx, "apps/app1", &rpcpb.ActuatedAsset{
   147  					State: &modelpb.AssetState{
   148  						State: &modelpb.AssetState_Appengine{
   149  							Appengine: mockedReportedState("new actuated", 0),
   150  						},
   151  					},
   152  				})
   153  				op.HandleActuatedState(ctx, "apps/app2", &rpcpb.ActuatedAsset{
   154  					State: &modelpb.AssetState{
   155  						State: &modelpb.AssetState_Appengine{
   156  							Appengine: mockedReportedState("new actuated", 0),
   157  						},
   158  					},
   159  				})
   160  
   161  				So(op.Apply(ctx), ShouldBeNil)
   162  
   163  				// Updated Actuation entity.
   164  				storedActuation := &Actuation{ID: "actuation-id"}
   165  				So(datastore.Get(ctx, storedActuation), ShouldBeNil)
   166  				So(storedActuation.Actuation, ShouldResembleProto, &modelpb.Actuation{
   167  					Id:         "actuation-id",
   168  					State:      modelpb.Actuation_SUCCEEDED,
   169  					Deployment: mockedDeployment,
   170  					Actuator:   mockedActuator,
   171  					Finished:   timestamppb.New(now),
   172  					LogUrl:     "new-log-url",
   173  				})
   174  				So(storedActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   175  
   176  				// Updated the asset assigned to this actuation.
   177  				assets, err := fetchAssets(ctx, []string{"apps/app1", "apps/app2"}, true)
   178  				So(err, ShouldBeNil)
   179  
   180  				So(assets["apps/app1"].Asset, ShouldResembleProto, &modelpb.Asset{
   181  					Id:                   "apps/app1",
   182  					LastActuation:        storedActuation.Actuation,
   183  					LastActuateActuation: storedActuation.Actuation,
   184  					IntendedState: &modelpb.AssetState{
   185  						State: &modelpb.AssetState_Appengine{
   186  							Appengine: mockedReportedState("intended", 0),
   187  						},
   188  					},
   189  					ReportedState: &modelpb.AssetState{
   190  						Timestamp:  timestamppb.New(now),
   191  						Deployment: storedActuation.Actuation.Deployment,
   192  						Actuator:   storedActuation.Actuation.Actuator,
   193  						State: &modelpb.AssetState_Appengine{
   194  							Appengine: mockedReportedState("new actuated", 0),
   195  						},
   196  					},
   197  					ActuatedState: &modelpb.AssetState{
   198  						Timestamp:  timestamppb.New(now),
   199  						Deployment: storedActuation.Actuation.Deployment,
   200  						Actuator:   storedActuation.Actuation.Actuator,
   201  						State: &modelpb.AssetState_Appengine{
   202  							Appengine: mockedReportedState("new actuated", 0),
   203  						},
   204  					},
   205  					AppliedState: &modelpb.AssetState{
   206  						State: &modelpb.AssetState_Appengine{
   207  							Appengine: mockedReportedState("intended", 0),
   208  						},
   209  					},
   210  				})
   211  				So(assets["apps/app1"].LastHistoryID, ShouldEqual, 124)
   212  				So(assets["apps/app1"].HistoryEntry, ShouldResembleProto, &modelpb.AssetHistory{
   213  					AssetId:   "apps/app1",
   214  					HistoryId: 124,
   215  					Actuation: storedActuation.Actuation,
   216  					PostActuationState: &modelpb.AssetState{
   217  						Timestamp:  timestamppb.New(now),
   218  						Deployment: storedActuation.Actuation.Deployment,
   219  						Actuator:   storedActuation.Actuation.Actuator,
   220  						State: &modelpb.AssetState_Appengine{
   221  							Appengine: mockedReportedState("new actuated", 0),
   222  						},
   223  					},
   224  					PriorConsecutiveFailures: 111,
   225  				})
   226  				So(assets["apps/app1"].ConsecutiveFailures, ShouldEqual, 0)
   227  
   228  				// Created the history entity.
   229  				rec := AssetHistory{ID: 124, Parent: datastore.KeyForObj(ctx, assets["apps/app1"])}
   230  				So(datastore.Get(ctx, &rec), ShouldBeNil)
   231  				So(rec.Entry, ShouldResembleProto, assets["apps/app1"].HistoryEntry)
   232  
   233  				// Wasn't touched.
   234  				So(assets["apps/app2"].Asset, ShouldResembleProto, &modelpb.Asset{
   235  					Id: "apps/app2",
   236  					IntendedState: &modelpb.AssetState{
   237  						State: &modelpb.AssetState_Appengine{
   238  							Appengine: mockedReportedState("intended", 0),
   239  						},
   240  					},
   241  					LastActuation: &modelpb.Actuation{
   242  						Id: "another-actuation",
   243  					},
   244  					LastActuateActuation: &modelpb.Actuation{
   245  						Id: "another-actuation",
   246  					},
   247  				})
   248  				So(assets["apps/app2"].LastHistoryID, ShouldEqual, 123)
   249  				So(assets["apps/app2"].HistoryEntry, ShouldResembleProto, &modelpb.AssetHistory{
   250  					AssetId:   "apps/app2",
   251  					HistoryId: 123,
   252  					Actuation: &modelpb.Actuation{
   253  						Id: "phony-to-be-untouched",
   254  					},
   255  				})
   256  				So(assets["apps/app2"].ConsecutiveFailures, ShouldEqual, 222)
   257  			})
   258  
   259  			Convey("Failed", func() {
   260  				op.UpdateActuationStatus(ctx, &statuspb.Status{
   261  					Code:    int32(codes.FailedPrecondition),
   262  					Message: "actuation boom",
   263  				}, "")
   264  				op.HandleActuatedState(ctx, "apps/app1", &rpcpb.ActuatedAsset{
   265  					State: &modelpb.AssetState{
   266  						Status: &statuspb.Status{
   267  							Code:    int32(codes.InvalidArgument),
   268  							Message: "status boom",
   269  						},
   270  					},
   271  				})
   272  				op.HandleActuatedState(ctx, "apps/app2", &rpcpb.ActuatedAsset{
   273  					State: &modelpb.AssetState{
   274  						Status: &statuspb.Status{
   275  							Code:    int32(codes.InvalidArgument),
   276  							Message: "status boom",
   277  						},
   278  					},
   279  				})
   280  
   281  				So(op.Apply(ctx), ShouldBeNil)
   282  
   283  				// Updated Actuation entity.
   284  				storedActuation := &Actuation{ID: "actuation-id"}
   285  				So(datastore.Get(ctx, storedActuation), ShouldBeNil)
   286  				So(storedActuation.Actuation, ShouldResembleProto, &modelpb.Actuation{
   287  					Id:    "actuation-id",
   288  					State: modelpb.Actuation_FAILED,
   289  					Status: &statuspb.Status{
   290  						Code:    int32(codes.FailedPrecondition),
   291  						Message: "actuation boom",
   292  					},
   293  					Deployment: mockedDeployment,
   294  					Actuator:   mockedActuator,
   295  					Finished:   timestamppb.New(now),
   296  					LogUrl:     "old-log-url",
   297  				})
   298  				So(storedActuation.State, ShouldEqual, modelpb.Actuation_FAILED)
   299  
   300  				// Updated the asset assigned to this actuation.
   301  				assets, err := fetchAssets(ctx, []string{"apps/app1", "apps/app2"}, true)
   302  				So(err, ShouldBeNil)
   303  
   304  				So(assets["apps/app1"].Asset, ShouldResembleProto, &modelpb.Asset{
   305  					Id:                   "apps/app1",
   306  					LastActuation:        storedActuation.Actuation,
   307  					LastActuateActuation: storedActuation.Actuation,
   308  					IntendedState: &modelpb.AssetState{
   309  						State: &modelpb.AssetState_Appengine{
   310  							Appengine: mockedReportedState("intended", 0),
   311  						},
   312  					},
   313  					ReportedState: &modelpb.AssetState{
   314  						State: &modelpb.AssetState_Appengine{
   315  							Appengine: mockedReportedState("old reported", 0),
   316  						},
   317  					},
   318  					ActuatedState: &modelpb.AssetState{
   319  						State: &modelpb.AssetState_Appengine{
   320  							Appengine: mockedReportedState("old actuated", 0),
   321  						},
   322  					},
   323  					PostActuationStatus: &statuspb.Status{
   324  						Code:    int32(codes.InvalidArgument),
   325  						Message: "status boom",
   326  					},
   327  				})
   328  
   329  				// Stored the historical record with correct PriorConsecutiveFailures
   330  				// counter.
   331  				So(assets["apps/app1"].ConsecutiveFailures, ShouldEqual, 112)
   332  				So(assets["apps/app1"].LastHistoryID, ShouldEqual, 124)
   333  				So(assets["apps/app1"].HistoryEntry.PriorConsecutiveFailures, ShouldEqual, 111)
   334  				rec := AssetHistory{ID: 124, Parent: datastore.KeyForObj(ctx, assets["apps/app1"])}
   335  				So(datastore.Get(ctx, &rec), ShouldBeNil)
   336  				So(rec.Entry, ShouldResembleProto, assets["apps/app1"].HistoryEntry)
   337  
   338  				// Wasn't touched.
   339  				So(assets["apps/app2"].Asset, ShouldResembleProto, &modelpb.Asset{
   340  					Id: "apps/app2",
   341  					IntendedState: &modelpb.AssetState{
   342  						State: &modelpb.AssetState_Appengine{
   343  							Appengine: mockedReportedState("intended", 0),
   344  						},
   345  					},
   346  					LastActuation: &modelpb.Actuation{
   347  						Id: "another-actuation",
   348  					},
   349  					LastActuateActuation: &modelpb.Actuation{
   350  						Id: "another-actuation",
   351  					},
   352  				})
   353  				So(assets["apps/app2"].ConsecutiveFailures, ShouldEqual, 222)
   354  			})
   355  		})
   356  	})
   357  }