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 }