github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/overlord/snapstate/conflict.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 "fmt" 24 "reflect" 25 26 "github.com/snapcore/snapd/overlord/state" 27 ) 28 29 // FinalTasks are task kinds for final tasks in a change which means no further 30 // change work should be performed afterward, usually these are tasks that 31 // commit a full system transition. 32 var FinalTasks = []string{"mark-seeded", "set-model"} 33 34 // ChangeConflictError represents an error because of snap conflicts between changes. 35 type ChangeConflictError struct { 36 Snap string 37 ChangeKind string 38 // a Message is optional, otherwise one is composed from the other information 39 Message string 40 } 41 42 func (e *ChangeConflictError) Error() string { 43 if e.Message != "" { 44 return e.Message 45 } 46 if e.ChangeKind != "" { 47 return fmt.Sprintf("snap %q has %q change in progress", e.Snap, e.ChangeKind) 48 } 49 return fmt.Sprintf("snap %q has changes in progress", e.Snap) 50 } 51 52 // An AffectedSnapsFunc returns a list of affected snap names for the given supported task. 53 type AffectedSnapsFunc func(*state.Task) ([]string, error) 54 55 var ( 56 affectedSnapsByAttr = make(map[string]AffectedSnapsFunc) 57 affectedSnapsByKind = make(map[string]AffectedSnapsFunc) 58 ) 59 60 // AddAffectedSnapsByAttrs registers an AffectedSnapsFunc for returning the affected snaps for tasks sporting the given identifying attribute, to use in conflicts detection. 61 func AddAffectedSnapsByAttr(attr string, f AffectedSnapsFunc) { 62 affectedSnapsByAttr[attr] = f 63 } 64 65 // AddAffectedSnapsByKind registers an AffectedSnapsFunc for returning the affected snaps for tasks of the given kind, to use in conflicts detection. Whenever possible using AddAffectedSnapsByAttr should be preferred. 66 func AddAffectedSnapsByKind(kind string, f AffectedSnapsFunc) { 67 affectedSnapsByKind[kind] = f 68 } 69 70 func affectedSnaps(t *state.Task) ([]string, error) { 71 // snapstate's own styled tasks 72 if t.Has("snap-setup") || t.Has("snap-setup-task") { 73 snapsup, err := TaskSnapSetup(t) 74 if err != nil { 75 return nil, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", t.Summary()) 76 } 77 return []string{snapsup.InstanceName()}, nil 78 } 79 80 if f := affectedSnapsByKind[t.Kind()]; f != nil { 81 return f(t) 82 } 83 84 for attrKey, f := range affectedSnapsByAttr { 85 if t.Has(attrKey) { 86 return f(t) 87 } 88 } 89 90 return nil, nil 91 } 92 93 func checkChangeConflictExclusiveKinds(st *state.State, newExclusiveChangeKind, ignoreChangeID string) error { 94 for _, chg := range st.Changes() { 95 if chg.Status().Ready() { 96 continue 97 } 98 switch chg.Kind() { 99 case "transition-ubuntu-core": 100 return &ChangeConflictError{ 101 Message: "ubuntu-core to core transition in progress, no other changes allowed until this is done", 102 ChangeKind: "transition-ubuntu-core", 103 } 104 case "transition-to-snapd-snap": 105 return &ChangeConflictError{ 106 Message: "transition to snapd snap in progress, no other changes allowed until this is done", 107 ChangeKind: "transition-to-snapd-snap", 108 } 109 case "remodel": 110 if ignoreChangeID != "" && chg.ID() == ignoreChangeID { 111 continue 112 } 113 return &ChangeConflictError{ 114 Message: "remodeling in progress, no other changes allowed until this is done", 115 ChangeKind: "remodel", 116 } 117 case "create-recovery-system": 118 if ignoreChangeID != "" && chg.ID() == ignoreChangeID { 119 continue 120 } 121 return &ChangeConflictError{ 122 Message: "creating recovery system in progress, no other changes allowed until this is done", 123 ChangeKind: "create-recovery-system", 124 } 125 default: 126 if newExclusiveChangeKind != "" { 127 // we want to run a new exclusive change, but other 128 // changes are in progress already 129 msg := fmt.Sprintf("other changes in progress (conflicting change %q), change %q not allowed until they are done", chg.Kind(), 130 newExclusiveChangeKind) 131 return &ChangeConflictError{ 132 Message: msg, 133 ChangeKind: chg.Kind(), 134 } 135 } 136 } 137 } 138 return nil 139 } 140 141 // CheckChangeConflictRunExclusively checks for conflicts with a new change which 142 // must be run when no other changes are running. 143 func CheckChangeConflictRunExclusively(st *state.State, newChangeKind string) error { 144 return checkChangeConflictExclusiveKinds(st, newChangeKind, "") 145 } 146 147 // CheckChangeConflictMany ensures that for the given instanceNames no other 148 // changes that alters the snaps (like remove, install, refresh) are in 149 // progress. If a conflict is detected an error is returned. 150 // 151 // It's like CheckChangeConflict, but for multiple snaps, and does not 152 // check snapst. 153 func CheckChangeConflictMany(st *state.State, instanceNames []string, ignoreChangeID string) error { 154 snapMap := make(map[string]bool, len(instanceNames)) 155 for _, k := range instanceNames { 156 snapMap[k] = true 157 } 158 159 // check whether there are other changes that need to run exclusively 160 if err := checkChangeConflictExclusiveKinds(st, "", ignoreChangeID); err != nil { 161 return err 162 } 163 164 for _, task := range st.Tasks() { 165 chg := task.Change() 166 if chg == nil || chg.Status().Ready() { 167 continue 168 } 169 if ignoreChangeID != "" && chg.ID() == ignoreChangeID { 170 continue 171 } 172 if chg.Kind() == "become-operational" { 173 // become-operational will be retried until success 174 // and on its own just runs a hook on gadget: 175 // do not make it interfere with user requests 176 // TODO: consider a use vs change modeling of 177 // conflicts 178 continue 179 } 180 181 snaps, err := affectedSnaps(task) 182 if err != nil { 183 return err 184 } 185 186 for _, snap := range snaps { 187 if snapMap[snap] { 188 return &ChangeConflictError{Snap: snap, ChangeKind: chg.Kind()} 189 } 190 } 191 } 192 193 return nil 194 } 195 196 // CheckChangeConflict ensures that for the given instanceName no other 197 // changes that alters the snap (like remove, install, refresh) are in 198 // progress. It also ensures that snapst (if not nil) did not get 199 // modified. If a conflict is detected an error is returned. 200 func CheckChangeConflict(st *state.State, instanceName string, snapst *SnapState) error { 201 return checkChangeConflictIgnoringOneChange(st, instanceName, snapst, "") 202 } 203 204 func checkChangeConflictIgnoringOneChange(st *state.State, instanceName string, snapst *SnapState, ignoreChangeID string) error { 205 if err := CheckChangeConflictMany(st, []string{instanceName}, ignoreChangeID); err != nil { 206 return err 207 } 208 209 if snapst != nil { 210 // caller wants us to also make sure the SnapState in state 211 // matches the one they provided. Necessary because we need to 212 // unlock while talking to the store, during which a change can 213 // sneak in (if it's before the taskset is created) (e.g. for 214 // install, while getting the snap info; for refresh, when 215 // getting what needs refreshing). 216 var cursnapst SnapState 217 if err := Get(st, instanceName, &cursnapst); err != nil && err != state.ErrNoState { 218 return err 219 } 220 221 // TODO: implement the rather-boring-but-more-performant SnapState.Equals 222 if !reflect.DeepEqual(snapst, &cursnapst) { 223 return &ChangeConflictError{Snap: instanceName} 224 } 225 } 226 227 return nil 228 }