gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/snapstate/storehelpers.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2018 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 21 22 import ( 23 "context" 24 "fmt" 25 "sort" 26 27 "github.com/snapcore/snapd/asserts/snapasserts" 28 "github.com/snapcore/snapd/logger" 29 "github.com/snapcore/snapd/overlord/auth" 30 "github.com/snapcore/snapd/overlord/state" 31 "github.com/snapcore/snapd/snap" 32 "github.com/snapcore/snapd/store" 33 "github.com/snapcore/snapd/strutil" 34 ) 35 36 var currentSnaps = currentSnapsImpl 37 38 // EnforcedValidationSets allows to hook getting of validation sets in enforce 39 // mode into installation/refresh/removal of snaps. It gets hooked from 40 // assertstate. 41 var EnforcedValidationSets func(st *state.State) (*snapasserts.ValidationSets, error) 42 43 func userIDForSnap(st *state.State, snapst *SnapState, fallbackUserID int) (int, error) { 44 userID := snapst.UserID 45 _, err := auth.User(st, userID) 46 if err == nil { 47 return userID, nil 48 } 49 if err != auth.ErrInvalidUser { 50 return 0, err 51 } 52 return fallbackUserID, nil 53 } 54 55 // userFromUserID returns the first valid user from a series of userIDs 56 // used as successive fallbacks. 57 func userFromUserID(st *state.State, userIDs ...int) (*auth.UserState, error) { 58 var user *auth.UserState 59 var err error 60 for _, userID := range userIDs { 61 if userID == 0 { 62 err = nil 63 continue 64 } 65 user, err = auth.User(st, userID) 66 if err != auth.ErrInvalidUser { 67 break 68 } 69 } 70 return user, err 71 } 72 73 func refreshOptions(st *state.State, origOpts *store.RefreshOptions) (*store.RefreshOptions, error) { 74 var opts store.RefreshOptions 75 76 if origOpts != nil { 77 if origOpts.PrivacyKey != "" { 78 // nothing to add 79 return origOpts, nil 80 } 81 opts = *origOpts 82 } 83 84 if err := st.Get("refresh-privacy-key", &opts.PrivacyKey); err != nil && err != state.ErrNoState { 85 return nil, fmt.Errorf("cannot obtain store request salt: %v", err) 86 } 87 if opts.PrivacyKey == "" { 88 return nil, fmt.Errorf("internal error: request salt is unset") 89 } 90 return &opts, nil 91 } 92 93 // installSize returns total download size of snaps and their prerequisites 94 // (bases and default content providers), querying the store as neccessarry, 95 // potentially more than once. It assumes the initial list of snaps already has 96 // download infos set. 97 // The state must be locked by the caller. 98 var installSize = func(st *state.State, snaps []minimalInstallInfo, userID int) (uint64, error) { 99 curSnaps, err := currentSnaps(st) 100 if err != nil { 101 return 0, err 102 } 103 104 user, err := userFromUserID(st, userID) 105 if err != nil { 106 return 0, err 107 } 108 109 accountedSnaps := map[string]bool{} 110 for _, snap := range curSnaps { 111 accountedSnaps[snap.InstanceName] = true 112 } 113 114 var prereqs []string 115 116 resolveBaseAndContentProviders := func(inst minimalInstallInfo) { 117 if inst.Type() != snap.TypeApp { 118 return 119 } 120 if inst.SnapBase() != "none" { 121 base := defaultCoreSnapName 122 if inst.SnapBase() != "" { 123 base = inst.SnapBase() 124 } 125 if !accountedSnaps[base] { 126 prereqs = append(prereqs, base) 127 accountedSnaps[base] = true 128 } 129 } 130 for _, snapName := range inst.Prereq(st) { 131 if !accountedSnaps[snapName] { 132 prereqs = append(prereqs, snapName) 133 accountedSnaps[snapName] = true 134 } 135 } 136 } 137 138 snapSizes := map[string]uint64{} 139 for _, inst := range snaps { 140 if inst.DownloadSize() == 0 { 141 return 0, fmt.Errorf("internal error: download info missing for %q", inst.InstanceName()) 142 } 143 snapSizes[inst.InstanceName()] = uint64(inst.DownloadSize()) 144 resolveBaseAndContentProviders(inst) 145 } 146 147 opts, err := refreshOptions(st, nil) 148 if err != nil { 149 return 0, err 150 } 151 152 theStore := Store(st, nil) 153 channel := defaultPrereqSnapsChannel() 154 155 // this can potentially be executed multiple times if we (recursively) 156 // find new prerequisites or bases. 157 for len(prereqs) > 0 { 158 actions := []*store.SnapAction{} 159 for _, prereq := range prereqs { 160 action := &store.SnapAction{ 161 Action: "install", 162 InstanceName: prereq, 163 Channel: channel, 164 } 165 actions = append(actions, action) 166 } 167 168 // calls to the store should be done without holding the state lock 169 st.Unlock() 170 results, _, err := theStore.SnapAction(context.TODO(), curSnaps, actions, nil, user, opts) 171 st.Lock() 172 if err != nil { 173 return 0, err 174 } 175 prereqs = []string{} 176 for _, res := range results { 177 snapSizes[res.InstanceName()] = uint64(res.Size) 178 // results may have new base or content providers 179 resolveBaseAndContentProviders(installSnapInfo{res.Info}) 180 } 181 } 182 183 // state is locked at this point 184 185 // since we unlock state above when querying store, other changes may affect 186 // same snaps, therefore obtain current snaps again and only compute total 187 // size of snaps that would actually need to be installed. 188 curSnaps, err = currentSnaps(st) 189 if err != nil { 190 return 0, err 191 } 192 for _, snap := range curSnaps { 193 delete(snapSizes, snap.InstanceName) 194 } 195 196 var total uint64 197 for _, sz := range snapSizes { 198 total += sz 199 } 200 201 return total, nil 202 } 203 204 func installInfo(ctx context.Context, st *state.State, name string, revOpts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (store.SnapActionResult, error) { 205 // TODO: support ignore-validation 206 207 curSnaps, err := currentSnaps(st) 208 if err != nil { 209 return store.SnapActionResult{}, err 210 } 211 212 user, err := userFromUserID(st, userID) 213 if err != nil { 214 return store.SnapActionResult{}, err 215 } 216 217 opts, err := refreshOptions(st, nil) 218 if err != nil { 219 return store.SnapActionResult{}, err 220 } 221 222 action := &store.SnapAction{ 223 Action: "install", 224 InstanceName: name, 225 } 226 227 // cannot specify both with the API 228 if revOpts.Revision.Unset() { 229 // the desired channel 230 action.Channel = revOpts.Channel 231 // the desired cohort key 232 action.CohortKey = revOpts.CohortKey 233 } else { 234 // the desired revision 235 action.Revision = revOpts.Revision 236 } 237 238 theStore := Store(st, deviceCtx) 239 st.Unlock() // calls to the store should be done without holding the state lock 240 res, _, err := theStore.SnapAction(ctx, curSnaps, []*store.SnapAction{action}, nil, user, opts) 241 st.Lock() 242 243 return singleActionResult(name, action.Action, res, err) 244 } 245 246 func updateInfo(st *state.State, snapst *SnapState, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*snap.Info, error) { 247 curSnaps, err := currentSnaps(st) 248 if err != nil { 249 return nil, err 250 } 251 252 refreshOpts, err := refreshOptions(st, nil) 253 if err != nil { 254 return nil, err 255 } 256 257 curInfo, user, err := preUpdateInfo(st, snapst, flags.Amend, userID) 258 if err != nil { 259 return nil, err 260 } 261 262 var storeFlags store.SnapActionFlags 263 if flags.IgnoreValidation { 264 storeFlags = store.SnapActionIgnoreValidation 265 } else { 266 storeFlags = store.SnapActionEnforceValidation 267 } 268 269 action := &store.SnapAction{ 270 Action: "refresh", 271 InstanceName: curInfo.InstanceName(), 272 SnapID: curInfo.SnapID, 273 // the desired channel 274 Channel: opts.Channel, 275 CohortKey: opts.CohortKey, 276 Flags: storeFlags, 277 } 278 279 if curInfo.SnapID == "" { // amend 280 action.Action = "install" 281 action.Epoch = curInfo.Epoch 282 } 283 284 theStore := Store(st, deviceCtx) 285 st.Unlock() // calls to the store should be done without holding the state lock 286 res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, refreshOpts) 287 st.Lock() 288 289 sar, err := singleActionResult(curInfo.InstanceName(), action.Action, res, err) 290 return sar.Info, err 291 } 292 293 func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) { 294 user, err := userFromUserID(st, snapst.UserID, userID) 295 if err != nil { 296 return nil, nil, err 297 } 298 299 curInfo, err := snapst.CurrentInfo() 300 if err != nil { 301 return nil, nil, err 302 } 303 304 if curInfo.SnapID == "" { // covers also trymode 305 if !amend { 306 return nil, nil, store.ErrLocalSnap 307 } 308 } 309 310 return curInfo, user, nil 311 } 312 313 var ErrMissingExpectedResult = fmt.Errorf("unexpectedly empty response from the server (try again later)") 314 315 func singleActionResult(name, action string, results []store.SnapActionResult, e error) (store.SnapActionResult, error) { 316 if len(results) > 1 { 317 return store.SnapActionResult{}, fmt.Errorf("internal error: multiple store results for a single snap op") 318 } 319 if len(results) > 0 { 320 // TODO: if we also have an error log/warn about it 321 return results[0], nil 322 } 323 324 if saErr, ok := e.(*store.SnapActionError); ok { 325 if len(saErr.Other) != 0 { 326 return store.SnapActionResult{}, saErr 327 } 328 329 var snapErr error 330 switch action { 331 case "refresh": 332 snapErr = saErr.Refresh[name] 333 case "install": 334 snapErr = saErr.Install[name] 335 } 336 if snapErr != nil { 337 return store.SnapActionResult{}, snapErr 338 } 339 340 // no result, atypical case 341 if saErr.NoResults { 342 return store.SnapActionResult{}, ErrMissingExpectedResult 343 } 344 } 345 346 return store.SnapActionResult{}, e 347 } 348 349 func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, deviceCtx DeviceContext) (*snap.Info, error) { 350 // TODO: support ignore-validation? 351 352 curSnaps, err := currentSnaps(st) 353 if err != nil { 354 return nil, err 355 } 356 357 curInfo, user, err := preUpdateInfo(st, snapst, false, userID) 358 if err != nil { 359 return nil, err 360 } 361 362 opts, err := refreshOptions(st, nil) 363 if err != nil { 364 return nil, err 365 } 366 367 action := &store.SnapAction{ 368 Action: "refresh", 369 SnapID: curInfo.SnapID, 370 InstanceName: curInfo.InstanceName(), 371 // the desired revision 372 Revision: revision, 373 } 374 375 theStore := Store(st, deviceCtx) 376 st.Unlock() // calls to the store should be done without holding the state lock 377 res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, opts) 378 st.Lock() 379 380 sar, err := singleActionResult(curInfo.InstanceName(), action.Action, res, err) 381 return sar.Info, err 382 } 383 384 func currentSnapsImpl(st *state.State) ([]*store.CurrentSnap, error) { 385 snapStates, err := All(st) 386 if err != nil { 387 return nil, err 388 } 389 390 if len(snapStates) == 0 { 391 // no snaps installed, do not bother any further 392 return nil, nil 393 } 394 395 curSnaps := collectCurrentSnaps(snapStates, nil) 396 return curSnaps, nil 397 } 398 399 func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) { 400 curSnaps = make([]*store.CurrentSnap, 0, len(snapStates)) 401 402 for _, snapst := range snapStates { 403 if snapst.TryMode { 404 // try mode snaps are completely local and 405 // irrelevant for the operation 406 continue 407 } 408 409 snapInfo, err := snapst.CurrentInfo() 410 if err != nil { 411 continue 412 } 413 414 if snapInfo.SnapID == "" { 415 // the store won't be able to tell what this 416 // is and so cannot include it in the 417 // operation 418 continue 419 } 420 421 installed := &store.CurrentSnap{ 422 InstanceName: snapInfo.InstanceName(), 423 SnapID: snapInfo.SnapID, 424 // the desired channel (not snapInfo.Channel!) 425 TrackingChannel: snapst.TrackingChannel, 426 Revision: snapInfo.Revision, 427 RefreshedDate: revisionDate(snapInfo), 428 IgnoreValidation: snapst.IgnoreValidation, 429 Epoch: snapInfo.Epoch, 430 CohortKey: snapst.CohortKey, 431 } 432 curSnaps = append(curSnaps, installed) 433 434 if consider != nil { 435 consider(installed, snapst) 436 } 437 } 438 439 return curSnaps 440 } 441 442 func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) { 443 snapStates, err := All(st) 444 if err != nil { 445 return nil, nil, nil, err 446 } 447 448 opts, err = refreshOptions(st, opts) 449 if err != nil { 450 return nil, nil, nil, err 451 } 452 453 // check if we have this name at all 454 for _, name := range names { 455 if _, ok := snapStates[name]; !ok { 456 return nil, nil, nil, snap.NotInstalledError{Snap: name} 457 } 458 } 459 460 sort.Strings(names) 461 462 var fallbackID int 463 // normalize fallback user 464 if !user.HasStoreAuth() { 465 user = nil 466 } else { 467 fallbackID = user.ID 468 } 469 470 actionsByUserID := make(map[int][]*store.SnapAction) 471 stateByInstanceName := make(map[string]*SnapState, len(snapStates)) 472 ignoreValidationByInstanceName := make(map[string]bool) 473 nCands := 0 474 475 addCand := func(installed *store.CurrentSnap, snapst *SnapState) { 476 // FIXME: snaps that are not active are skipped for now 477 // until we know what we want to do 478 if !snapst.Active { 479 return 480 } 481 482 if len(names) == 0 && snapst.DevMode { 483 // no auto-refresh for devmode 484 return 485 } 486 487 if len(names) > 0 && !strutil.SortedListContains(names, installed.InstanceName) { 488 return 489 } 490 491 stateByInstanceName[installed.InstanceName] = snapst 492 493 if len(names) == 0 { 494 installed.Block = snapst.Block() 495 } 496 497 userID := snapst.UserID 498 if userID == 0 { 499 userID = fallbackID 500 } 501 actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{ 502 Action: "refresh", 503 SnapID: installed.SnapID, 504 InstanceName: installed.InstanceName, 505 }) 506 if snapst.IgnoreValidation { 507 ignoreValidationByInstanceName[installed.InstanceName] = true 508 } 509 nCands++ 510 } 511 // determine current snaps and collect candidates for refresh 512 curSnaps := collectCurrentSnaps(snapStates, addCand) 513 514 actionsForUser := make(map[*auth.UserState][]*store.SnapAction, len(actionsByUserID)) 515 noUserActions := actionsByUserID[0] 516 for userID, actions := range actionsByUserID { 517 if userID == 0 { 518 continue 519 } 520 u, err := userFromUserID(st, userID, 0) 521 if err != nil { 522 return nil, nil, nil, err 523 } 524 if u.HasStoreAuth() { 525 actionsForUser[u] = actions 526 } else { 527 noUserActions = append(noUserActions, actions...) 528 } 529 } 530 // coalesce if possible 531 if len(noUserActions) != 0 { 532 if len(actionsForUser) == 0 { 533 actionsForUser[nil] = noUserActions 534 } else { 535 // coalesce no user actions with one other user's 536 for u1, actions := range actionsForUser { 537 actionsForUser[u1] = append(actions, noUserActions...) 538 break 539 } 540 } 541 } 542 543 // TODO: possibly support a deviceCtx 544 theStore := Store(st, nil) 545 546 updates := make([]*snap.Info, 0, nCands) 547 for u, actions := range actionsForUser { 548 st.Unlock() 549 sarsForUser, _, err := theStore.SnapAction(ctx, curSnaps, actions, nil, u, opts) 550 st.Lock() 551 if err != nil { 552 saErr, ok := err.(*store.SnapActionError) 553 if !ok { 554 return nil, nil, nil, err 555 } 556 // TODO: use the warning infra here when we have it 557 logger.Noticef("%v", saErr) 558 } 559 560 for _, sar := range sarsForUser { 561 updates = append(updates, sar.Info) 562 } 563 } 564 565 return updates, stateByInstanceName, ignoreValidationByInstanceName, nil 566 } 567 568 func installCandidates(st *state.State, names []string, channel string, user *auth.UserState) ([]store.SnapActionResult, error) { 569 curSnaps, err := currentSnaps(st) 570 if err != nil { 571 return nil, err 572 } 573 574 opts, err := refreshOptions(st, nil) 575 if err != nil { 576 return nil, err 577 } 578 579 actions := make([]*store.SnapAction, len(names)) 580 for i, name := range names { 581 actions[i] = &store.SnapAction{ 582 Action: "install", 583 InstanceName: name, 584 // the desired channel 585 Channel: channel, 586 } 587 } 588 589 // TODO: possibly support a deviceCtx 590 theStore := Store(st, nil) 591 st.Unlock() // calls to the store should be done without holding the state lock 592 defer st.Lock() 593 results, _, err := theStore.SnapAction(context.TODO(), curSnaps, actions, nil, user, opts) 594 return results, err 595 }