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, &nothing), 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, &nothing), 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  }