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 }