github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/overlord/snapstate/autorefresh_gating_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package snapstate_test 21 22 import ( 23 "context" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "time" 29 30 . "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/interfaces" 34 "github.com/snapcore/snapd/interfaces/builtin" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/osutil" 37 "github.com/snapcore/snapd/overlord/auth" 38 "github.com/snapcore/snapd/overlord/configstate/config" 39 "github.com/snapcore/snapd/overlord/hookstate" 40 "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" 41 "github.com/snapcore/snapd/overlord/snapstate" 42 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 43 "github.com/snapcore/snapd/overlord/state" 44 "github.com/snapcore/snapd/release" 45 "github.com/snapcore/snapd/snap" 46 "github.com/snapcore/snapd/snap/snaptest" 47 "github.com/snapcore/snapd/store" 48 "github.com/snapcore/snapd/testutil" 49 ) 50 51 type autoRefreshGatingStore struct { 52 *fakeStore 53 refreshedSnaps []*snap.Info 54 } 55 56 type autorefreshGatingSuite struct { 57 testutil.BaseTest 58 state *state.State 59 repo *interfaces.Repository 60 store *autoRefreshGatingStore 61 } 62 63 var _ = Suite(&autorefreshGatingSuite{}) 64 65 func (s *autorefreshGatingSuite) SetUpTest(c *C) { 66 s.BaseTest.SetUpTest(c) 67 dirs.SetRootDir(c.MkDir()) 68 s.AddCleanup(func() { 69 dirs.SetRootDir("/") 70 }) 71 s.state = state.New(nil) 72 73 s.repo = interfaces.NewRepository() 74 for _, iface := range builtin.Interfaces() { 75 c.Assert(s.repo.AddInterface(iface), IsNil) 76 } 77 78 s.state.Lock() 79 defer s.state.Unlock() 80 ifacerepo.Replace(s.state, s.repo) 81 82 s.store = &autoRefreshGatingStore{fakeStore: &fakeStore{}} 83 snapstate.ReplaceStore(s.state, s.store) 84 s.state.Set("refresh-privacy-key", "privacy-key") 85 } 86 87 func (r *autoRefreshGatingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { 88 if assertQuery != nil { 89 panic("no assertion query support") 90 } 91 if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 { 92 panic("expected in test one action for each current snaps, and at least one snap") 93 } 94 for _, a := range actions { 95 if a.Action != "refresh" { 96 panic("expected refresh actions") 97 } 98 } 99 100 res := []store.SnapActionResult{} 101 for _, rs := range r.refreshedSnaps { 102 res = append(res, store.SnapActionResult{Info: rs}) 103 } 104 105 return res, nil, nil 106 } 107 108 func mockInstalledSnap(c *C, st *state.State, snapYaml string, hasHook bool) *snap.Info { 109 snapInfo := snaptest.MockSnap(c, string(snapYaml), &snap.SideInfo{ 110 Revision: snap.R(1), 111 }) 112 113 snapName := snapInfo.SnapName() 114 si := &snap.SideInfo{RealName: snapName, SnapID: "id", Revision: snap.R(1)} 115 snapstate.Set(st, snapName, &snapstate.SnapState{ 116 Active: true, 117 Sequence: []*snap.SideInfo{si}, 118 Current: si.Revision, 119 SnapType: string(snapInfo.Type()), 120 }) 121 122 if hasHook { 123 c.Assert(os.MkdirAll(snapInfo.HooksDir(), 0775), IsNil) 124 err := ioutil.WriteFile(filepath.Join(snapInfo.HooksDir(), "gate-auto-refresh"), nil, 0755) 125 c.Assert(err, IsNil) 126 } 127 return snapInfo 128 } 129 130 func mockLastRefreshed(c *C, st *state.State, refreshedTime string, snaps ...string) { 131 refreshed, err := time.Parse(time.RFC3339, refreshedTime) 132 c.Assert(err, IsNil) 133 for _, snapName := range snaps { 134 var snapst snapstate.SnapState 135 c.Assert(snapstate.Get(st, snapName, &snapst), IsNil) 136 snapst.LastRefreshTime = &refreshed 137 snapstate.Set(st, snapName, &snapst) 138 } 139 } 140 141 const baseSnapAyaml = `name: base-snap-a 142 type: base 143 ` 144 145 const snapAyaml = `name: snap-a 146 type: app 147 base: base-snap-a 148 ` 149 150 const baseSnapByaml = `name: base-snap-b 151 type: base 152 ` 153 154 const snapByaml = `name: snap-b 155 type: app 156 base: base-snap-b 157 version: 1 158 ` 159 160 const kernelYaml = `name: kernel 161 type: kernel 162 version: 1 163 ` 164 165 const gadget1Yaml = `name: gadget 166 type: gadget 167 version: 1 168 ` 169 170 const snapCyaml = `name: snap-c 171 type: app 172 version: 1 173 ` 174 175 const snapDyaml = `name: snap-d 176 type: app 177 version: 1 178 slots: 179 slot: desktop 180 ` 181 182 const snapEyaml = `name: snap-e 183 type: app 184 version: 1 185 base: other-base 186 plugs: 187 plug: desktop 188 ` 189 190 const snapFyaml = `name: snap-f 191 type: app 192 version: 1 193 plugs: 194 plug: desktop 195 ` 196 197 const snapGyaml = `name: snap-g 198 type: app 199 version: 1 200 base: other-base 201 plugs: 202 desktop: 203 mir: 204 ` 205 206 const coreYaml = `name: core 207 type: os 208 version: 1 209 slots: 210 desktop: 211 mir: 212 ` 213 214 const core18Yaml = `name: core18 215 type: os 216 version: 1 217 ` 218 219 const snapdYaml = `name: snapd 220 version: 1 221 type: snapd 222 slots: 223 desktop: 224 ` 225 226 func (s *autorefreshGatingSuite) TestHoldDurationLeft(c *C) { 227 now, err := time.Parse(time.RFC3339, "2021-06-03T10:00:00Z") 228 c.Assert(err, IsNil) 229 maxPostponement := time.Hour * 24 * 90 230 231 for i, tc := range []struct { 232 lastRefresh, firstHeld string 233 maxDuration string 234 expected string 235 }{ 236 { 237 "2021-05-03T10:00:00Z", // last refreshed (1 month ago) 238 "2021-06-03T10:00:00Z", // first held now 239 "48h", // max duration 240 "48h", // expected 241 }, 242 { 243 "2021-05-03T10:00:00Z", // last refreshed (1 month ago) 244 "2021-06-02T10:00:00Z", // first held (1 day ago) 245 "48h", // max duration 246 "24h", // expected 247 }, 248 { 249 "2021-05-03T10:00:00Z", // last refreshed (1 month ago) 250 "2021-06-01T10:00:00Z", // first held (2 days ago) 251 "48h", // max duration 252 "00h", // expected 253 }, 254 { 255 "2021-03-08T10:00:00Z", // last refreshed (almost 3 months ago) 256 "2021-06-01T10:00:00Z", // first held 257 "2160h", // max duration (90 days) 258 "72h", // expected 259 }, 260 { 261 "2021-03-04T10:00:00Z", // last refreshed 262 "2021-06-01T10:00:00Z", // first held (2 days ago) 263 "2160h", // max duration (90 days) 264 "-24h", // expected (refresh is 1 day overdue) 265 }, 266 { 267 "2021-06-01T10:00:00Z", // last refreshed (2 days ago) 268 "2021-06-03T10:00:00Z", // first held now 269 "2160h", // max duration (90 days) 270 "2112h", // expected (max minus 2 days) 271 }, 272 } { 273 lastRefresh, err := time.Parse(time.RFC3339, tc.lastRefresh) 274 c.Assert(err, IsNil) 275 firstHeld, err := time.Parse(time.RFC3339, tc.firstHeld) 276 c.Assert(err, IsNil) 277 maxDuration, err := time.ParseDuration(tc.maxDuration) 278 c.Assert(err, IsNil) 279 expected, err := time.ParseDuration(tc.expected) 280 c.Assert(err, IsNil) 281 282 left := snapstate.HoldDurationLeft(now, lastRefresh, firstHeld, maxDuration, maxPostponement) 283 c.Check(left, Equals, expected, Commentf("case #%d", i)) 284 } 285 } 286 287 func (s *autorefreshGatingSuite) TestLastRefreshedHelper(c *C) { 288 st := s.state 289 st.Lock() 290 defer st.Unlock() 291 292 inf := mockInstalledSnap(c, st, snapAyaml, false) 293 stat, err := os.Stat(inf.MountFile()) 294 c.Assert(err, IsNil) 295 296 refreshed, err := snapstate.LastRefreshed(st, "snap-a") 297 c.Assert(err, IsNil) 298 c.Check(refreshed, DeepEquals, stat.ModTime()) 299 300 t, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z") 301 c.Assert(err, IsNil) 302 303 var snapst snapstate.SnapState 304 c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil) 305 snapst.LastRefreshTime = &t 306 snapstate.Set(st, "snap-a", &snapst) 307 308 refreshed, err = snapstate.LastRefreshed(st, "snap-a") 309 c.Assert(err, IsNil) 310 c.Check(refreshed, DeepEquals, t) 311 } 312 313 func (s *autorefreshGatingSuite) TestHoldRefreshHelper(c *C) { 314 st := s.state 315 st.Lock() 316 defer st.Unlock() 317 318 restore := snapstate.MockTimeNow(func() time.Time { 319 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 320 c.Assert(err, IsNil) 321 return t 322 }) 323 defer restore() 324 325 mockInstalledSnap(c, st, snapAyaml, false) 326 mockInstalledSnap(c, st, snapByaml, false) 327 mockInstalledSnap(c, st, snapCyaml, false) 328 mockInstalledSnap(c, st, snapDyaml, false) 329 mockInstalledSnap(c, st, snapEyaml, false) 330 mockInstalledSnap(c, st, snapFyaml, false) 331 332 mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f") 333 334 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 335 // this could be merged with the above HoldRefresh call, but it's fine if 336 // done separately too. 337 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil) 338 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil) 339 c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil) 340 341 var gating map[string]map[string]*snapstate.HoldState 342 c.Assert(st.Get("snaps-hold", &gating), IsNil) 343 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 344 "snap-b": { 345 // holding of other snaps for maxOtherHoldDuration (48h) 346 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 347 }, 348 "snap-c": { 349 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 350 }, 351 "snap-e": { 352 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 353 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 354 }, 355 "snap-f": { 356 // holding self set for maxPostponement minus 1 day due to last refresh. 357 "snap-f": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"), 358 }, 359 }) 360 } 361 362 func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) { 363 st := s.state 364 st.Lock() 365 defer st.Unlock() 366 367 lastRefreshed := "2021-05-09T10:00:00Z" 368 now := "2021-05-10T10:00:00Z" 369 restore := snapstate.MockTimeNow(func() time.Time { 370 t, err := time.Parse(time.RFC3339, now) 371 c.Assert(err, IsNil) 372 return t 373 }) 374 defer restore() 375 376 mockInstalledSnap(c, st, snapAyaml, false) 377 mockInstalledSnap(c, st, snapByaml, false) 378 // snap-a was last refreshed yesterday 379 mockLastRefreshed(c, st, lastRefreshed, "snap-a") 380 381 // hold it for just a bit (10h) initially 382 hold := time.Hour * 10 383 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 384 var gating map[string]map[string]*snapstate.HoldState 385 c.Assert(st.Get("snaps-hold", &gating), IsNil) 386 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 387 "snap-a": { 388 "snap-b": snapstate.MockHoldState(now, "2021-05-10T20:00:00Z"), 389 }, 390 }) 391 392 // holding for a shorter time is fine too 393 hold = time.Hour * 5 394 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 395 c.Assert(st.Get("snaps-hold", &gating), IsNil) 396 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 397 "snap-a": { 398 "snap-b": snapstate.MockHoldState(now, "2021-05-10T15:00:00Z"), 399 }, 400 }) 401 402 oldNow := now 403 404 // a refresh on next day 405 now = "2021-05-11T08:00:00Z" 406 407 // default hold time requested 408 hold = 0 409 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 410 c.Assert(st.Get("snaps-hold", &gating), IsNil) 411 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 412 "snap-a": { 413 // maximum for holding other snaps, but taking into consideration 414 // firstHeld time = "2021-05-10T10:00:00". 415 "snap-b": snapstate.MockHoldState(oldNow, "2021-05-12T10:00:00Z"), 416 }, 417 }) 418 } 419 420 func (s *autorefreshGatingSuite) TestHoldRefreshHelperCloseToMaxPostponement(c *C) { 421 st := s.state 422 st.Lock() 423 defer st.Unlock() 424 425 lastRefreshedStr := "2021-01-01T10:00:00Z" 426 lastRefreshed, err := time.Parse(time.RFC3339, lastRefreshedStr) 427 c.Assert(err, IsNil) 428 // we are 1 day before maxPostponent 429 now := lastRefreshed.Add(89 * time.Hour * 24) 430 431 restore := snapstate.MockTimeNow(func() time.Time { return now }) 432 defer restore() 433 434 mockInstalledSnap(c, st, snapAyaml, false) 435 mockInstalledSnap(c, st, snapByaml, false) 436 mockLastRefreshed(c, st, lastRefreshedStr, "snap-a") 437 438 // request default hold time 439 var hold time.Duration 440 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 441 442 var gating map[string]map[string]*snapstate.HoldState 443 c.Assert(st.Get("snaps-hold", &gating), IsNil) 444 c.Assert(gating, HasLen, 1) 445 c.Check(gating["snap-a"]["snap-b"].HoldUntil.String(), DeepEquals, lastRefreshed.Add(90*time.Hour*24).String()) 446 } 447 448 func (s *autorefreshGatingSuite) TestHoldRefreshExplicitHoldTime(c *C) { 449 st := s.state 450 st.Lock() 451 defer st.Unlock() 452 453 now := "2021-05-10T10:00:00Z" 454 restore := snapstate.MockTimeNow(func() time.Time { 455 t, err := time.Parse(time.RFC3339, now) 456 c.Assert(err, IsNil) 457 return t 458 }) 459 defer restore() 460 461 mockInstalledSnap(c, st, snapAyaml, false) 462 mockInstalledSnap(c, st, snapByaml, false) 463 464 hold := time.Hour * 24 * 3 465 // holding self for 3 days 466 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil) 467 468 // snap-b holds snap-a for 1 day 469 hold = time.Hour * 24 470 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 471 472 var gating map[string]map[string]*snapstate.HoldState 473 c.Assert(st.Get("snaps-hold", &gating), IsNil) 474 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 475 "snap-a": { 476 "snap-a": snapstate.MockHoldState(now, "2021-05-13T10:00:00Z"), 477 "snap-b": snapstate.MockHoldState(now, "2021-05-11T10:00:00Z"), 478 }, 479 }) 480 } 481 482 func (s *autorefreshGatingSuite) TestHoldRefreshHelperErrors(c *C) { 483 st := s.state 484 st.Lock() 485 defer st.Unlock() 486 487 now := "2021-05-10T10:00:00Z" 488 restore := snapstate.MockTimeNow(func() time.Time { 489 t, err := time.Parse(time.RFC3339, now) 490 c.Assert(err, IsNil) 491 return t 492 }) 493 defer restore() 494 495 mockInstalledSnap(c, st, snapAyaml, false) 496 mockInstalledSnap(c, st, snapByaml, false) 497 // snap-b was refreshed a few days ago 498 mockLastRefreshed(c, st, "2021-05-01T10:00:00Z", "snap-b") 499 500 // holding itself 501 hold := time.Hour * 24 * 96 502 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-a" of 2304h0m0s by snap "snap-a" exceeds maximum holding time`) 503 504 // holding other snap 505 hold = time.Hour * 49 506 err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b") 507 c.Check(err, ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`) 508 herr, ok := err.(*snapstate.HoldError) 509 c.Assert(ok, Equals, true) 510 c.Check(herr.SnapsInError, DeepEquals, map[string]snapstate.HoldDurationError{ 511 "snap-b": { 512 Err: fmt.Errorf(`requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`), 513 DurationLeft: 48 * time.Hour, 514 }, 515 }) 516 517 // hold for maximum allowed for other snaps 518 hold = time.Hour * 48 519 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil) 520 // 2 days passed since it was first held 521 now = "2021-05-12T10:00:00Z" 522 hold = time.Minute * 2 523 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) 524 525 // refreshed long time ago (> maxPostponement) 526 mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b") 527 hold = time.Hour * 2 528 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) 529 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) 530 } 531 532 func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) { 533 st := s.state 534 st.Lock() 535 defer st.Unlock() 536 537 mockInstalledSnap(c, st, snapAyaml, false) 538 mockInstalledSnap(c, st, snapByaml, false) 539 mockInstalledSnap(c, st, snapCyaml, false) 540 mockInstalledSnap(c, st, snapDyaml, false) 541 542 mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-b", "snap-c", "snap-d") 543 544 restore := snapstate.MockTimeNow(func() time.Time { 545 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 546 c.Assert(err, IsNil) 547 return t 548 }) 549 defer restore() 550 551 // nothing is held initially 552 held, err := snapstate.HeldSnaps(st) 553 c.Assert(err, IsNil) 554 c.Check(held, IsNil) 555 556 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 557 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil) 558 // holding self 559 c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil) 560 561 held, err = snapstate.HeldSnaps(st) 562 c.Assert(err, IsNil) 563 c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true}) 564 565 c.Assert(snapstate.ProceedWithRefresh(st, "snap-a"), IsNil) 566 567 held, err = snapstate.HeldSnaps(st) 568 c.Assert(err, IsNil) 569 c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true}) 570 571 c.Assert(snapstate.ProceedWithRefresh(st, "snap-d"), IsNil) 572 held, err = snapstate.HeldSnaps(st) 573 c.Assert(err, IsNil) 574 c.Check(held, IsNil) 575 } 576 577 func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) { 578 st := s.state 579 st.Lock() 580 defer st.Unlock() 581 582 restore := snapstate.MockTimeNow(func() time.Time { 583 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 584 c.Assert(err, IsNil) 585 return t 586 }) 587 defer restore() 588 589 mockInstalledSnap(c, st, snapAyaml, false) 590 mockInstalledSnap(c, st, snapByaml, false) 591 mockInstalledSnap(c, st, snapCyaml, false) 592 mockInstalledSnap(c, st, snapDyaml, false) 593 594 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 595 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) 596 597 c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil) 598 var gating map[string]map[string]*snapstate.HoldState 599 c.Assert(st.Get("snaps-hold", &gating), IsNil) 600 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 601 "snap-d": { 602 // holding self set for maxPostponement (95 days - buffer = 90 days) 603 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"), 604 }, 605 }) 606 607 held, err := snapstate.HeldSnaps(st) 608 c.Assert(err, IsNil) 609 c.Check(held, DeepEquals, map[string]bool{"snap-d": true}) 610 } 611 612 const useHook = true 613 const noHook = false 614 615 func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) { 616 c.Assert(task.Kind(), Equals, "conditional-auto-refresh") 617 var snaps map[string]*snapstate.RefreshCandidate 618 c.Assert(task.Get("snaps", &snaps), IsNil) 619 c.Check(snaps, DeepEquals, expected) 620 } 621 622 func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) { 623 restore := release.MockOnClassic(true) 624 defer restore() 625 626 st := s.state 627 628 st.Lock() 629 defer st.Unlock() 630 mockInstalledSnap(c, s.state, snapAyaml, useHook) 631 baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook) 632 // unrelated snaps 633 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 634 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 635 636 c.Assert(s.repo.AddSnap(snapB), IsNil) 637 638 updates := []*snap.Info{baseSnapA} 639 affected, err := snapstate.AffectedByRefresh(st, updates) 640 c.Assert(err, IsNil) 641 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 642 "snap-a": { 643 Base: true, 644 AffectingSnaps: map[string]bool{ 645 "base-snap-a": true, 646 }}}) 647 } 648 649 func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) { 650 restore := release.MockOnClassic(true) 651 defer restore() 652 653 st := s.state 654 655 st.Lock() 656 defer st.Unlock() 657 snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook) 658 core := mockInstalledSnap(c, s.state, coreYaml, noHook) 659 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 660 661 c.Assert(s.repo.AddSnap(core), IsNil) 662 c.Assert(s.repo.AddSnap(snapB), IsNil) 663 c.Assert(s.repo.AddSnap(snapC), IsNil) 664 665 updates := []*snap.Info{core} 666 affected, err := snapstate.AffectedByRefresh(st, updates) 667 c.Assert(err, IsNil) 668 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 669 "snap-c": { 670 Base: true, 671 AffectingSnaps: map[string]bool{ 672 "core": true, 673 }}}) 674 } 675 676 func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) { 677 restore := release.MockOnClassic(true) 678 defer restore() 679 680 st := s.state 681 682 st.Lock() 683 defer st.Unlock() 684 kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook) 685 mockInstalledSnap(c, s.state, snapCyaml, useHook) 686 mockInstalledSnap(c, s.state, snapByaml, noHook) 687 688 updates := []*snap.Info{kernel} 689 affected, err := snapstate.AffectedByRefresh(st, updates) 690 c.Assert(err, IsNil) 691 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 692 "snap-c": { 693 Restart: true, 694 AffectingSnaps: map[string]bool{ 695 "kernel": true, 696 }}}) 697 } 698 699 func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) { 700 restore := release.MockOnClassic(true) 701 defer restore() 702 703 st := s.state 704 705 st.Lock() 706 defer st.Unlock() 707 kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook) 708 mockInstalledSnap(c, s.state, snapCyaml, useHook) 709 mockInstalledSnap(c, s.state, snapByaml, noHook) 710 711 updates := []*snap.Info{kernel} 712 affected, err := snapstate.AffectedByRefresh(st, updates) 713 c.Assert(err, IsNil) 714 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 715 "snap-c": { 716 Restart: true, 717 AffectingSnaps: map[string]bool{ 718 "gadget": true, 719 }}}) 720 } 721 722 func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) { 723 restore := release.MockOnClassic(true) 724 defer restore() 725 726 st := s.state 727 728 st.Lock() 729 defer st.Unlock() 730 731 snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook) 732 snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook) 733 // unrelated snap 734 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 735 736 c.Assert(s.repo.AddSnap(snapF), IsNil) 737 c.Assert(s.repo.AddSnap(snapD), IsNil) 738 c.Assert(s.repo.AddSnap(snapE), IsNil) 739 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}} 740 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 741 c.Assert(err, IsNil) 742 743 updates := []*snap.Info{snapD} 744 affected, err := snapstate.AffectedByRefresh(st, updates) 745 c.Assert(err, IsNil) 746 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 747 "snap-e": { 748 Restart: true, 749 AffectingSnaps: map[string]bool{ 750 "snap-d": true, 751 }}}) 752 } 753 754 func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) { 755 restore := release.MockOnClassic(true) 756 defer restore() 757 758 st := s.state 759 760 st.Lock() 761 defer st.Unlock() 762 763 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 764 core := mockInstalledSnap(c, s.state, coreYaml, noHook) 765 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 766 767 c.Assert(s.repo.AddSnap(snapG), IsNil) 768 c.Assert(s.repo.AddSnap(core), IsNil) 769 c.Assert(s.repo.AddSnap(snapB), IsNil) 770 771 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}} 772 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 773 c.Assert(err, IsNil) 774 775 updates := []*snap.Info{core} 776 affected, err := snapstate.AffectedByRefresh(st, updates) 777 c.Assert(err, IsNil) 778 c.Check(affected, HasLen, 0) 779 } 780 781 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) { 782 restore := release.MockOnClassic(true) 783 defer restore() 784 785 st := s.state 786 787 st.Lock() 788 defer st.Unlock() 789 790 snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook) 791 snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook) 792 // unrelated snap 793 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 794 795 c.Assert(s.repo.AddSnap(snapF), IsNil) 796 c.Assert(s.repo.AddSnap(snapD), IsNil) 797 c.Assert(s.repo.AddSnap(snapE), IsNil) 798 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}} 799 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 800 c.Assert(err, IsNil) 801 802 // snapE has a plug using mount backend and is refreshed, this affects slot of snap-d. 803 updates := []*snap.Info{snapE} 804 affected, err := snapstate.AffectedByRefresh(st, updates) 805 c.Assert(err, IsNil) 806 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 807 "snap-d": { 808 Restart: true, 809 AffectingSnaps: map[string]bool{ 810 "snap-e": true, 811 }}}) 812 } 813 814 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) { 815 restore := release.MockOnClassic(true) 816 defer restore() 817 818 st := s.state 819 820 st.Lock() 821 defer st.Unlock() 822 823 snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, useHook) 824 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 825 // unrelated snap 826 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 827 828 c.Assert(s.repo.AddSnap(snapF), IsNil) 829 c.Assert(s.repo.AddSnap(snapdSnap), IsNil) 830 c.Assert(s.repo.AddSnap(snapG), IsNil) 831 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}} 832 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 833 c.Assert(err, IsNil) 834 835 // snapE has a plug using mount backend, refreshing snapd affects snapE. 836 updates := []*snap.Info{snapdSnap} 837 affected, err := snapstate.AffectedByRefresh(st, updates) 838 c.Assert(err, IsNil) 839 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 840 "snap-g": { 841 Restart: true, 842 AffectingSnaps: map[string]bool{ 843 "snapd": true, 844 }}}) 845 } 846 847 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) { 848 restore := release.MockOnClassic(true) 849 defer restore() 850 851 st := s.state 852 853 st.Lock() 854 defer st.Unlock() 855 856 coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook) 857 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 858 859 c.Assert(s.repo.AddSnap(coreSnap), IsNil) 860 c.Assert(s.repo.AddSnap(snapG), IsNil) 861 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}} 862 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 863 c.Assert(err, IsNil) 864 865 // snapG has a plug using mount backend, refreshing core affects snapE. 866 updates := []*snap.Info{coreSnap} 867 affected, err := snapstate.AffectedByRefresh(st, updates) 868 c.Assert(err, IsNil) 869 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 870 "snap-g": { 871 Restart: true, 872 AffectingSnaps: map[string]bool{ 873 "core": true, 874 }}}) 875 } 876 877 func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) { 878 restore := release.MockOnClassic(false) 879 defer restore() 880 881 st := s.state 882 883 r := snapstatetest.MockDeviceModel(ModelWithBase("core18")) 884 defer r() 885 886 st.Lock() 887 defer st.Unlock() 888 mockInstalledSnap(c, s.state, snapAyaml, useHook) 889 mockInstalledSnap(c, s.state, snapByaml, useHook) 890 mockInstalledSnap(c, s.state, snapDyaml, useHook) 891 mockInstalledSnap(c, s.state, snapEyaml, useHook) 892 core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook) 893 894 updates := []*snap.Info{core18} 895 affected, err := snapstate.AffectedByRefresh(st, updates) 896 c.Assert(err, IsNil) 897 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 898 "snap-a": { 899 Base: false, 900 Restart: true, 901 AffectingSnaps: map[string]bool{ 902 "core18": true, 903 }, 904 }, 905 "snap-b": { 906 Base: false, 907 Restart: true, 908 AffectingSnaps: map[string]bool{ 909 "core18": true, 910 }, 911 }, 912 "snap-d": { 913 Base: false, 914 Restart: true, 915 AffectingSnaps: map[string]bool{ 916 "core18": true, 917 }, 918 }, 919 "snap-e": { 920 Base: false, 921 Restart: true, 922 AffectingSnaps: map[string]bool{ 923 "core18": true, 924 }}}) 925 } 926 927 func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) { 928 st := s.state 929 st.Lock() 930 defer st.Unlock() 931 932 affected := map[string]*snapstate.AffectedSnapInfo{ 933 "snap-a": { 934 Base: true, 935 Restart: true, 936 AffectingSnaps: map[string]bool{ 937 "snap-c": true, 938 "snap-d": true, 939 }, 940 }, 941 "snap-b": { 942 AffectingSnaps: map[string]bool{ 943 "snap-e": true, 944 "snap-f": true, 945 }, 946 }, 947 } 948 949 seenSnaps := make(map[string]bool) 950 951 ts := snapstate.CreateGateAutoRefreshHooks(st, affected) 952 c.Assert(ts.Tasks(), HasLen, 2) 953 954 checkHook := func(t *state.Task) { 955 c.Assert(t.Kind(), Equals, "run-hook") 956 var hs hookstate.HookSetup 957 c.Assert(t.Get("hook-setup", &hs), IsNil) 958 c.Check(hs.Hook, Equals, "gate-auto-refresh") 959 c.Check(hs.Optional, Equals, true) 960 seenSnaps[hs.Snap] = true 961 962 var data interface{} 963 c.Assert(t.Get("hook-context", &data), IsNil) 964 965 // the order of hook tasks is not deterministic 966 if hs.Snap == "snap-a" { 967 c.Check(data, DeepEquals, map[string]interface{}{ 968 "base": true, 969 "restart": true, 970 "affecting-snaps": []interface{}{"snap-c", "snap-d"}}) 971 } else { 972 c.Assert(hs.Snap, Equals, "snap-b") 973 c.Check(data, DeepEquals, map[string]interface{}{ 974 "base": false, 975 "restart": false, 976 "affecting-snaps": []interface{}{"snap-e", "snap-f"}}) 977 } 978 } 979 980 checkHook(ts.Tasks()[0]) 981 checkHook(ts.Tasks()[1]) 982 983 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true}) 984 } 985 986 func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) { 987 st := s.state 988 st.Lock() 989 defer st.Unlock() 990 991 st.Set("seeded", true) 992 993 restore := snapstatetest.MockDeviceModel(DefaultModel()) 994 defer restore() 995 996 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 997 return nil, nil 998 } 999 defer func() { snapstate.AutoAliases = nil }() 1000 1001 s.store.refreshedSnaps = []*snap.Info{{ 1002 Architectures: []string{"all"}, 1003 SnapType: snap.TypeApp, 1004 SideInfo: snap.SideInfo{ 1005 RealName: "snap-a", 1006 Revision: snap.R(8), 1007 }, 1008 }} 1009 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1010 1011 // gate-auto-refresh-hook feature not enabled, expect old-style refresh. 1012 _, tss, err := snapstate.AutoRefresh(context.TODO(), st) 1013 c.Check(err, IsNil) 1014 c.Assert(tss, HasLen, 2) 1015 c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites") 1016 c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap") 1017 c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh") 1018 1019 // enable gate-auto-refresh-hook feature 1020 tr := config.NewTransaction(s.state) 1021 tr.Set("core", "experimental.gate-auto-refresh-hook", true) 1022 tr.Commit() 1023 1024 _, tss, err = snapstate.AutoRefresh(context.TODO(), st) 1025 c.Check(err, IsNil) 1026 c.Assert(tss, HasLen, 2) 1027 // TODO: verify conditional-auto-refresh task data 1028 c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh") 1029 c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook") 1030 } 1031 1032 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) { 1033 s.store.refreshedSnaps = []*snap.Info{{ 1034 Architectures: []string{"all"}, 1035 SnapType: snap.TypeApp, 1036 SideInfo: snap.SideInfo{ 1037 RealName: "snap-a", 1038 Revision: snap.R(8), 1039 }, 1040 }, { 1041 Architectures: []string{"all"}, 1042 SnapType: snap.TypeBase, 1043 SideInfo: snap.SideInfo{ 1044 RealName: "base-snap-b", 1045 Revision: snap.R(3), 1046 }, 1047 }, { 1048 Architectures: []string{"all"}, 1049 SnapType: snap.TypeApp, 1050 SideInfo: snap.SideInfo{ 1051 RealName: "snap-c", 1052 Revision: snap.R(5), 1053 }, 1054 }} 1055 1056 st := s.state 1057 st.Lock() 1058 defer st.Unlock() 1059 1060 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1061 mockInstalledSnap(c, s.state, snapByaml, useHook) 1062 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1063 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1064 1065 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1066 defer restore() 1067 1068 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1069 c.Assert(err, IsNil) 1070 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"}) 1071 c.Assert(tss, HasLen, 2) 1072 1073 c.Assert(tss[0].Tasks(), HasLen, 1) 1074 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1075 "snap-a": { 1076 SnapSetup: snapstate.SnapSetup{ 1077 Type: "app", 1078 PlugsOnly: true, 1079 Flags: snapstate.Flags{ 1080 IsAutoRefresh: true, 1081 }, 1082 SideInfo: &snap.SideInfo{ 1083 RealName: "snap-a", 1084 Revision: snap.R(8), 1085 }, 1086 DownloadInfo: &snap.DownloadInfo{}, 1087 }, 1088 }, 1089 "base-snap-b": { 1090 SnapSetup: snapstate.SnapSetup{ 1091 Type: "base", 1092 PlugsOnly: true, 1093 Flags: snapstate.Flags{ 1094 IsAutoRefresh: true, 1095 }, 1096 SideInfo: &snap.SideInfo{ 1097 RealName: "base-snap-b", 1098 Revision: snap.R(3), 1099 }, 1100 DownloadInfo: &snap.DownloadInfo{}, 1101 }, 1102 }, 1103 "snap-c": { 1104 SnapSetup: snapstate.SnapSetup{ 1105 Type: "app", 1106 PlugsOnly: true, 1107 Flags: snapstate.Flags{ 1108 IsAutoRefresh: true, 1109 }, 1110 SideInfo: &snap.SideInfo{ 1111 RealName: "snap-c", 1112 Revision: snap.R(5), 1113 }, 1114 DownloadInfo: &snap.DownloadInfo{}, 1115 }, 1116 }, 1117 }) 1118 1119 c.Assert(tss[1].Tasks(), HasLen, 2) 1120 1121 // check hooks for affected snaps 1122 seenSnaps := make(map[string]bool) 1123 var hs hookstate.HookSetup 1124 c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil) 1125 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1126 seenSnaps[hs.Snap] = true 1127 1128 c.Assert(tss[1].Tasks()[1].Get("hook-setup", &hs), IsNil) 1129 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1130 seenSnaps[hs.Snap] = true 1131 1132 // hook for snap-a because it gets refreshed, for snap-b because its base 1133 // gets refreshed. snap-c is refreshed but doesn't have the hook. 1134 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true}) 1135 1136 // check that refresh-candidates in the state were updated 1137 var candidates map[string]*snapstate.RefreshCandidate 1138 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1139 c.Assert(candidates, HasLen, 3) 1140 c.Check(candidates["snap-a"], NotNil) 1141 c.Check(candidates["base-snap-b"], NotNil) 1142 c.Check(candidates["snap-c"], NotNil) 1143 } 1144 1145 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) { 1146 s.store.refreshedSnaps = []*snap.Info{{ 1147 Architectures: []string{"all"}, 1148 SnapType: snap.TypeApp, 1149 SideInfo: snap.SideInfo{ 1150 RealName: "snap-a", 1151 Revision: snap.R(8), 1152 }, 1153 }, { 1154 Architectures: []string{"all"}, 1155 SnapType: snap.TypeBase, 1156 SideInfo: snap.SideInfo{ 1157 RealName: "snap-c", 1158 Revision: snap.R(5), 1159 }, 1160 }} 1161 1162 st := s.state 1163 st.Lock() 1164 defer st.Unlock() 1165 1166 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1167 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1168 1169 conflictChange := st.NewChange("conflicting change", "") 1170 conflictTask := st.NewTask("conflicting task", "") 1171 si := &snap.SideInfo{ 1172 RealName: "snap-c", 1173 Revision: snap.R(1), 1174 } 1175 sup := snapstate.SnapSetup{SideInfo: si} 1176 conflictTask.Set("snap-setup", sup) 1177 conflictChange.AddTask(conflictTask) 1178 1179 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1180 defer restore() 1181 1182 logbuf, restoreLogger := logger.MockLogger() 1183 defer restoreLogger() 1184 1185 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1186 c.Assert(err, IsNil) 1187 c.Check(names, DeepEquals, []string{"snap-a"}) 1188 c.Assert(tss, HasLen, 2) 1189 1190 c.Assert(tss[0].Tasks(), HasLen, 1) 1191 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1192 "snap-a": { 1193 SnapSetup: snapstate.SnapSetup{ 1194 Type: "app", 1195 PlugsOnly: true, 1196 Flags: snapstate.Flags{ 1197 IsAutoRefresh: true, 1198 }, 1199 SideInfo: &snap.SideInfo{ 1200 RealName: "snap-a", 1201 Revision: snap.R(8), 1202 }, 1203 DownloadInfo: &snap.DownloadInfo{}, 1204 }}}) 1205 1206 c.Assert(tss[1].Tasks(), HasLen, 1) 1207 1208 c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`) 1209 1210 seenSnaps := make(map[string]bool) 1211 var hs hookstate.HookSetup 1212 c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil) 1213 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1214 seenSnaps[hs.Snap] = true 1215 1216 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true}) 1217 1218 // check that refresh-candidates in the state were updated 1219 var candidates map[string]*snapstate.RefreshCandidate 1220 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1221 c.Assert(candidates, HasLen, 2) 1222 c.Check(candidates["snap-a"], NotNil) 1223 c.Check(candidates["snap-c"], NotNil) 1224 } 1225 1226 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) { 1227 s.store.refreshedSnaps = []*snap.Info{{ 1228 Architectures: []string{"all"}, 1229 SnapType: snap.TypeBase, 1230 SideInfo: snap.SideInfo{ 1231 RealName: "base-snap-b", 1232 Revision: snap.R(3), 1233 }, 1234 }, { 1235 Architectures: []string{"all"}, 1236 SnapType: snap.TypeBase, 1237 SideInfo: snap.SideInfo{ 1238 RealName: "snap-c", 1239 Revision: snap.R(5), 1240 }, 1241 }} 1242 1243 st := s.state 1244 st.Lock() 1245 defer st.Unlock() 1246 1247 mockInstalledSnap(c, s.state, snapByaml, noHook) 1248 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1249 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1250 1251 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1252 defer restore() 1253 1254 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1255 c.Assert(err, IsNil) 1256 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"}) 1257 c.Assert(tss, HasLen, 1) 1258 1259 c.Assert(tss[0].Tasks(), HasLen, 1) 1260 c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh") 1261 } 1262 1263 func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) { 1264 info := &snap.Info{ 1265 SuggestedName: name, 1266 SideInfo: *si, 1267 Architectures: []string{"all"}, 1268 SnapType: snap.TypeApp, 1269 Epoch: snap.Epoch{}, 1270 } 1271 switch name { 1272 case "base-snap-b": 1273 info.SnapType = snap.TypeBase 1274 case "snap-a", "snap-b": 1275 info.Hooks = map[string]*snap.HookInfo{ 1276 "gate-auto-refresh": { 1277 Name: "gate-auto-refresh", 1278 Snap: info, 1279 }, 1280 } 1281 if name == "snap-b" { 1282 info.Base = "base-snap-b" 1283 } 1284 } 1285 return info, nil 1286 } 1287 1288 func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) { 1289 st := s.state 1290 st.Lock() 1291 defer st.Unlock() 1292 1293 restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) { 1294 c.Fatal("unexpected call to installSize") 1295 return 0, nil 1296 }) 1297 defer restoreInstallSize() 1298 1299 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1300 fakeStore: s.fakeStore, 1301 refreshedSnaps: []*snap.Info{{ 1302 Architectures: []string{"all"}, 1303 SnapType: snap.TypeApp, 1304 SideInfo: snap.SideInfo{ 1305 RealName: "snap-a", 1306 Revision: snap.R(8), 1307 }, 1308 }, { 1309 Architectures: []string{"all"}, 1310 SnapType: snap.TypeBase, 1311 SideInfo: snap.SideInfo{ 1312 RealName: "base-snap-b", 1313 Revision: snap.R(3), 1314 }, 1315 }}}) 1316 1317 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1318 mockInstalledSnap(c, s.state, snapByaml, useHook) 1319 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1320 1321 snapstate.MockSnapReadInfo(fakeReadInfo) 1322 1323 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1324 defer restore() 1325 1326 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1327 c.Assert(err, IsNil) 1328 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1329 1330 chg := s.state.NewChange("refresh", "...") 1331 for _, ts := range tss { 1332 chg.AddAll(ts) 1333 } 1334 1335 s.state.Unlock() 1336 defer s.se.Stop() 1337 s.settle(c) 1338 s.state.Lock() 1339 1340 c.Check(chg.Status(), Equals, state.DoneStatus) 1341 c.Check(chg.Err(), IsNil) 1342 1343 expected := []string{ 1344 "conditional-auto-refresh", 1345 "run-hook [snap-a;gate-auto-refresh]", 1346 // snap-b hook is triggered because of base-snap-b refresh 1347 "run-hook [snap-b;gate-auto-refresh]", 1348 "prerequisites", 1349 "download-snap", 1350 "validate-snap", 1351 "mount-snap", 1352 "run-hook [base-snap-b;pre-refresh]", 1353 "stop-snap-services", 1354 "remove-aliases", 1355 "unlink-current-snap", 1356 "copy-snap-data", 1357 "setup-profiles", 1358 "link-snap", 1359 "auto-connect", 1360 "set-auto-aliases", 1361 "setup-aliases", 1362 "run-hook [base-snap-b;post-refresh]", 1363 "start-snap-services", 1364 "cleanup", 1365 "run-hook [base-snap-b;check-health]", 1366 "prerequisites", 1367 "download-snap", 1368 "validate-snap", 1369 "mount-snap", 1370 "run-hook [snap-a;pre-refresh]", 1371 "stop-snap-services", 1372 "remove-aliases", 1373 "unlink-current-snap", 1374 "copy-snap-data", 1375 "setup-profiles", 1376 "link-snap", 1377 "auto-connect", 1378 "set-auto-aliases", 1379 "setup-aliases", 1380 "run-hook [snap-a;post-refresh]", 1381 "start-snap-services", 1382 "cleanup", 1383 "run-hook [snap-a;configure]", 1384 "run-hook [snap-a;check-health]", 1385 "check-rerefresh", 1386 } 1387 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 1388 } 1389 1390 func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) { 1391 st := s.state 1392 st.Lock() 1393 defer st.Unlock() 1394 1395 restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error { 1396 c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123)) 1397 if fail { 1398 return &osutil.NotEnoughDiskSpaceError{} 1399 } 1400 return nil 1401 }) 1402 defer restore() 1403 1404 var installSizeCalled bool 1405 restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) { 1406 installSizeCalled = true 1407 seen := map[string]bool{} 1408 for _, sn := range snaps { 1409 seen[sn.InstanceName()] = true 1410 } 1411 c.Check(seen, DeepEquals, map[string]bool{ 1412 "base-snap-b": true, 1413 "snap-a": true, 1414 }) 1415 return 123, nil 1416 }) 1417 defer restoreInstallSize() 1418 1419 restoreModel := snapstatetest.MockDeviceModel(DefaultModel()) 1420 defer restoreModel() 1421 1422 tr := config.NewTransaction(s.state) 1423 tr.Set("core", "experimental.check-disk-space-refresh", true) 1424 tr.Commit() 1425 1426 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1427 fakeStore: s.fakeStore, 1428 refreshedSnaps: []*snap.Info{{ 1429 Architectures: []string{"all"}, 1430 SnapType: snap.TypeApp, 1431 SideInfo: snap.SideInfo{ 1432 RealName: "snap-a", 1433 Revision: snap.R(8), 1434 }, 1435 }, { 1436 Architectures: []string{"all"}, 1437 SnapType: snap.TypeBase, 1438 SideInfo: snap.SideInfo{ 1439 RealName: "base-snap-b", 1440 Revision: snap.R(3), 1441 }, 1442 }}}) 1443 1444 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1445 mockInstalledSnap(c, s.state, snapByaml, useHook) 1446 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1447 1448 snapstate.MockSnapReadInfo(fakeReadInfo) 1449 1450 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1451 c.Assert(err, IsNil) 1452 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1453 1454 chg := s.state.NewChange("refresh", "...") 1455 for _, ts := range tss { 1456 chg.AddAll(ts) 1457 } 1458 1459 s.state.Unlock() 1460 defer s.se.Stop() 1461 s.settle(c) 1462 s.state.Lock() 1463 1464 c.Check(installSizeCalled, Equals, true) 1465 if fail { 1466 c.Check(chg.Status(), Equals, state.ErrorStatus) 1467 c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`) 1468 } else { 1469 c.Check(chg.Status(), Equals, state.DoneStatus) 1470 c.Check(chg.Err(), IsNil) 1471 } 1472 } 1473 1474 func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) { 1475 fail := true 1476 s.testAutoRefreshPhase2DiskSpaceCheck(c, fail) 1477 } 1478 1479 func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) { 1480 var nofail bool 1481 s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail) 1482 } 1483 1484 // XXX: this case is probably artificial; with proper conflict prevention 1485 // we shouldn't get conflicts from doInstall in phase2. 1486 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) { 1487 st := s.state 1488 st.Lock() 1489 defer st.Unlock() 1490 1491 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1492 fakeStore: s.fakeStore, 1493 refreshedSnaps: []*snap.Info{{ 1494 Architectures: []string{"all"}, 1495 SnapType: snap.TypeApp, 1496 SideInfo: snap.SideInfo{ 1497 RealName: "snap-a", 1498 Revision: snap.R(8), 1499 }, 1500 }, { 1501 Architectures: []string{"all"}, 1502 SnapType: snap.TypeBase, 1503 SideInfo: snap.SideInfo{ 1504 RealName: "base-snap-b", 1505 Revision: snap.R(3), 1506 }, 1507 }}}) 1508 1509 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1510 mockInstalledSnap(c, s.state, snapByaml, useHook) 1511 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1512 1513 snapstate.MockSnapReadInfo(fakeReadInfo) 1514 1515 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1516 defer restore() 1517 1518 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1519 c.Assert(err, IsNil) 1520 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1521 1522 chg := s.state.NewChange("refresh", "...") 1523 for _, ts := range tss { 1524 chg.AddAll(ts) 1525 } 1526 1527 conflictChange := st.NewChange("conflicting change", "") 1528 conflictTask := st.NewTask("conflicting task", "") 1529 si := &snap.SideInfo{ 1530 RealName: "snap-a", 1531 Revision: snap.R(1), 1532 } 1533 sup := snapstate.SnapSetup{SideInfo: si} 1534 conflictTask.Set("snap-setup", sup) 1535 conflictChange.AddTask(conflictTask) 1536 conflictTask.WaitFor(tss[0].Tasks()[0]) 1537 1538 s.state.Unlock() 1539 defer s.se.Stop() 1540 s.settle(c) 1541 s.state.Lock() 1542 1543 c.Assert(chg.Status(), Equals, state.DoneStatus) 1544 c.Check(chg.Err(), IsNil) 1545 1546 expected := []string{ 1547 "conditional-auto-refresh", 1548 "run-hook [snap-a;gate-auto-refresh]", 1549 // snap-b hook is triggered because of base-snap-b refresh 1550 "run-hook [snap-b;gate-auto-refresh]", 1551 "prerequisites", 1552 "download-snap", 1553 "validate-snap", 1554 "mount-snap", 1555 "run-hook [base-snap-b;pre-refresh]", 1556 "stop-snap-services", 1557 "remove-aliases", 1558 "unlink-current-snap", 1559 "copy-snap-data", 1560 "setup-profiles", 1561 "link-snap", 1562 "auto-connect", 1563 "set-auto-aliases", 1564 "setup-aliases", 1565 "run-hook [base-snap-b;post-refresh]", 1566 "start-snap-services", 1567 "cleanup", 1568 "run-hook [base-snap-b;check-health]", 1569 "check-rerefresh", 1570 } 1571 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 1572 } 1573 1574 func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) { 1575 st := s.state 1576 st.Lock() 1577 defer st.Unlock() 1578 1579 restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) { 1580 c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh") 1581 var candidates map[string]*snapstate.RefreshCandidate 1582 c.Assert(gatingTask.Get("snaps", &candidates), IsNil) 1583 seenSnaps := make(map[string]bool) 1584 var filteredByGatingHooks []*snapstate.RefreshCandidate 1585 for _, cand := range candidates { 1586 seenSnaps[cand.InstanceName()] = true 1587 if cand.InstanceName() == "snap-a" { 1588 continue 1589 } 1590 filteredByGatingHooks = append(filteredByGatingHooks, cand) 1591 } 1592 c.Check(seenSnaps, DeepEquals, map[string]bool{ 1593 "snap-a": true, 1594 "base-snap-b": true, 1595 }) 1596 return filteredByGatingHooks, nil 1597 }) 1598 defer restore() 1599 1600 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1601 fakeStore: s.fakeStore, 1602 refreshedSnaps: []*snap.Info{ 1603 { 1604 Architectures: []string{"all"}, 1605 SnapType: snap.TypeApp, 1606 SideInfo: snap.SideInfo{ 1607 RealName: "snap-a", 1608 Revision: snap.R(8), 1609 }, 1610 }, { 1611 Architectures: []string{"all"}, 1612 SnapType: snap.TypeBase, 1613 SideInfo: snap.SideInfo{ 1614 RealName: "base-snap-b", 1615 Revision: snap.R(3), 1616 }, 1617 }, 1618 }}) 1619 1620 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1621 mockInstalledSnap(c, s.state, snapByaml, useHook) 1622 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1623 1624 snapstate.MockSnapReadInfo(fakeReadInfo) 1625 1626 restoreModel := snapstatetest.MockDeviceModel(DefaultModel()) 1627 defer restoreModel() 1628 1629 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st) 1630 c.Assert(err, IsNil) 1631 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1632 1633 chg := s.state.NewChange("refresh", "...") 1634 for _, ts := range tss { 1635 chg.AddAll(ts) 1636 } 1637 1638 s.state.Unlock() 1639 defer s.se.Stop() 1640 s.settle(c) 1641 s.state.Lock() 1642 1643 c.Assert(chg.Status(), Equals, state.DoneStatus) 1644 c.Check(chg.Err(), IsNil) 1645 1646 expected := []string{ 1647 "conditional-auto-refresh", 1648 "run-hook [snap-a;gate-auto-refresh]", 1649 // snap-b hook is triggered because of base-snap-b refresh 1650 "run-hook [snap-b;gate-auto-refresh]", 1651 "prerequisites", 1652 "download-snap", 1653 "validate-snap", 1654 "mount-snap", 1655 "run-hook [base-snap-b;pre-refresh]", 1656 "stop-snap-services", 1657 "remove-aliases", 1658 "unlink-current-snap", 1659 "copy-snap-data", 1660 "setup-profiles", 1661 "link-snap", 1662 "auto-connect", 1663 "set-auto-aliases", 1664 "setup-aliases", 1665 "run-hook [base-snap-b;post-refresh]", 1666 "start-snap-services", 1667 "cleanup", 1668 "run-hook [base-snap-b;check-health]", 1669 "check-rerefresh", 1670 } 1671 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 1672 } 1673 1674 func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) { 1675 for i, t := range tasks { 1676 var got string 1677 if t.Kind() == "run-hook" { 1678 var hsup hookstate.HookSetup 1679 c.Assert(t.Get("hook-setup", &hsup), IsNil) 1680 got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook) 1681 } else { 1682 got = t.Kind() 1683 } 1684 c.Assert(got, Equals, expected[i]) 1685 } 1686 }