gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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 config.ClearExternalConfigMap() 52 } 53 54 type setGetOp string 55 56 func (op setGetOp) kind() string { 57 return strings.Fields(string(op))[0] 58 } 59 60 func (op setGetOp) list() []string { 61 args := strings.Fields(string(op)) 62 return args[1:] 63 } 64 65 func (op setGetOp) args() map[string]interface{} { 66 m := make(map[string]interface{}) 67 args := strings.Fields(string(op)) 68 for _, pair := range args[1:] { 69 if pair == "=>" { 70 break 71 } 72 kv := strings.SplitN(pair, "=", 2) 73 var v interface{} 74 if err := jsonutil.DecodeWithNumber(strings.NewReader(kv[1]), &v); err != nil { 75 v = kv[1] 76 } 77 m[kv[0]] = v 78 } 79 return m 80 } 81 82 func (op setGetOp) error() string { 83 if i := strings.Index(string(op), " => "); i >= 0 { 84 return string(op[i+4:]) 85 } 86 return "" 87 } 88 89 func (op setGetOp) fails() bool { 90 return op.error() != "" 91 } 92 93 var setGetTests = [][]setGetOp{{ 94 // Basics. 95 `get foo=-`, 96 `getroot => snap "core" has no configuration`, 97 `set one=1 two=2`, 98 `set big=1234567890`, 99 `setunder three=3 big=9876543210`, 100 `get one=1 big=1234567890 two=2 three=-`, 101 `getunder one=- two=- three=3 big=9876543210`, 102 `changes core.big core.one core.two`, 103 `commit`, 104 `getunder one=1 two=2 three=3`, 105 `get one=1 two=2 three=3`, 106 `set two=22 four=4 big=1234567890`, 107 `changes core.big core.four core.two`, 108 `get one=1 two=22 three=3 four=4 big=1234567890`, 109 `getunder one=1 two=2 three=3 four=-`, 110 `commit`, 111 `getunder one=1 two=22 three=3 four=4`, 112 }, { 113 // Trivial full doc. 114 `set doc={"one":1,"two":2}`, 115 `get doc={"one":1,"two":2}`, 116 `changes core.doc.one core.doc.two`, 117 }, { 118 // Nulls via dotted path 119 `set doc={"one":1,"two":2}`, 120 `commit`, 121 `set doc.one=null`, 122 `changes core.doc.one`, 123 `get doc={"two":2}`, 124 `getunder doc={"one":1,"two":2}`, 125 `commit`, 126 `get doc={"two":2}`, 127 `getroot ={"doc":{"two":2}}`, 128 `getunder doc={"two":2}`, // nils are not committed to state 129 }, { 130 // Nulls via dotted path, resuling in empty map 131 `set doc={"one":{"three":3},"two":2}`, 132 `set doc.one.three=null`, 133 `changes core.doc.one.three core.doc.two`, 134 `get doc={"one":{},"two":2}`, 135 `commit`, 136 `get doc={"one":{},"two":2}`, 137 `getunder doc={"one":{},"two":2}`, // nils are not committed to state 138 }, { 139 // Nulls via dotted path in a doc 140 `set doc={"one":1,"two":2}`, 141 `set doc.three={"four":4}`, 142 `get doc={"one":1,"two":2,"three":{"four":4}}`, 143 `set doc.three={"four":null}`, 144 `changes core.doc.one core.doc.three.four core.doc.two`, 145 `get doc={"one":1,"two":2,"three":{}}`, 146 `commit`, 147 `get doc={"one":1,"two":2,"three":{}}`, 148 `getunder doc={"one":1,"two":2,"three":{}}`, // nils are not committed to state 149 }, { 150 // Nulls nested in a document 151 `set doc={"one":{"three":3,"two":2}}`, 152 `changes core.doc.one.three core.doc.one.two`, 153 `set doc={"one":{"three":null,"two":2}}`, 154 `changes core.doc.one.three core.doc.one.two`, 155 `get doc={"one":{"two":2}}`, 156 `commit`, 157 `get doc={"one":{"two":2}}`, 158 `getunder doc={"one":{"two":2}}`, // nils are not committed to state 159 }, { 160 // Nulls with mutating 161 `set doc={"one":{"two":2}}`, 162 `set doc.one.two=null`, 163 `changes core.doc.one.two`, 164 `set doc.one="foo"`, 165 `get doc.one="foo"`, 166 `commit`, 167 `get doc={"one":"foo"}`, 168 `getunder doc={"one":"foo"}`, // nils are not committed to state 169 }, { 170 // Nulls, intermediate temporary maps 171 `set doc={"one":{"two":2}}`, 172 `commit`, 173 `set doc.one.three.four.five=null`, 174 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 175 `commit`, 176 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 177 `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state 178 }, { 179 // Nulls, same transaction 180 `set doc={"one":{"two":2}}`, 181 `set doc.one.three.four.five=null`, 182 `changes core.doc.one.three.four.five core.doc.one.two`, 183 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 184 `commit`, 185 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 186 `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state 187 }, { 188 // Null leading to empty doc 189 `set doc={"one":1}`, 190 `set doc.one=null`, 191 `changes core.doc.one`, 192 `commit`, 193 `get doc={}`, 194 }, { 195 // Nulls leading to no snap configuration 196 `set doc="foo"`, 197 `set doc=null`, 198 `changes core.doc`, 199 `commit`, 200 `get doc=-`, 201 `getroot => snap "core" has no configuration`, 202 }, { 203 // set null over non-existing path 204 `set x.y.z=null`, 205 `changes core.x.y.z`, 206 `commit`, 207 `get x.y.z=-`, 208 }, { 209 // set null over non-existing path with initial config 210 `set foo=bar`, 211 `commit`, 212 `set x=null`, 213 `changes core.x`, 214 `commit`, 215 `get x=-`, 216 }, { 217 // Nulls, set then unset and set back over same partial path 218 `set doc.x.a=1`, 219 `commit`, 220 `set doc.x.a=null`, 221 `get doc={"x":{}}}`, 222 `set doc.x.a=6`, 223 `get doc={"x":{"a":6}}`, 224 `commit`, 225 `get doc={"x":{"a":6}}`, 226 `getrootunder ={"doc":{"x":{"a":6}}}`, 227 }, { 228 // Nulls, set then unset and set back over same path 229 `set doc.x.a=1`, 230 `commit`, 231 `set doc.x=null`, 232 `get doc={}`, 233 `set doc.x.a=3`, 234 `get doc={"x":{"a":3}}`, 235 `commit`, 236 `get doc={"x":{"a":3}}`, 237 `getrootunder ={"doc":{"x":{"a":3}}}`, 238 }, { 239 // Nulls, set then unset and set back root element 240 `set doc.x.a=1`, 241 `commit`, 242 `set doc.x=null`, 243 `get doc={}`, 244 `set doc=null`, 245 `get doc=-`, 246 `set doc=99`, 247 `commit`, 248 `get doc=99`, 249 `getrootunder ={"doc":99}`, 250 }, { 251 // Nulls, set then unset over same path 252 `set doc.x.a=1 doc.x.b=2`, 253 `commit`, 254 `set doc.x=null`, 255 `set doc.x.a=null`, 256 `set doc.x.b=null`, 257 `get doc={"x":{}}`, 258 `commit`, 259 `get doc={"x":{}}`, 260 `getrootunder ={"doc":{"x":{}}}`, 261 }, { 262 // Nulls, set then unset and set back over same path 263 `set doc.x.a=1`, 264 `commit`, 265 `set doc.x=null`, 266 `set doc.x.a=null`, 267 `get doc={"x":{}}`, 268 `set doc={"x":{"a":9}}`, 269 `set doc.x.a=1`, 270 `get doc={"x":{"a":1}}`, 271 `commit`, 272 `get doc={"x":{"a":1}}`, 273 `getrootunder ={"doc":{"x":{"a":1}}}`, 274 }, { 275 // Root doc 276 `set doc={"one":1,"two":2}`, 277 `changes core.doc.one core.doc.two`, 278 `getroot ={"doc":{"one":1,"two":2}}`, 279 `commit`, 280 `getroot ={"doc":{"one":1,"two":2}}`, 281 `getrootunder ={"doc":{"one":1,"two":2}}`, 282 }, { 283 // Nested mutations. 284 `set one.two.three=3`, 285 `changes core.one.two.three`, 286 `set one.five=5`, 287 `changes core.one.five core.one.two.three`, 288 `setunder one={"two":{"four":4}}`, 289 `get one={"two":{"three":3},"five":5}`, 290 `get one.two={"three":3}`, 291 `get one.two.three=3`, 292 `get one.five=5`, 293 `commit`, 294 `getunder one={"two":{"three":3,"four":4},"five":5}`, 295 `get one={"two":{"three":3,"four":4},"five":5}`, 296 `get one.two={"three":3,"four":4}`, 297 `get one.two.three=3`, 298 `get one.two.four=4`, 299 `get one.five=5`, 300 }, { 301 // Nested partial update with full get 302 `set one={"two":2,"three":3}`, 303 `commit`, 304 // update just one subkey 305 `set one.two=0`, 306 // both subkeys are returned 307 `get one={"two":0,"three":3}`, 308 `getroot ={"one":{"two":0,"three":3}}`, 309 `get one.two=0`, 310 `get one.three=3`, 311 `getunder one={"two":2,"three":3}`, 312 `changes core.one.two`, 313 `commit`, 314 `getroot ={"one":{"two":0,"three":3}}`, 315 `get one={"two":0,"three":3}`, 316 `getunder one={"two":0,"three":3}`, 317 }, { 318 // Replacement with nested mutation. 319 `set one={"two":{"three":3}}`, 320 `changes core.one.two.three`, 321 `set one.five=5`, 322 `changes core.one.five core.one.two.three`, 323 `get one={"two":{"three":3},"five":5}`, 324 `get one.two={"three":3}`, 325 `get one.two.three=3`, 326 `get one.five=5`, 327 `setunder one={"two":{"four":4},"six":6}`, 328 `commit`, 329 `getunder one={"two":{"three":3},"five":5}`, 330 }, { 331 // Cannot go through known scalar implicitly. 332 `set one.two=2`, 333 `changes core.one.two`, 334 `set one.two.three=3 => snap "core" option "one\.two" is not a map`, 335 `get one.two.three=3 => snap "core" option "one\.two" is not a map`, 336 `get one={"two":2}`, 337 `commit`, 338 `set one.two.three=3 => snap "core" option "one\.two" is not a map`, 339 `get one.two.three=3 => snap "core" option "one\.two" is not a map`, 340 `get one={"two":2}`, 341 `getunder one={"two":2}`, 342 }, { 343 // Unknown scalars may be overwritten though. 344 `setunder one={"two":2}`, 345 `set one.two.three=3`, 346 `changes core.one.two.three`, 347 `commit`, 348 `getunder one={"two":{"three":3}}`, 349 }, { 350 // Invalid option names. 351 `set BAD=1 => invalid option name: "BAD"`, 352 `set 42=1 => invalid option name: "42"`, 353 `set .bad=1 => invalid option name: ""`, 354 `set bad.=1 => invalid option name: ""`, 355 `set bad..bad=1 => invalid option name: ""`, 356 `set one.bad--bad.two=1 => invalid option name: "bad--bad"`, 357 `set one.-bad.two=1 => invalid option name: "-bad"`, 358 `set one.bad-.two=1 => invalid option name: "bad-"`, 359 }} 360 361 func (s *transactionSuite) TestSetGet(c *C) { 362 s.state.Lock() 363 defer s.state.Unlock() 364 365 for _, test := range setGetTests { 366 c.Logf("-----") 367 s.state.Set("config", map[string]interface{}{}) 368 t := config.NewTransaction(s.state) 369 snap := "core" 370 for _, op := range test { 371 c.Logf("%s", op) 372 switch op.kind() { 373 case "set": 374 for k, v := range op.args() { 375 err := t.Set(snap, k, v) 376 if op.fails() { 377 c.Assert(err, ErrorMatches, op.error()) 378 } else { 379 c.Assert(err, IsNil) 380 } 381 } 382 383 case "get": 384 for k, expected := range op.args() { 385 var obtained interface{} 386 err := t.Get(snap, k, &obtained) 387 if op.fails() { 388 c.Assert(err, ErrorMatches, op.error()) 389 var nothing interface{} 390 c.Assert(t.GetMaybe(snap, k, ¬hing), ErrorMatches, op.error()) 391 c.Assert(nothing, IsNil) 392 continue 393 } 394 if expected == "-" { 395 if !config.IsNoOption(err) { 396 c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) 397 } 398 c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k)) 399 var nothing interface{} 400 c.Assert(t.GetMaybe(snap, k, ¬hing), IsNil) 401 c.Assert(nothing, IsNil) 402 continue 403 } 404 c.Assert(err, IsNil) 405 c.Assert(obtained, DeepEquals, expected) 406 407 obtained = nil 408 c.Assert(t.GetMaybe(snap, k, &obtained), IsNil) 409 c.Assert(obtained, DeepEquals, expected) 410 } 411 412 case "commit": 413 t.Commit() 414 415 case "changes": 416 expected := op.list() 417 obtained := t.Changes() 418 c.Check(obtained, DeepEquals, expected) 419 420 case "setunder": 421 var config map[string]map[string]interface{} 422 s.state.Get("config", &config) 423 if config == nil { 424 config = make(map[string]map[string]interface{}) 425 } 426 if config[snap] == nil { 427 config[snap] = make(map[string]interface{}) 428 } 429 for k, v := range op.args() { 430 if v == "-" { 431 delete(config[snap], k) 432 if len(config[snap]) == 0 { 433 delete(config[snap], snap) 434 } 435 } else { 436 config[snap][k] = v 437 } 438 } 439 s.state.Set("config", config) 440 441 case "getunder": 442 var config map[string]map[string]*json.RawMessage 443 s.state.Get("config", &config) 444 for k, expected := range op.args() { 445 obtained, ok := config[snap][k] 446 if expected == "-" { 447 if ok { 448 c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) 449 } 450 continue 451 } 452 var cfg interface{} 453 c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil) 454 c.Assert(cfg, DeepEquals, expected) 455 } 456 case "getroot": 457 var obtained interface{} 458 err := t.Get(snap, "", &obtained) 459 if op.fails() { 460 c.Assert(err, ErrorMatches, op.error()) 461 continue 462 } 463 c.Assert(err, IsNil) 464 c.Assert(obtained, DeepEquals, op.args()[""]) 465 case "getrootunder": 466 var config map[string]*json.RawMessage 467 s.state.Get("config", &config) 468 for _, expected := range op.args() { 469 obtained, ok := config[snap] 470 c.Assert(ok, Equals, true) 471 var cfg interface{} 472 c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil) 473 c.Assert(cfg, DeepEquals, expected) 474 } 475 default: 476 panic("unknown test op kind: " + op.kind()) 477 } 478 } 479 } 480 } 481 482 type brokenType struct { 483 on string 484 } 485 486 func (b *brokenType) UnmarshalJSON(data []byte) error { 487 if b.on == string(data) { 488 return fmt.Errorf("BAM!") 489 } 490 return nil 491 } 492 493 func (s *transactionSuite) TestCommitOverNilSnapConfig(c *C) { 494 s.state.Lock() 495 defer s.state.Unlock() 496 497 // simulate invalid nil map created due to LP #1917870 by snap restore 498 s.state.Set("config", map[string]interface{}{"test-snap": nil}) 499 t := config.NewTransaction(s.state) 500 501 c.Assert(t.Set("test-snap", "foo", "bar"), IsNil) 502 t.Commit() 503 var v string 504 t.Get("test-snap", "foo", &v) 505 c.Assert(v, Equals, "bar") 506 } 507 508 func (s *transactionSuite) TestGetUnmarshalError(c *C) { 509 s.state.Lock() 510 defer s.state.Unlock() 511 c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil) 512 s.transaction.Commit() 513 514 tr := config.NewTransaction(s.state) 515 c.Check(tr.Set("test-snap", "foo", "break"), IsNil) 516 517 // Pristine state is good, value in the transaction breaks. 518 broken := brokenType{`"break"`} 519 err := tr.Get("test-snap", "foo", &broken) 520 c.Assert(err, ErrorMatches, ".*BAM!.*") 521 522 // Pristine state breaks, nothing in the transaction. 523 tr.Commit() 524 err = tr.Get("test-snap", "foo", &broken) 525 c.Assert(err, ErrorMatches, ".*BAM!.*") 526 } 527 528 func (s *transactionSuite) TestNoConfiguration(c *C) { 529 s.state.Lock() 530 defer s.state.Unlock() 531 532 var res interface{} 533 tr := config.NewTransaction(s.state) 534 err := tr.Get("some-snap", "", &res) 535 c.Assert(err, NotNil) 536 c.Assert(config.IsNoOption(err), Equals, true) 537 c.Assert(err, ErrorMatches, `snap "some-snap" has no configuration`) 538 } 539 540 func (s *transactionSuite) TestState(c *C) { 541 s.state.Lock() 542 defer s.state.Unlock() 543 544 tr := config.NewTransaction(s.state) 545 c.Check(tr.State(), DeepEquals, s.state) 546 } 547 548 func (s *transactionSuite) TestPristineIsNotTainted(c *C) { 549 s.state.Lock() 550 defer s.state.Unlock() 551 552 tr := config.NewTransaction(s.state) 553 c.Check(tr.Set("test-snap", "foo.a.a", "a"), IsNil) 554 tr.Commit() 555 556 var data interface{} 557 var result interface{} 558 tr = config.NewTransaction(s.state) 559 c.Check(tr.Set("test-snap", "foo.b", "b"), IsNil) 560 c.Check(tr.Set("test-snap", "foo.a.a", "b"), IsNil) 561 c.Assert(tr.Get("test-snap", "foo", &result), IsNil) 562 c.Check(result, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "b"}, "b": "b"}) 563 564 pristine := tr.PristineConfig() 565 c.Assert(json.Unmarshal([]byte(*pristine["test-snap"]["foo"]), &data), IsNil) 566 c.Assert(data, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "a"}}) 567 } 568 569 func (s *transactionSuite) TestPristineGet(c *C) { 570 s.state.Lock() 571 defer s.state.Unlock() 572 573 // start with a pristine config 574 s.state.Set("config", map[string]map[string]interface{}{ 575 "some-snap": {"opt1": "pristine-value"}, 576 }) 577 578 // change the config 579 var res interface{} 580 tr := config.NewTransaction(s.state) 581 err := tr.Set("some-snap", "opt1", "changed-value") 582 c.Assert(err, IsNil) 583 584 // and get will get the changed value 585 err = tr.Get("some-snap", "opt1", &res) 586 c.Assert(err, IsNil) 587 c.Assert(res, Equals, "changed-value") 588 589 // but GetPristine will get the pristine value 590 err = tr.GetPristine("some-snap", "opt1", &res) 591 c.Assert(err, IsNil) 592 c.Assert(res, Equals, "pristine-value") 593 594 // and GetPristine errors for options that don't exist in pristine 595 var res2 interface{} 596 err = tr.Set("some-snap", "opt2", "other-value") 597 c.Assert(err, IsNil) 598 err = tr.GetPristine("some-snap", "opt2", &res2) 599 c.Assert(err, ErrorMatches, `snap "some-snap" has no "opt2" configuration option`) 600 // but GetPristineMaybe does not error but also give no value 601 err = tr.GetPristineMaybe("some-snap", "opt2", &res2) 602 c.Assert(err, IsNil) 603 c.Assert(res2, IsNil) 604 // but regular get works 605 err = tr.Get("some-snap", "opt2", &res2) 606 c.Assert(err, IsNil) 607 c.Assert(res2, Equals, "other-value") 608 } 609 610 func (s *transactionSuite) TestExternalGetError(c *C) { 611 612 tests := []string{ 613 "/", "..", "รค#!", 614 "a..b", 615 } 616 617 for _, tc := range tests { 618 err := config.RegisterExternalConfig("some-snap", tc, func(key string) (interface{}, error) { 619 return nil, nil 620 }) 621 c.Assert(err, ErrorMatches, "cannot register external config: invalid option name:.*") 622 } 623 } 624 625 func (s *transactionSuite) TestExternalGetSimple(c *C) { 626 s.state.Lock() 627 defer s.state.Unlock() 628 s.state.Set("config", map[string]map[string]interface{}{ 629 "some-snap": { 630 "other-key": "other-value", 631 }, 632 }) 633 634 n := 0 635 err := config.RegisterExternalConfig("some-snap", "key.external", func(key string) (interface{}, error) { 636 n++ 637 638 s := fmt.Sprintf("%s=external-value", key) 639 return s, nil 640 }) 641 c.Assert(err, IsNil) 642 643 tr := config.NewTransaction(s.state) 644 645 var res string 646 // non-external keys work fine 647 err = tr.Get("some-snap", "other-key", &res) 648 c.Assert(err, IsNil) 649 c.Check(res, Equals, "other-value") 650 // no external helper was called because the requested key was not 651 // part of the external configuration 652 c.Check(n, Equals, 0) 653 654 // simple case: subkey is external 655 err = tr.Get("some-snap", "key.external", &res) 656 c.Assert(err, IsNil) 657 c.Check(res, Equals, "key.external=external-value") 658 // the external config function was called now 659 c.Check(n, Equals, 1) 660 } 661 662 func (s *transactionSuite) TestExternalDeepNesting(c *C) { 663 s.state.Lock() 664 defer s.state.Unlock() 665 666 config.RegisterExternalConfig("some-snap", "key.external", func(key string) (interface{}, error) { 667 c.Check(key, Equals, "key.external.subkey") 668 669 m := make(map[string]string) 670 m["subkey"] = fmt.Sprintf("nested-value") 671 m["other-subkey"] = fmt.Sprintf("other-nested-value") 672 673 return m, nil 674 }) 675 676 tr := config.NewTransaction(s.state) 677 var res string 678 err := tr.Get("some-snap", "key.external.subkey", &res) 679 c.Assert(err, IsNil) 680 c.Check(res, Equals, "nested-value") 681 } 682 683 func (s *transactionSuite) TestExternalSetShadowsExternal(c *C) { 684 s.state.Lock() 685 defer s.state.Unlock() 686 687 err := config.RegisterExternalConfig("some-snap", "key.nested.external", func(key string) (interface{}, error) { 688 c.Fatalf("unexpected call to external config function") 689 return nil, nil 690 }) 691 c.Assert(err, IsNil) 692 693 tests := []struct { 694 snap, key string 695 value interface{} 696 isOk bool 697 }{ 698 // "key" must be a map because "key.external" must exist 699 {"some-snap", "key", "non-map-value", false}, 700 {"some-snap", "key.nested", "non-map-value", false}, 701 702 // setting external values directly is fine 703 {"some-snap", "key.nested.external", "some-value", true}, 704 // setting a sub-value of "key" is fine 705 {"some-snap", "key.subkey", "some-value", true}, 706 // setting a sub-value of "key.nested" is fine 707 {"some-snap", "key.nested.subkey", "some-value", true}, 708 // setting the external value itself is fine (of course) 709 {"some-snap", "key.nested.external", "some-value", true}, 710 711 // but setting nested to some map value is fine 712 {"some-snap", "key.nested", map[string]interface{}{}, true}, 713 {"some-snap", "key.nested", map[string]interface{}{"foo": 1}, true}, 714 {"some-snap", "key.nested", map[string]interface{}{"external": 1}, true}, 715 716 // other snaps without external config are not affected 717 {"other-snap", "key", "non-map-value", true}, 718 } 719 720 for _, tc := range tests { 721 tr := config.NewTransaction(s.state) 722 err := tr.Set(tc.snap, tc.key, tc.value) 723 if tc.isOk { 724 c.Check(err, IsNil, Commentf("%v", tc)) 725 } else { 726 c.Check(err, ErrorMatches, fmt.Sprintf(`cannot set %q for "some-snap" to non-map value because "key.nested.external" is a external configuration`, tc.key), Commentf("%v", tc)) 727 } 728 } 729 } 730 731 func (s *transactionSuite) TestExternalGetRootDocIsMerged(c *C) { 732 s.state.Lock() 733 defer s.state.Unlock() 734 s.state.Set("config", map[string]map[string]interface{}{ 735 "some-snap": { 736 "some-key": "some-value", 737 "other-key": "value", 738 }, 739 }) 740 741 n := 0 742 err := config.RegisterExternalConfig("some-snap", "key.external", func(key string) (interface{}, error) { 743 n++ 744 745 s := fmt.Sprintf("%s=external-value", key) 746 return s, nil 747 }) 748 c.Assert(err, IsNil) 749 750 tr := config.NewTransaction(s.state) 751 752 var res map[string]interface{} 753 // the root doc 754 err = tr.Get("some-snap", "", &res) 755 c.Assert(err, IsNil) 756 c.Check(res, DeepEquals, map[string]interface{}{ 757 "some-key": "some-value", 758 "other-key": "value", 759 "key": map[string]interface{}{ 760 "external": "key.external=external-value", 761 }, 762 }) 763 } 764 765 func (s *transactionSuite) TestExternalGetSubtreeMerged(c *C) { 766 s.state.Lock() 767 defer s.state.Unlock() 768 s.state.Set("config", map[string]map[string]interface{}{ 769 "some-snap": { 770 "other-key": "other-value", 771 "real-and-external": map[string]interface{}{ 772 "real": "real-value", 773 }, 774 }, 775 }) 776 777 n := 0 778 err := config.RegisterExternalConfig("some-snap", "real-and-external.external", func(key string) (interface{}, error) { 779 n++ 780 781 s := fmt.Sprintf("%s=external-value", key) 782 return s, nil 783 }) 784 c.Assert(err, IsNil) 785 786 tr := config.NewTransaction(s.state) 787 788 var res string 789 // non-external keys work fine 790 err = tr.Get("some-snap", "other-key", &res) 791 c.Assert(err, IsNil) 792 c.Check(res, Equals, "other-value") 793 // no external helper was called because the requested key was not 794 // part of the external configuration 795 c.Check(n, Equals, 0) 796 797 var res2 map[string]interface{} 798 err = tr.Get("some-snap", "real-and-external", &res2) 799 c.Assert(err, IsNil) 800 c.Check(res2, HasLen, 2) 801 // real 802 c.Check(res2["real"], Equals, "real-value") 803 // and external values are combined 804 c.Check(res2["external"], Equals, "real-and-external.external=external-value") 805 // the external config function was called 806 c.Check(n, Equals, 1) 807 } 808 809 func (s *transactionSuite) TestExternalCommitValuesNotStored(c *C) { 810 s.state.Lock() 811 defer s.state.Unlock() 812 813 err := config.RegisterExternalConfig("some-snap", "simple-external", func(key string) (interface{}, error) { 814 c.Errorf("external func should not get called in this test") 815 return nil, nil 816 }) 817 c.Assert(err, IsNil) 818 err = config.RegisterExternalConfig("some-snap", "key.external", func(key string) (interface{}, error) { 819 c.Errorf("external func should not get called in this test") 820 return nil, nil 821 }) 822 c.Assert(err, IsNil) 823 err = config.RegisterExternalConfig("some-snap", "key.nested.external", func(key string) (interface{}, error) { 824 c.Errorf("external func should not get called in this test") 825 return nil, nil 826 }) 827 c.Assert(err, IsNil) 828 829 tr := config.NewTransaction(s.state) 830 831 // unrelated snap 832 c.Check(tr.Set("other-snap", "key", "value"), IsNil) 833 834 // top level external config with simple value 835 c.Check(tr.Set("some-snap", "simple-external", "will-not-get-set"), IsNil) 836 // top level external config with map 837 c.Check(tr.Set("some-snap", "key.external.a", "1"), IsNil) 838 c.Check(tr.Set("some-snap", "key.external.b", "2"), IsNil) 839 // nested external config 840 c.Check(tr.Set("some-snap", "key.nested.external.sub", "won't-get-set"), IsNil) 841 c.Check(tr.Set("some-snap", "key.nested.external.sub2.sub2sub", "also-won't-get-set"), IsNil) 842 // real configuration 843 c.Check(tr.Set("some-snap", "key.not-external", "value"), IsNil) 844 c.Check(tr.Set("some-snap", "key.nested.not-external", "value"), IsNil) 845 tr.Commit() 846 847 // and check what got stored in the state 848 var config map[string]map[string]interface{} 849 s.state.Get("config", &config) 850 c.Check(config["some-snap"], HasLen, 1) 851 c.Check(config["some-snap"], DeepEquals, map[string]interface{}{ 852 "key": map[string]interface{}{ 853 "not-external": "value", 854 "nested": map[string]interface{}{ 855 "not-external": "value", 856 }, 857 }, 858 }) 859 // other-snap is unrelated 860 c.Check(config["other-snap"]["key"], Equals, "value") 861 } 862 863 func (s *transactionSuite) TestOverlapsWithExternalConfigErr(c *C) { 864 _, err := config.OverlapsWithExternalConfig("invalid#", "valid") 865 c.Check(err, ErrorMatches, `cannot check overlap for requested key: invalid option name: "invalid#"`) 866 867 _, err = config.OverlapsWithExternalConfig("valid", "invalid#") 868 c.Check(err, ErrorMatches, `cannot check overlap for external key: invalid option name: "invalid#"`) 869 } 870 871 func (s *transactionSuite) TestOverlapsWithExternalConfig(c *C) { 872 tests := []struct { 873 requestedKey, externalKey string 874 overlap bool 875 }{ 876 {"a", "a", true}, 877 {"a", "a.external", true}, 878 {"a.external.subkey", "a.external", true}, 879 880 {"a.other", "a.external", false}, 881 {"z", "a", false}, 882 {"z", "a.external", false}, 883 {"z.nested", "a.external", false}, 884 {"z.nested.other", "a.external", false}, 885 } 886 887 for _, tc := range tests { 888 overlap, err := config.OverlapsWithExternalConfig(tc.requestedKey, tc.externalKey) 889 c.Assert(err, IsNil) 890 c.Check(overlap, Equals, tc.overlap, Commentf("%v", tc)) 891 } 892 }