github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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/logger" 28 "github.com/snapcore/snapd/overlord/auth" 29 "github.com/snapcore/snapd/overlord/state" 30 "github.com/snapcore/snapd/snap" 31 "github.com/snapcore/snapd/store" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 func userIDForSnap(st *state.State, snapst *SnapState, fallbackUserID int) (int, error) { 36 userID := snapst.UserID 37 _, err := auth.User(st, userID) 38 if err == nil { 39 return userID, nil 40 } 41 if err != auth.ErrInvalidUser { 42 return 0, err 43 } 44 return fallbackUserID, nil 45 } 46 47 // userFromUserID returns the first valid user from a series of userIDs 48 // used as successive fallbacks. 49 func userFromUserID(st *state.State, userIDs ...int) (*auth.UserState, error) { 50 var user *auth.UserState 51 var err error 52 for _, userID := range userIDs { 53 if userID == 0 { 54 err = nil 55 continue 56 } 57 user, err = auth.User(st, userID) 58 if err != auth.ErrInvalidUser { 59 break 60 } 61 } 62 return user, err 63 } 64 65 func refreshOptions(st *state.State, origOpts *store.RefreshOptions) (*store.RefreshOptions, error) { 66 var opts store.RefreshOptions 67 68 if origOpts != nil { 69 if origOpts.PrivacyKey != "" { 70 // nothing to add 71 return origOpts, nil 72 } 73 opts = *origOpts 74 } 75 76 if err := st.Get("refresh-privacy-key", &opts.PrivacyKey); err != nil && err != state.ErrNoState { 77 return nil, fmt.Errorf("cannot obtain store request salt: %v", err) 78 } 79 if opts.PrivacyKey == "" { 80 return nil, fmt.Errorf("internal error: request salt is unset") 81 } 82 return &opts, nil 83 } 84 85 func installInfo(ctx context.Context, st *state.State, name string, revOpts *RevisionOptions, userID int, deviceCtx DeviceContext) (*snap.Info, error) { 86 // TODO: support ignore-validation? 87 88 curSnaps, err := currentSnaps(st) 89 if err != nil { 90 return nil, err 91 } 92 93 user, err := userFromUserID(st, userID) 94 if err != nil { 95 return nil, err 96 } 97 98 opts, err := refreshOptions(st, nil) 99 if err != nil { 100 return nil, err 101 } 102 103 action := &store.SnapAction{ 104 Action: "install", 105 InstanceName: name, 106 } 107 108 // cannot specify both with the API 109 if revOpts.Revision.Unset() { 110 // the desired channel 111 action.Channel = revOpts.Channel 112 // the desired cohort key 113 action.CohortKey = revOpts.CohortKey 114 } else { 115 // the desired revision 116 action.Revision = revOpts.Revision 117 } 118 119 theStore := Store(st, deviceCtx) 120 st.Unlock() // calls to the store should be done without holding the state lock 121 res, err := theStore.SnapAction(ctx, curSnaps, []*store.SnapAction{action}, user, opts) 122 st.Lock() 123 124 return singleActionResult(name, action.Action, res, err) 125 } 126 127 func updateInfo(st *state.State, snapst *SnapState, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*snap.Info, error) { 128 curSnaps, err := currentSnaps(st) 129 if err != nil { 130 return nil, err 131 } 132 133 refreshOpts, err := refreshOptions(st, nil) 134 if err != nil { 135 return nil, err 136 } 137 138 curInfo, user, err := preUpdateInfo(st, snapst, flags.Amend, userID) 139 if err != nil { 140 return nil, err 141 } 142 143 var storeFlags store.SnapActionFlags 144 if flags.IgnoreValidation { 145 storeFlags = store.SnapActionIgnoreValidation 146 } else { 147 storeFlags = store.SnapActionEnforceValidation 148 } 149 150 action := &store.SnapAction{ 151 Action: "refresh", 152 InstanceName: curInfo.InstanceName(), 153 SnapID: curInfo.SnapID, 154 // the desired channel 155 Channel: opts.Channel, 156 CohortKey: opts.CohortKey, 157 Flags: storeFlags, 158 } 159 160 if curInfo.SnapID == "" { // amend 161 action.Action = "install" 162 action.Epoch = curInfo.Epoch 163 } 164 165 theStore := Store(st, deviceCtx) 166 st.Unlock() // calls to the store should be done without holding the state lock 167 res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, refreshOpts) 168 st.Lock() 169 170 return singleActionResult(curInfo.InstanceName(), action.Action, res, err) 171 } 172 173 func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) { 174 user, err := userFromUserID(st, snapst.UserID, userID) 175 if err != nil { 176 return nil, nil, err 177 } 178 179 curInfo, err := snapst.CurrentInfo() 180 if err != nil { 181 return nil, nil, err 182 } 183 184 if curInfo.SnapID == "" { // covers also trymode 185 if !amend { 186 return nil, nil, store.ErrLocalSnap 187 } 188 } 189 190 return curInfo, user, nil 191 } 192 193 var ErrMissingExpectedResult = fmt.Errorf("unexpectedly empty response from the server (try again later)") 194 195 func singleActionResult(name, action string, results []*snap.Info, e error) (info *snap.Info, err error) { 196 if len(results) > 1 { 197 return nil, fmt.Errorf("internal error: multiple store results for a single snap op") 198 } 199 if len(results) > 0 { 200 // TODO: if we also have an error log/warn about it 201 return results[0], nil 202 } 203 204 if saErr, ok := e.(*store.SnapActionError); ok { 205 if len(saErr.Other) != 0 { 206 return nil, saErr 207 } 208 209 var snapErr error 210 switch action { 211 case "refresh": 212 snapErr = saErr.Refresh[name] 213 case "install": 214 snapErr = saErr.Install[name] 215 } 216 if snapErr != nil { 217 return nil, snapErr 218 } 219 220 // no result, atypical case 221 if saErr.NoResults { 222 return nil, ErrMissingExpectedResult 223 } 224 } 225 226 return nil, e 227 } 228 229 func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, deviceCtx DeviceContext) (*snap.Info, error) { 230 // TODO: support ignore-validation? 231 232 curSnaps, err := currentSnaps(st) 233 if err != nil { 234 return nil, err 235 } 236 237 curInfo, user, err := preUpdateInfo(st, snapst, false, userID) 238 if err != nil { 239 return nil, err 240 } 241 242 opts, err := refreshOptions(st, nil) 243 if err != nil { 244 return nil, err 245 } 246 247 action := &store.SnapAction{ 248 Action: "refresh", 249 SnapID: curInfo.SnapID, 250 InstanceName: curInfo.InstanceName(), 251 // the desired revision 252 Revision: revision, 253 } 254 255 theStore := Store(st, deviceCtx) 256 st.Unlock() // calls to the store should be done without holding the state lock 257 res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, opts) 258 st.Lock() 259 260 return singleActionResult(curInfo.InstanceName(), action.Action, res, err) 261 } 262 263 func currentSnaps(st *state.State) ([]*store.CurrentSnap, error) { 264 snapStates, err := All(st) 265 if err != nil { 266 return nil, err 267 } 268 269 if len(snapStates) == 0 { 270 // no snaps installed, do not bother any further 271 return nil, nil 272 } 273 274 curSnaps := collectCurrentSnaps(snapStates, nil) 275 return curSnaps, nil 276 } 277 278 func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) { 279 curSnaps = make([]*store.CurrentSnap, 0, len(snapStates)) 280 281 for _, snapst := range snapStates { 282 if snapst.TryMode { 283 // try mode snaps are completely local and 284 // irrelevant for the operation 285 continue 286 } 287 288 snapInfo, err := snapst.CurrentInfo() 289 if err != nil { 290 continue 291 } 292 293 if snapInfo.SnapID == "" { 294 // the store won't be able to tell what this 295 // is and so cannot include it in the 296 // operation 297 continue 298 } 299 300 installed := &store.CurrentSnap{ 301 InstanceName: snapInfo.InstanceName(), 302 SnapID: snapInfo.SnapID, 303 // the desired channel (not snapInfo.Channel!) 304 TrackingChannel: snapst.Channel, 305 Revision: snapInfo.Revision, 306 RefreshedDate: revisionDate(snapInfo), 307 IgnoreValidation: snapst.IgnoreValidation, 308 Epoch: snapInfo.Epoch, 309 CohortKey: snapst.CohortKey, 310 } 311 curSnaps = append(curSnaps, installed) 312 313 if consider != nil { 314 consider(installed, snapst) 315 } 316 } 317 318 return curSnaps 319 } 320 321 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) { 322 snapStates, err := All(st) 323 if err != nil { 324 return nil, nil, nil, err 325 } 326 327 opts, err = refreshOptions(st, opts) 328 if err != nil { 329 return nil, nil, nil, err 330 } 331 332 // check if we have this name at all 333 for _, name := range names { 334 if _, ok := snapStates[name]; !ok { 335 return nil, nil, nil, snap.NotInstalledError{Snap: name} 336 } 337 } 338 339 sort.Strings(names) 340 341 var fallbackID int 342 // normalize fallback user 343 if !user.HasStoreAuth() { 344 user = nil 345 } else { 346 fallbackID = user.ID 347 } 348 349 actionsByUserID := make(map[int][]*store.SnapAction) 350 stateByInstanceName := make(map[string]*SnapState, len(snapStates)) 351 ignoreValidationByInstanceName := make(map[string]bool) 352 nCands := 0 353 354 addCand := func(installed *store.CurrentSnap, snapst *SnapState) { 355 // FIXME: snaps that are not active are skipped for now 356 // until we know what we want to do 357 if !snapst.Active { 358 return 359 } 360 361 if len(names) == 0 && snapst.DevMode { 362 // no auto-refresh for devmode 363 return 364 } 365 366 if len(names) > 0 && !strutil.SortedListContains(names, installed.InstanceName) { 367 return 368 } 369 370 stateByInstanceName[installed.InstanceName] = snapst 371 372 if len(names) == 0 { 373 installed.Block = snapst.Block() 374 } 375 376 userID := snapst.UserID 377 if userID == 0 { 378 userID = fallbackID 379 } 380 actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{ 381 Action: "refresh", 382 SnapID: installed.SnapID, 383 InstanceName: installed.InstanceName, 384 }) 385 if snapst.IgnoreValidation { 386 ignoreValidationByInstanceName[installed.InstanceName] = true 387 } 388 nCands++ 389 } 390 // determine current snaps and collect candidates for refresh 391 curSnaps := collectCurrentSnaps(snapStates, addCand) 392 393 actionsForUser := make(map[*auth.UserState][]*store.SnapAction, len(actionsByUserID)) 394 noUserActions := actionsByUserID[0] 395 for userID, actions := range actionsByUserID { 396 if userID == 0 { 397 continue 398 } 399 u, err := userFromUserID(st, userID, 0) 400 if err != nil { 401 return nil, nil, nil, err 402 } 403 if u.HasStoreAuth() { 404 actionsForUser[u] = actions 405 } else { 406 noUserActions = append(noUserActions, actions...) 407 } 408 } 409 // coalesce if possible 410 if len(noUserActions) != 0 { 411 if len(actionsForUser) == 0 { 412 actionsForUser[nil] = noUserActions 413 } else { 414 // coalesce no user actions with one other user's 415 for u1, actions := range actionsForUser { 416 actionsForUser[u1] = append(actions, noUserActions...) 417 break 418 } 419 } 420 } 421 422 // TODO: possibly support a deviceCtx 423 theStore := Store(st, nil) 424 425 updates := make([]*snap.Info, 0, nCands) 426 for u, actions := range actionsForUser { 427 st.Unlock() 428 updatesForUser, err := theStore.SnapAction(ctx, curSnaps, actions, u, opts) 429 st.Lock() 430 if err != nil { 431 saErr, ok := err.(*store.SnapActionError) 432 if !ok { 433 return nil, nil, nil, err 434 } 435 // TODO: use the warning infra here when we have it 436 logger.Noticef("%v", saErr) 437 } 438 439 updates = append(updates, updatesForUser...) 440 } 441 442 return updates, stateByInstanceName, ignoreValidationByInstanceName, nil 443 } 444 445 func installCandidates(st *state.State, names []string, channel string, user *auth.UserState) ([]*snap.Info, error) { 446 curSnaps, err := currentSnaps(st) 447 if err != nil { 448 return nil, err 449 } 450 451 opts, err := refreshOptions(st, nil) 452 if err != nil { 453 return nil, err 454 } 455 456 actions := make([]*store.SnapAction, len(names)) 457 for i, name := range names { 458 actions[i] = &store.SnapAction{ 459 Action: "install", 460 InstanceName: name, 461 // the desired channel 462 Channel: channel, 463 } 464 } 465 466 // TODO: possibly support a deviceCtx 467 theStore := Store(st, nil) 468 st.Unlock() // calls to the store should be done without holding the state lock 469 defer st.Lock() 470 return theStore.SnapAction(context.TODO(), curSnaps, actions, user, opts) 471 }