github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/configstate/config/transaction_test.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_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "strings" 27 "testing" 28 29 . "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/jsonutil" 32 "github.com/snapcore/snapd/overlord/configstate/config" 33 "github.com/snapcore/snapd/overlord/state" 34 ) 35 36 func TestT(t *testing.T) { TestingT(t) } 37 38 type transactionSuite struct { 39 state *state.State 40 transaction *config.Transaction 41 } 42 43 var _ = Suite(&transactionSuite{}) 44 45 func (s *transactionSuite) SetUpTest(c *C) { 46 s.state = state.New(nil) 47 s.state.Lock() 48 defer s.state.Unlock() 49 s.transaction = config.NewTransaction(s.state) 50 } 51 52 type setGetOp string 53 54 func (op setGetOp) kind() string { 55 return strings.Fields(string(op))[0] 56 } 57 58 func (op setGetOp) list() []string { 59 args := strings.Fields(string(op)) 60 return args[1:] 61 } 62 63 func (op setGetOp) args() map[string]interface{} { 64 m := make(map[string]interface{}) 65 args := strings.Fields(string(op)) 66 for _, pair := range args[1:] { 67 if pair == "=>" { 68 break 69 } 70 kv := strings.SplitN(pair, "=", 2) 71 var v interface{} 72 if err := jsonutil.DecodeWithNumber(strings.NewReader(kv[1]), &v); err != nil { 73 v = kv[1] 74 } 75 m[kv[0]] = v 76 } 77 return m 78 } 79 80 func (op setGetOp) error() string { 81 if i := strings.Index(string(op), " => "); i >= 0 { 82 return string(op[i+4:]) 83 } 84 return "" 85 } 86 87 func (op setGetOp) fails() bool { 88 return op.error() != "" 89 } 90 91 var setGetTests = [][]setGetOp{{ 92 // Basics. 93 `get foo=-`, 94 `getroot => snap "core" has no configuration`, 95 `set one=1 two=2`, 96 `set big=1234567890`, 97 `setunder three=3 big=9876543210`, 98 `get one=1 big=1234567890 two=2 three=-`, 99 `getunder one=- two=- three=3 big=9876543210`, 100 `changes core.big core.one core.two`, 101 `commit`, 102 `getunder one=1 two=2 three=3`, 103 `get one=1 two=2 three=3`, 104 `set two=22 four=4 big=1234567890`, 105 `changes core.big core.four core.two`, 106 `get one=1 two=22 three=3 four=4 big=1234567890`, 107 `getunder one=1 two=2 three=3 four=-`, 108 `commit`, 109 `getunder one=1 two=22 three=3 four=4`, 110 }, { 111 // Trivial full doc. 112 `set doc={"one":1,"two":2}`, 113 `get doc={"one":1,"two":2}`, 114 `changes core.doc.one core.doc.two`, 115 }, { 116 // Nulls via dotted path 117 `set doc={"one":1,"two":2}`, 118 `commit`, 119 `set doc.one=null`, 120 `changes core.doc.one`, 121 `get doc={"two":2}`, 122 `getunder doc={"one":1,"two":2}`, 123 `commit`, 124 `get doc={"two":2}`, 125 `getroot ={"doc":{"two":2}}`, 126 `getunder doc={"two":2}`, // nils are not committed to state 127 }, { 128 // Nulls via dotted path, resuling in empty map 129 `set doc={"one":{"three":3},"two":2}`, 130 `set doc.one.three=null`, 131 `changes core.doc.one.three core.doc.two`, 132 `get doc={"one":{},"two":2}`, 133 `commit`, 134 `get doc={"one":{},"two":2}`, 135 `getunder doc={"one":{},"two":2}`, // nils are not committed to state 136 }, { 137 // Nulls via dotted path in a doc 138 `set doc={"one":1,"two":2}`, 139 `set doc.three={"four":4}`, 140 `get doc={"one":1,"two":2,"three":{"four":4}}`, 141 `set doc.three={"four":null}`, 142 `changes core.doc.one core.doc.three.four core.doc.two`, 143 `get doc={"one":1,"two":2,"three":{}}`, 144 `commit`, 145 `get doc={"one":1,"two":2,"three":{}}`, 146 `getunder doc={"one":1,"two":2,"three":{}}`, // nils are not committed to state 147 }, { 148 // Nulls nested in a document 149 `set doc={"one":{"three":3,"two":2}}`, 150 `changes core.doc.one.three core.doc.one.two`, 151 `set doc={"one":{"three":null,"two":2}}`, 152 `changes core.doc.one.three core.doc.one.two`, 153 `get doc={"one":{"two":2}}`, 154 `commit`, 155 `get doc={"one":{"two":2}}`, 156 `getunder doc={"one":{"two":2}}`, // nils are not committed to state 157 }, { 158 // Nulls with mutating 159 `set doc={"one":{"two":2}}`, 160 `set doc.one.two=null`, 161 `changes core.doc.one.two`, 162 `set doc.one="foo"`, 163 `get doc.one="foo"`, 164 `commit`, 165 `get doc={"one":"foo"}`, 166 `getunder doc={"one":"foo"}`, // nils are not committed to state 167 }, { 168 // Nulls, intermediate temporary maps 169 `set doc={"one":{"two":2}}`, 170 `commit`, 171 `set doc.one.three.four.five=null`, 172 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 173 `commit`, 174 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 175 `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state 176 }, { 177 // Nulls, same transaction 178 `set doc={"one":{"two":2}}`, 179 `set doc.one.three.four.five=null`, 180 `changes core.doc.one.three.four.five core.doc.one.two`, 181 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 182 `commit`, 183 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 184 `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state 185 }, { 186 // Null leading to empty doc 187 `set doc={"one":1}`, 188 `set doc.one=null`, 189 `changes core.doc.one`, 190 `commit`, 191 `get doc={}`, 192 }, { 193 // Nulls leading to no snap configuration 194 `set doc="foo"`, 195 `set doc=null`, 196 `changes core.doc`, 197 `commit`, 198 `get doc=-`, 199 `getroot => snap "core" has no configuration`, 200 }, { 201 // set null over non-existing path 202 `set x.y.z=null`, 203 `changes core.x.y.z`, 204 `commit`, 205 `get x.y.z=-`, 206 }, { 207 // set null over non-existing path with initial config 208 `set foo=bar`, 209 `commit`, 210 `set x=null`, 211 `changes core.x`, 212 `commit`, 213 `get x=-`, 214 }, { 215 // Nulls, set then unset and set back over same partial path 216 `set doc.x.a=1`, 217 `commit`, 218 `set doc.x.a=null`, 219 `get doc={"x":{}}}`, 220 `set doc.x.a=6`, 221 `get doc={"x":{"a":6}}`, 222 `commit`, 223 `get doc={"x":{"a":6}}`, 224 `getrootunder ={"doc":{"x":{"a":6}}}`, 225 }, { 226 // Nulls, set then unset and set back over same path 227 `set doc.x.a=1`, 228 `commit`, 229 `set doc.x=null`, 230 `get doc={}`, 231 `set doc.x.a=3`, 232 `get doc={"x":{"a":3}}`, 233 `commit`, 234 `get doc={"x":{"a":3}}`, 235 `getrootunder ={"doc":{"x":{"a":3}}}`, 236 }, { 237 // Nulls, set then unset and set back root element 238 `set doc.x.a=1`, 239 `commit`, 240 `set doc.x=null`, 241 `get doc={}`, 242 `set doc=null`, 243 `get doc=-`, 244 `set doc=99`, 245 `commit`, 246 `get doc=99`, 247 `getrootunder ={"doc":99}`, 248 }, { 249 // Nulls, set then unset over same path 250 `set doc.x.a=1 doc.x.b=2`, 251 `commit`, 252 `set doc.x=null`, 253 `set doc.x.a=null`, 254 `set doc.x.b=null`, 255 `get doc={"x":{}}`, 256 `commit`, 257 `get doc={"x":{}}`, 258 `getrootunder ={"doc":{"x":{}}}`, 259 }, { 260 // Nulls, set then unset and set back over same path 261 `set doc.x.a=1`, 262 `commit`, 263 `set doc.x=null`, 264 `set doc.x.a=null`, 265 `get doc={"x":{}}`, 266 `set doc={"x":{"a":9}}`, 267 `set doc.x.a=1`, 268 `get doc={"x":{"a":1}}`, 269 `commit`, 270 `get doc={"x":{"a":1}}`, 271 `getrootunder ={"doc":{"x":{"a":1}}}`, 272 }, { 273 // Root doc 274 `set doc={"one":1,"two":2}`, 275 `changes core.doc.one core.doc.two`, 276 `getroot ={"doc":{"one":1,"two":2}}`, 277 `commit`, 278 `getroot ={"doc":{"one":1,"two":2}}`, 279 `getrootunder ={"doc":{"one":1,"two":2}}`, 280 }, { 281 // Nested mutations. 282 `set one.two.three=3`, 283 `changes core.one.two.three`, 284 `set one.five=5`, 285 `changes core.one.five core.one.two.three`, 286 `setunder one={"two":{"four":4}}`, 287 `get one={"two":{"three":3},"five":5}`, 288 `get one.two={"three":3}`, 289 `get one.two.three=3`, 290 `get one.five=5`, 291 `commit`, 292 `getunder one={"two":{"three":3,"four":4},"five":5}`, 293 `get one={"two":{"three":3,"four":4},"five":5}`, 294 `get one.two={"three":3,"four":4}`, 295 `get one.two.three=3`, 296 `get one.two.four=4`, 297 `get one.five=5`, 298 }, { 299 // Nested partial update with full get 300 `set one={"two":2,"three":3}`, 301 `commit`, 302 // update just one subkey 303 `set one.two=0`, 304 // both subkeys are returned 305 `get one={"two":0,"three":3}`, 306 `getroot ={"one":{"two":0,"three":3}}`, 307 `get one.two=0`, 308 `get one.three=3`, 309 `getunder one={"two":2,"three":3}`, 310 `changes core.one.two`, 311 `commit`, 312 `getroot ={"one":{"two":0,"three":3}}`, 313 `get one={"two":0,"three":3}`, 314 `getunder one={"two":0,"three":3}`, 315 }, { 316 // Replacement with nested mutation. 317 `set one={"two":{"three":3}}`, 318 `changes core.one.two.three`, 319 `set one.five=5`, 320 `changes core.one.five core.one.two.three`, 321 `get one={"two":{"three":3},"five":5}`, 322 `get one.two={"three":3}`, 323 `get one.two.three=3`, 324 `get one.five=5`, 325 `setunder one={"two":{"four":4},"six":6}`, 326 `commit`, 327 `getunder one={"two":{"three":3},"five":5}`, 328 }, { 329 // Cannot go through known scalar implicitly. 330 `set one.two=2`, 331 `changes core.one.two`, 332 `set one.two.three=3 => snap "core" option "one\.two" is not a map`, 333 `get one.two.three=3 => snap "core" option "one\.two" is not a map`, 334 `get one={"two":2}`, 335 `commit`, 336 `set one.two.three=3 => snap "core" option "one\.two" is not a map`, 337 `get one.two.three=3 => snap "core" option "one\.two" is not a map`, 338 `get one={"two":2}`, 339 `getunder one={"two":2}`, 340 }, { 341 // Unknown scalars may be overwritten though. 342 `setunder one={"two":2}`, 343 `set one.two.three=3`, 344 `changes core.one.two.three`, 345 `commit`, 346 `getunder one={"two":{"three":3}}`, 347 }, { 348 // Invalid option names. 349 `set BAD=1 => invalid option name: "BAD"`, 350 `set 42=1 => invalid option name: "42"`, 351 `set .bad=1 => invalid option name: ""`, 352 `set bad.=1 => invalid option name: ""`, 353 `set bad..bad=1 => invalid option name: ""`, 354 `set one.bad--bad.two=1 => invalid option name: "bad--bad"`, 355 `set one.-bad.two=1 => invalid option name: "-bad"`, 356 `set one.bad-.two=1 => invalid option name: "bad-"`, 357 }} 358 359 func (s *transactionSuite) TestSetGet(c *C) { 360 s.state.Lock() 361 defer s.state.Unlock() 362 363 for _, test := range setGetTests { 364 c.Logf("-----") 365 s.state.Set("config", map[string]interface{}{}) 366 t := config.NewTransaction(s.state) 367 snap := "core" 368 for _, op := range test { 369 c.Logf("%s", op) 370 switch op.kind() { 371 case "set": 372 for k, v := range op.args() { 373 err := t.Set(snap, k, v) 374 if op.fails() { 375 c.Assert(err, ErrorMatches, op.error()) 376 } else { 377 c.Assert(err, IsNil) 378 } 379 } 380 381 case "get": 382 for k, expected := range op.args() { 383 var obtained interface{} 384 err := t.Get(snap, k, &obtained) 385 if op.fails() { 386 c.Assert(err, ErrorMatches, op.error()) 387 var nothing interface{} 388 c.Assert(t.GetMaybe(snap, k, ¬hing), ErrorMatches, op.error()) 389 c.Assert(nothing, IsNil) 390 continue 391 } 392 if expected == "-" { 393 if !config.IsNoOption(err) { 394 c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) 395 } 396 c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k)) 397 var nothing interface{} 398 c.Assert(t.GetMaybe(snap, k, ¬hing), IsNil) 399 c.Assert(nothing, IsNil) 400 continue 401 } 402 c.Assert(err, IsNil) 403 c.Assert(obtained, DeepEquals, expected) 404 405 obtained = nil 406 c.Assert(t.GetMaybe(snap, k, &obtained), IsNil) 407 c.Assert(obtained, DeepEquals, expected) 408 } 409 410 case "commit": 411 t.Commit() 412 413 case "changes": 414 expected := op.list() 415 obtained := t.Changes() 416 c.Check(obtained, DeepEquals, expected) 417 418 case "setunder": 419 var config map[string]map[string]interface{} 420 s.state.Get("config", &config) 421 if config == nil { 422 config = make(map[string]map[string]interface{}) 423 } 424 if config[snap] == nil { 425 config[snap] = make(map[string]interface{}) 426 } 427 for k, v := range op.args() { 428 if v == "-" { 429 delete(config[snap], k) 430 if len(config[snap]) == 0 { 431 delete(config[snap], snap) 432 } 433 } else { 434 config[snap][k] = v 435 } 436 } 437 s.state.Set("config", config) 438 439 case "getunder": 440 var config map[string]map[string]*json.RawMessage 441 s.state.Get("config", &config) 442 for k, expected := range op.args() { 443 obtained, ok := config[snap][k] 444 if expected == "-" { 445 if ok { 446 c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) 447 } 448 continue 449 } 450 var cfg interface{} 451 c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil) 452 c.Assert(cfg, DeepEquals, expected) 453 } 454 case "getroot": 455 var obtained interface{} 456 err := t.Get(snap, "", &obtained) 457 if op.fails() { 458 c.Assert(err, ErrorMatches, op.error()) 459 continue 460 } 461 c.Assert(err, IsNil) 462 c.Assert(obtained, DeepEquals, op.args()[""]) 463 case "getrootunder": 464 var config map[string]*json.RawMessage 465 s.state.Get("config", &config) 466 for _, expected := range op.args() { 467 obtained, ok := config[snap] 468 c.Assert(ok, Equals, true) 469 var cfg interface{} 470 c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil) 471 c.Assert(cfg, DeepEquals, expected) 472 } 473 default: 474 panic("unknown test op kind: " + op.kind()) 475 } 476 } 477 } 478 } 479 480 type brokenType struct { 481 on string 482 } 483 484 func (b *brokenType) UnmarshalJSON(data []byte) error { 485 if b.on == string(data) { 486 return fmt.Errorf("BAM!") 487 } 488 return nil 489 } 490 491 func (s *transactionSuite) TestCommitOverNilSnapConfig(c *C) { 492 s.state.Lock() 493 defer s.state.Unlock() 494 495 // simulate invalid nil map created due to LP #1917870 by snap restore 496 s.state.Set("config", map[string]interface{}{"test-snap": nil}) 497 t := config.NewTransaction(s.state) 498 499 c.Assert(t.Set("test-snap", "foo", "bar"), IsNil) 500 t.Commit() 501 var v string 502 t.Get("test-snap", "foo", &v) 503 c.Assert(v, Equals, "bar") 504 } 505 506 func (s *transactionSuite) TestGetUnmarshalError(c *C) { 507 s.state.Lock() 508 defer s.state.Unlock() 509 c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil) 510 s.transaction.Commit() 511 512 tr := config.NewTransaction(s.state) 513 c.Check(tr.Set("test-snap", "foo", "break"), IsNil) 514 515 // Pristine state is good, value in the transaction breaks. 516 broken := brokenType{`"break"`} 517 err := tr.Get("test-snap", "foo", &broken) 518 c.Assert(err, ErrorMatches, ".*BAM!.*") 519 520 // Pristine state breaks, nothing in the transaction. 521 tr.Commit() 522 err = tr.Get("test-snap", "foo", &broken) 523 c.Assert(err, ErrorMatches, ".*BAM!.*") 524 } 525 526 func (s *transactionSuite) TestNoConfiguration(c *C) { 527 s.state.Lock() 528 defer s.state.Unlock() 529 530 var res interface{} 531 tr := config.NewTransaction(s.state) 532 err := tr.Get("some-snap", "", &res) 533 c.Assert(err, NotNil) 534 c.Assert(config.IsNoOption(err), Equals, true) 535 c.Assert(err, ErrorMatches, `snap "some-snap" has no configuration`) 536 } 537 538 func (s *transactionSuite) TestState(c *C) { 539 s.state.Lock() 540 defer s.state.Unlock() 541 542 tr := config.NewTransaction(s.state) 543 c.Check(tr.State(), DeepEquals, s.state) 544 } 545 546 func (s *transactionSuite) TestPristineIsNotTainted(c *C) { 547 s.state.Lock() 548 defer s.state.Unlock() 549 550 tr := config.NewTransaction(s.state) 551 c.Check(tr.Set("test-snap", "foo.a.a", "a"), IsNil) 552 tr.Commit() 553 554 var data interface{} 555 var result interface{} 556 tr = config.NewTransaction(s.state) 557 c.Check(tr.Set("test-snap", "foo.b", "b"), IsNil) 558 c.Check(tr.Set("test-snap", "foo.a.a", "b"), IsNil) 559 c.Assert(tr.Get("test-snap", "foo", &result), IsNil) 560 c.Check(result, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "b"}, "b": "b"}) 561 562 pristine := tr.PristineConfig() 563 c.Assert(json.Unmarshal([]byte(*pristine["test-snap"]["foo"]), &data), IsNil) 564 c.Assert(data, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "a"}}) 565 } 566 567 func (s *transactionSuite) TestPristineGet(c *C) { 568 s.state.Lock() 569 defer s.state.Unlock() 570 571 // start with a pristine config 572 s.state.Set("config", map[string]map[string]interface{}{ 573 "some-snap": {"opt1": "pristine-value"}, 574 }) 575 576 // change the config 577 var res interface{} 578 tr := config.NewTransaction(s.state) 579 err := tr.Set("some-snap", "opt1", "changed-value") 580 c.Assert(err, IsNil) 581 582 // and get will get the changed value 583 err = tr.Get("some-snap", "opt1", &res) 584 c.Assert(err, IsNil) 585 c.Assert(res, Equals, "changed-value") 586 587 // but GetPristine will get the pristine value 588 err = tr.GetPristine("some-snap", "opt1", &res) 589 c.Assert(err, IsNil) 590 c.Assert(res, Equals, "pristine-value") 591 592 // and GetPristine errors for options that don't exist in pristine 593 var res2 interface{} 594 err = tr.Set("some-snap", "opt2", "other-value") 595 c.Assert(err, IsNil) 596 err = tr.GetPristine("some-snap", "opt2", &res2) 597 c.Assert(err, ErrorMatches, `snap "some-snap" has no "opt2" configuration option`) 598 // but GetPristineMaybe does not error but also give no value 599 err = tr.GetPristineMaybe("some-snap", "opt2", &res2) 600 c.Assert(err, IsNil) 601 c.Assert(res2, IsNil) 602 // but regular get works 603 err = tr.Get("some-snap", "opt2", &res2) 604 c.Assert(err, IsNil) 605 c.Assert(res2, Equals, "other-value") 606 }