go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/roundtrip_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 "go.chromium.org/luci/common/clock/testclock" 23 "go.chromium.org/luci/gae/impl/memory" 24 "go.chromium.org/luci/gae/service/datastore" 25 26 "go.chromium.org/luci/deploy/api/modelpb" 27 "go.chromium.org/luci/deploy/api/rpcpb" 28 29 . "github.com/smartystreets/goconvey/convey" 30 ) 31 32 func TestRoundTrip(t *testing.T) { 33 t.Parallel() 34 35 Convey("With datastore", t, func() { 36 now := testclock.TestRecentTimeUTC.Round(time.Millisecond) 37 ctx, _ := testclock.UseTime(context.Background(), now) 38 ctx = memory.Use(ctx) 39 40 store := func(a *modelpb.Asset) { 41 So(datastore.Put(ctx, &Asset{ 42 ID: a.Id, 43 Asset: a, 44 }), ShouldBeNil) 45 } 46 47 fetch := func(assetID string) *modelpb.Asset { 48 ent := &Asset{ID: assetID} 49 So(datastore.Get(ctx, ent), ShouldBeNil) 50 return ent.Asset 51 } 52 53 history := func(assetID string, historyID int64) *modelpb.AssetHistory { 54 ent := &AssetHistory{ 55 ID: historyID, 56 Parent: datastore.NewKey(ctx, "Asset", assetID, 0, nil), 57 } 58 if datastore.Get(ctx, ent) == datastore.ErrNoSuchEntity { 59 return nil 60 } 61 return ent.Entry 62 } 63 64 intendedState := func(payload string, traffic int32) *modelpb.AssetState { 65 return &modelpb.AssetState{ 66 State: &modelpb.AssetState_Appengine{ 67 Appengine: mockedIntendedState(payload, traffic), 68 }, 69 } 70 } 71 72 reportedState := func(payload string, traffic int32) *modelpb.AssetState { 73 return &modelpb.AssetState{ 74 State: &modelpb.AssetState_Appengine{ 75 Appengine: mockedReportedState(payload, traffic), 76 }, 77 } 78 } 79 80 Convey("Two assets, one up-to-date", func() { 81 store(&modelpb.Asset{ 82 Id: "apps/app-1", 83 AppliedState: intendedState("app-1", 0), 84 LastActuateActuation: &modelpb.Actuation{ 85 Id: "old-actuation", 86 }, 87 }) 88 store(&modelpb.Asset{ 89 Id: "apps/app-2", 90 AppliedState: intendedState("app-2", 0), 91 LastActuateActuation: &modelpb.Actuation{ 92 Id: "old-actuation", 93 }, 94 }) 95 96 beginOp, err := NewActuationBeginOp(ctx, []string{"apps/app-1", "apps/app-2"}, &modelpb.Actuation{ 97 Id: "new-actuation", 98 }) 99 So(err, ShouldBeNil) 100 101 // SKIP_UPTODATE decision. 102 beginOp.MakeDecision(ctx, "apps/app-1", &rpcpb.AssetToActuate{ 103 Config: &modelpb.AssetConfig{EnableAutomation: true}, 104 IntendedState: intendedState("app-1", 0), 105 ReportedState: reportedState("app-1", 0), 106 }) 107 // ACTUATE_STALE decision. 108 beginOp.MakeDecision(ctx, "apps/app-2", &rpcpb.AssetToActuate{ 109 Config: &modelpb.AssetConfig{EnableAutomation: true}, 110 IntendedState: intendedState("app-2", 500), 111 ReportedState: reportedState("app-2", 0), 112 }) 113 114 _, err = beginOp.Apply(ctx) 115 So(err, ShouldBeNil) 116 117 asset1 := fetch("apps/app-1") 118 asset2 := fetch("apps/app-2") 119 120 // Both assets are associated with the actuation in EXECUTING state. 121 So(asset1.LastActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING) 122 So(asset2.LastActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING) 123 124 // LastActuateActuation changes only for the stale asset. 125 So(asset1.LastActuateActuation.Id, ShouldEqual, "old-actuation") 126 So(asset2.LastActuateActuation.Id, ShouldEqual, "new-actuation") 127 So(asset2.LastActuateActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING) 128 129 // Finish the executing actuation. 130 actuation := &Actuation{ID: "new-actuation"} 131 So(datastore.Get(ctx, actuation), ShouldBeNil) 132 endOp, err := NewActuationEndOp(ctx, actuation) 133 So(err, ShouldBeNil) 134 endOp.UpdateActuationStatus(ctx, nil, "") 135 endOp.HandleActuatedState(ctx, "apps/app-2", &rpcpb.ActuatedAsset{ 136 State: reportedState("app-2", 500), 137 }) 138 So(endOp.Apply(ctx), ShouldBeNil) 139 140 asset1 = fetch("apps/app-1") 141 asset2 = fetch("apps/app-2") 142 143 // Both assets are associated with the actuation in SUCCEEDED state. 144 So(asset1.LastActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED) 145 So(asset2.LastActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED) 146 147 // LastActuateActuation changes only for the stale asset. 148 So(asset1.LastActuateActuation.Id, ShouldEqual, "old-actuation") 149 So(asset2.LastActuateActuation.Id, ShouldEqual, "new-actuation") 150 So(asset2.LastActuateActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED) 151 }) 152 153 Convey("Crashing actuation", func() { 154 store(&modelpb.Asset{ 155 Id: "apps/app", 156 AppliedState: intendedState("app", 0), 157 }) 158 159 beginOp, err := NewActuationBeginOp(ctx, []string{"apps/app"}, &modelpb.Actuation{ 160 Id: "actuation-0", 161 }) 162 So(err, ShouldBeNil) 163 164 // ACTUATE_STALE decision. 165 beginOp.MakeDecision(ctx, "apps/app", &rpcpb.AssetToActuate{ 166 Config: &modelpb.AssetConfig{EnableAutomation: true}, 167 IntendedState: intendedState("app", 500), 168 ReportedState: reportedState("app", 0), 169 }) 170 _, err = beginOp.Apply(ctx) 171 So(err, ShouldBeNil) 172 173 // The asset points to this actuation. 174 asset := fetch("apps/app") 175 So(asset.LastActuation.Id, ShouldEqual, "actuation-0") 176 So(asset.LastActuateActuation.Id, ShouldEqual, "actuation-0") 177 178 // Another actuation starts before the previous one finishes. 179 beginOp, err = NewActuationBeginOp(ctx, []string{"apps/app"}, &modelpb.Actuation{ 180 Id: "actuation-1", 181 }) 182 So(err, ShouldBeNil) 183 184 // ACTUATE_STALE decision. 185 beginOp.MakeDecision(ctx, "apps/app", &rpcpb.AssetToActuate{ 186 Config: &modelpb.AssetConfig{EnableAutomation: true}, 187 IntendedState: intendedState("app", 500), 188 ReportedState: reportedState("app", 0), 189 }) 190 _, err = beginOp.Apply(ctx) 191 So(err, ShouldBeNil) 192 193 // The asset points to the new actuation. 194 asset = fetch("apps/app") 195 So(asset.LastActuation.Id, ShouldEqual, "actuation-1") 196 So(asset.LastActuateActuation.Id, ShouldEqual, "actuation-1") 197 198 // There's a history log that points to the crashed actuation. 199 hist := history("apps/app", 1) 200 So(hist.Actuation.Id, ShouldEqual, "actuation-0") 201 So(hist.Actuation.State, ShouldEqual, modelpb.Actuation_EXPIRED) 202 203 // Finish the stale actuation, it should be a noop. 204 actuation := &Actuation{ID: "actuation-0"} 205 So(datastore.Get(ctx, actuation), ShouldBeNil) 206 endOp, err := NewActuationEndOp(ctx, actuation) 207 So(err, ShouldBeNil) 208 endOp.UpdateActuationStatus(ctx, nil, "") 209 endOp.HandleActuatedState(ctx, "apps/app", &rpcpb.ActuatedAsset{ 210 State: reportedState("app", 777), 211 }) 212 So(endOp.Apply(ctx), ShouldBeNil) 213 214 // No new history entries. 215 So(history("apps/app", 2), ShouldBeNil) 216 217 // Finish the active actuation. 218 actuation = &Actuation{ID: "actuation-1"} 219 So(datastore.Get(ctx, actuation), ShouldBeNil) 220 endOp, err = NewActuationEndOp(ctx, actuation) 221 So(err, ShouldBeNil) 222 endOp.UpdateActuationStatus(ctx, nil, "") 223 endOp.HandleActuatedState(ctx, "apps/app", &rpcpb.ActuatedAsset{ 224 State: reportedState("app", 500), 225 }) 226 So(endOp.Apply(ctx), ShouldBeNil) 227 228 // Have the new history log entry. 229 hist = history("apps/app", 2) 230 So(hist.Actuation.Id, ShouldEqual, "actuation-1") 231 So(hist.Actuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED) 232 }) 233 }) 234 }