github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/configstate/config/transaction.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 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 config 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "sort" 27 "strings" 28 "sync" 29 30 "github.com/snapcore/snapd/jsonutil" 31 "github.com/snapcore/snapd/overlord/state" 32 ) 33 34 // Transaction holds a copy of the configuration originally present in the 35 // provided state which can be queried and mutated in isolation from 36 // concurrent logic. All changes performed into it are persisted back into 37 // the state at once when Commit is called. 38 // 39 // Transactions are safe to access and modify concurrently. 40 type Transaction struct { 41 mu sync.Mutex 42 state *state.State 43 pristine map[string]map[string]*json.RawMessage // snap => key => value 44 changes map[string]map[string]interface{} 45 } 46 47 // NewTransaction creates a new configuration transaction initialized with the given state. 48 // 49 // The provided state must be locked by the caller. 50 func NewTransaction(st *state.State) *Transaction { 51 transaction := &Transaction{state: st} 52 transaction.changes = make(map[string]map[string]interface{}) 53 54 // Record the current state of the map containing the config of every snap 55 // in the system. We'll use it for this transaction. 56 err := st.Get("config", &transaction.pristine) 57 if err == state.ErrNoState { 58 transaction.pristine = make(map[string]map[string]*json.RawMessage) 59 } else if err != nil { 60 panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err)) 61 } 62 return transaction 63 } 64 65 // State returns the system State 66 func (t *Transaction) State() *state.State { 67 return t.state 68 } 69 70 func changes(cfgStr string, cfg map[string]interface{}) []string { 71 var out []string 72 for k := range cfg { 73 switch subCfg := cfg[k].(type) { 74 case map[string]interface{}: 75 out = append(out, changes(cfgStr+"."+k, subCfg)...) 76 case *json.RawMessage: 77 // check if we need to dive into a sub-config 78 var configm map[string]interface{} 79 if err := jsonutil.DecodeWithNumber(bytes.NewReader(*subCfg), &configm); err == nil { 80 // curiously, json decoder decodes json.RawMessage("null") into a nil map, so no change is 81 // reported when we recurse into it. This happens when unsetting a key and the underlying 82 // config path doesn't exist. 83 if len(configm) > 0 { 84 out = append(out, changes(cfgStr+"."+k, configm)...) 85 continue 86 } 87 } 88 out = append(out, []string{cfgStr + "." + k}...) 89 default: 90 out = append(out, []string{cfgStr + "." + k}...) 91 } 92 } 93 return out 94 } 95 96 // Changes returns the changing keys associated with this transaction 97 func (t *Transaction) Changes() []string { 98 var out []string 99 for k := range t.changes { 100 out = append(out, changes(k, t.changes[k])...) 101 } 102 sort.Strings(out) 103 return out 104 } 105 106 // Set sets the provided snap's configuration key to the given value. 107 // The provided key may be formed as a dotted key path through nested maps. 108 // For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. 109 // When the key is provided in that form, intermediate maps are mutated 110 // rather than replaced, and created when necessary. 111 // 112 // The provided value must marshal properly by encoding/json. 113 // Changes are not persisted until Commit is called. 114 func (t *Transaction) Set(instanceName, key string, value interface{}) error { 115 t.mu.Lock() 116 defer t.mu.Unlock() 117 118 config := t.changes[instanceName] 119 if config == nil { 120 config = make(map[string]interface{}) 121 } 122 123 data, err := json.Marshal(value) 124 if err != nil { 125 return fmt.Errorf("cannot marshal snap %q option %q: %s", instanceName, key, err) 126 } 127 raw := json.RawMessage(data) 128 129 subkeys, err := ParseKey(key) 130 if err != nil { 131 return err 132 } 133 134 // Check whether it's trying to traverse a non-map from pristine. This 135 // would go unperceived by the configuration patching below. 136 if len(subkeys) > 1 { 137 var result interface{} 138 err = getFromConfig(instanceName, subkeys, 0, t.pristine[instanceName], &result) 139 if err != nil && !IsNoOption(err) { 140 return err 141 } 142 } 143 144 // config here is never nil and PatchConfig always operates 145 // directly on and returns config if it's a 146 // map[string]interface{} 147 _, err = PatchConfig(instanceName, subkeys, 0, config, &raw) 148 if err != nil { 149 return err 150 } 151 152 t.changes[instanceName] = config 153 return nil 154 } 155 156 func (t *Transaction) copyPristine(snapName string) map[string]*json.RawMessage { 157 out := make(map[string]*json.RawMessage) 158 if config, ok := t.pristine[snapName]; ok { 159 for k, v := range config { 160 out[k] = v 161 } 162 } 163 return out 164 } 165 166 // Get unmarshals into result the cached value of the provided snap's configuration key. 167 // If the key does not exist, an error of type *NoOptionError is returned. 168 // The provided key may be formed as a dotted key path through nested maps. 169 // For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. 170 // 171 // Transactions do not see updates from the current state or from other transactions. 172 func (t *Transaction) Get(snapName, key string, result interface{}) error { 173 t.mu.Lock() 174 defer t.mu.Unlock() 175 176 subkeys, err := ParseKey(key) 177 if err != nil { 178 return err 179 } 180 181 // commit changes onto a copy of pristine configuration, so that get has a complete view of the config. 182 config := t.copyPristine(snapName) 183 applyChanges(config, t.changes[snapName]) 184 185 purgeNulls(config) 186 return getFromConfig(snapName, subkeys, 0, config, result) 187 } 188 189 // GetMaybe unmarshals into result the cached value of the provided snap's configuration key. 190 // If the key does not exist, no error is returned. 191 // 192 // Transactions do not see updates from the current state or from other transactions. 193 func (t *Transaction) GetMaybe(instanceName, key string, result interface{}) error { 194 err := t.Get(instanceName, key, result) 195 if err != nil && !IsNoOption(err) { 196 return err 197 } 198 return nil 199 } 200 201 // GetPristine unmarshals the cached pristine (before applying any 202 // changes) value of the provided snap's configuration key into 203 // result. 204 // 205 // If the key does not exist, an error of type *NoOptionError is returned. 206 func (t *Transaction) GetPristine(snapName, key string, result interface{}) error { 207 t.mu.Lock() 208 defer t.mu.Unlock() 209 210 subkeys, err := ParseKey(key) 211 if err != nil { 212 return err 213 } 214 215 return getFromConfig(snapName, subkeys, 0, t.pristine[snapName], result) 216 } 217 218 // GetPristineMaybe unmarshals the cached pristine (before applying any 219 // changes) value of the provided snap's configuration key into 220 // result. 221 // 222 // If the key does not exist, no error is returned. 223 func (t *Transaction) GetPristineMaybe(instanceName, key string, result interface{}) error { 224 err := t.GetPristine(instanceName, key, result) 225 if err != nil && !IsNoOption(err) { 226 return err 227 } 228 return nil 229 } 230 231 func getFromConfig(instanceName string, subkeys []string, pos int, config map[string]*json.RawMessage, result interface{}) error { 232 // special case - get root document 233 if len(subkeys) == 0 { 234 if len(config) == 0 { 235 return &NoOptionError{SnapName: instanceName} 236 } 237 raw := jsonRaw(config) 238 if err := jsonutil.DecodeWithNumber(bytes.NewReader(*raw), &result); err != nil { 239 return fmt.Errorf("internal error: cannot unmarshal snap %q root document: %s", instanceName, err) 240 } 241 return nil 242 } 243 244 raw, ok := config[subkeys[pos]] 245 if !ok { 246 return &NoOptionError{SnapName: instanceName, Key: strings.Join(subkeys[:pos+1], ".")} 247 } 248 249 // There is a known problem with json raw messages representing nulls when they are stored in nested structures, such as 250 // config map inside our state. These are turned into nils and need to be handled explicitly. 251 if raw == nil { 252 m := json.RawMessage("null") 253 raw = &m 254 } 255 256 if pos+1 == len(subkeys) { 257 if err := jsonutil.DecodeWithNumber(bytes.NewReader(*raw), &result); err != nil { 258 key := strings.Join(subkeys, ".") 259 return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", instanceName, key, result, err, *raw) 260 } 261 return nil 262 } 263 264 var configm map[string]*json.RawMessage 265 if err := jsonutil.DecodeWithNumber(bytes.NewReader(*raw), &configm); err != nil { 266 return fmt.Errorf("snap %q option %q is not a map", instanceName, strings.Join(subkeys[:pos+1], ".")) 267 } 268 return getFromConfig(instanceName, subkeys, pos+1, configm, result) 269 } 270 271 // Commit applies to the state the configuration changes made in the transaction 272 // and updates the observed configuration to the result of the operation. 273 // 274 // The state associated with the transaction must be locked by the caller. 275 func (t *Transaction) Commit() { 276 t.mu.Lock() 277 defer t.mu.Unlock() 278 279 if len(t.changes) == 0 { 280 return 281 } 282 283 // Update our copy of the config with the most recent one from the state. 284 err := t.state.Get("config", &t.pristine) 285 if err == state.ErrNoState { 286 t.pristine = make(map[string]map[string]*json.RawMessage) 287 } else if err != nil { 288 panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err)) 289 } 290 291 // Iterate through the write cache and save each item. 292 for instanceName, snapChanges := range t.changes { 293 config := t.pristine[instanceName] 294 // due to LP #1917870 we might have a hook configure task in flight 295 // that tries to apply config over nil map, create it if nil. 296 if config == nil { 297 config = make(map[string]*json.RawMessage) 298 } 299 applyChanges(config, snapChanges) 300 purgeNulls(config) 301 t.pristine[instanceName] = config 302 } 303 304 t.state.Set("config", t.pristine) 305 306 // The cache has been flushed, reset it. 307 t.changes = make(map[string]map[string]interface{}) 308 } 309 310 func applyChanges(config map[string]*json.RawMessage, changes map[string]interface{}) { 311 for k, v := range changes { 312 config[k] = commitChange(config[k], v) 313 } 314 } 315 316 func jsonRaw(v interface{}) *json.RawMessage { 317 data, err := json.Marshal(v) 318 if err != nil { 319 panic(fmt.Errorf("internal error: cannot marshal configuration: %v", err)) 320 } 321 raw := json.RawMessage(data) 322 return &raw 323 } 324 325 func commitChange(pristine *json.RawMessage, change interface{}) *json.RawMessage { 326 switch change := change.(type) { 327 case *json.RawMessage: 328 return change 329 case map[string]interface{}: 330 if pristine == nil { 331 return jsonRaw(change) 332 } 333 var pristinem map[string]*json.RawMessage 334 if err := jsonutil.DecodeWithNumber(bytes.NewReader(*pristine), &pristinem); err != nil { 335 // Not a map. Overwrite with the change. 336 return jsonRaw(change) 337 } 338 for k, v := range change { 339 pristinem[k] = commitChange(pristinem[k], v) 340 } 341 return jsonRaw(pristinem) 342 } 343 panic(fmt.Errorf("internal error: unexpected configuration type %T", change)) 344 } 345 346 // IsNoOption returns whether the provided error is a *NoOptionError. 347 func IsNoOption(err error) bool { 348 _, ok := err.(*NoOptionError) 349 return ok 350 } 351 352 // NoOptionError indicates that a config option is not set. 353 type NoOptionError struct { 354 SnapName string 355 Key string 356 } 357 358 func (e *NoOptionError) Error() string { 359 if e.Key == "" { 360 return fmt.Sprintf("snap %q has no configuration", e.SnapName) 361 } 362 return fmt.Sprintf("snap %q has no %q configuration option", e.SnapName, e.Key) 363 }