github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  	// Root doc
   216  	`set doc={"one":1,"two":2}`,
   217  	`changes core.doc.one core.doc.two`,
   218  	`getroot ={"doc":{"one":1,"two":2}}`,
   219  	`commit`,
   220  	`getroot ={"doc":{"one":1,"two":2}}`,
   221  	`getrootunder ={"doc":{"one":1,"two":2}}`,
   222  }, {
   223  	// Nested mutations.
   224  	`set one.two.three=3`,
   225  	`changes core.one.two.three`,
   226  	`set one.five=5`,
   227  	`changes core.one.five core.one.two.three`,
   228  	`setunder one={"two":{"four":4}}`,
   229  	`get one={"two":{"three":3},"five":5}`,
   230  	`get one.two={"three":3}`,
   231  	`get one.two.three=3`,
   232  	`get one.five=5`,
   233  	`commit`,
   234  	`getunder one={"two":{"three":3,"four":4},"five":5}`,
   235  	`get one={"two":{"three":3,"four":4},"five":5}`,
   236  	`get one.two={"three":3,"four":4}`,
   237  	`get one.two.three=3`,
   238  	`get one.two.four=4`,
   239  	`get one.five=5`,
   240  }, {
   241  	// Nested partial update with full get
   242  	`set one={"two":2,"three":3}`,
   243  	`commit`,
   244  	// update just one subkey
   245  	`set one.two=0`,
   246  	// both subkeys are returned
   247  	`get one={"two":0,"three":3}`,
   248  	`getroot ={"one":{"two":0,"three":3}}`,
   249  	`get one.two=0`,
   250  	`get one.three=3`,
   251  	`getunder one={"two":2,"three":3}`,
   252  	`changes core.one.two`,
   253  	`commit`,
   254  	`getroot ={"one":{"two":0,"three":3}}`,
   255  	`get one={"two":0,"three":3}`,
   256  	`getunder one={"two":0,"three":3}`,
   257  }, {
   258  	// Replacement with nested mutation.
   259  	`set one={"two":{"three":3}}`,
   260  	`changes core.one.two.three`,
   261  	`set one.five=5`,
   262  	`changes core.one.five core.one.two.three`,
   263  	`get one={"two":{"three":3},"five":5}`,
   264  	`get one.two={"three":3}`,
   265  	`get one.two.three=3`,
   266  	`get one.five=5`,
   267  	`setunder one={"two":{"four":4},"six":6}`,
   268  	`commit`,
   269  	`getunder one={"two":{"three":3},"five":5}`,
   270  }, {
   271  	// Cannot go through known scalar implicitly.
   272  	`set one.two=2`,
   273  	`changes core.one.two`,
   274  	`set one.two.three=3 => snap "core" option "one\.two" is not a map`,
   275  	`get one.two.three=3 => snap "core" option "one\.two" is not a map`,
   276  	`get one={"two":2}`,
   277  	`commit`,
   278  	`set one.two.three=3 => snap "core" option "one\.two" is not a map`,
   279  	`get one.two.three=3 => snap "core" option "one\.two" is not a map`,
   280  	`get one={"two":2}`,
   281  	`getunder one={"two":2}`,
   282  }, {
   283  	// Unknown scalars may be overwritten though.
   284  	`setunder one={"two":2}`,
   285  	`set one.two.three=3`,
   286  	`changes core.one.two.three`,
   287  	`commit`,
   288  	`getunder one={"two":{"three":3}}`,
   289  }, {
   290  	// Invalid option names.
   291  	`set BAD=1 => invalid option name: "BAD"`,
   292  	`set 42=1 => invalid option name: "42"`,
   293  	`set .bad=1 => invalid option name: ""`,
   294  	`set bad.=1 => invalid option name: ""`,
   295  	`set bad..bad=1 => invalid option name: ""`,
   296  	`set one.bad--bad.two=1 => invalid option name: "bad--bad"`,
   297  	`set one.-bad.two=1 => invalid option name: "-bad"`,
   298  	`set one.bad-.two=1 => invalid option name: "bad-"`,
   299  }}
   300  
   301  func (s *transactionSuite) TestSetGet(c *C) {
   302  	s.state.Lock()
   303  	defer s.state.Unlock()
   304  
   305  	for _, test := range setGetTests {
   306  		c.Logf("-----")
   307  		s.state.Set("config", map[string]interface{}{})
   308  		t := config.NewTransaction(s.state)
   309  		snap := "core"
   310  		for _, op := range test {
   311  			c.Logf("%s", op)
   312  			switch op.kind() {
   313  			case "set":
   314  				for k, v := range op.args() {
   315  					err := t.Set(snap, k, v)
   316  					if op.fails() {
   317  						c.Assert(err, ErrorMatches, op.error())
   318  					} else {
   319  						c.Assert(err, IsNil)
   320  					}
   321  				}
   322  
   323  			case "get":
   324  				for k, expected := range op.args() {
   325  					var obtained interface{}
   326  					err := t.Get(snap, k, &obtained)
   327  					if op.fails() {
   328  						c.Assert(err, ErrorMatches, op.error())
   329  						var nothing interface{}
   330  						c.Assert(t.GetMaybe(snap, k, &nothing), ErrorMatches, op.error())
   331  						c.Assert(nothing, IsNil)
   332  						continue
   333  					}
   334  					if expected == "-" {
   335  						if !config.IsNoOption(err) {
   336  							c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained)
   337  						}
   338  						c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k))
   339  						var nothing interface{}
   340  						c.Assert(t.GetMaybe(snap, k, &nothing), IsNil)
   341  						c.Assert(nothing, IsNil)
   342  						continue
   343  					}
   344  					c.Assert(err, IsNil)
   345  					c.Assert(obtained, DeepEquals, expected)
   346  
   347  					obtained = nil
   348  					c.Assert(t.GetMaybe(snap, k, &obtained), IsNil)
   349  					c.Assert(obtained, DeepEquals, expected)
   350  				}
   351  
   352  			case "commit":
   353  				t.Commit()
   354  
   355  			case "changes":
   356  				expected := op.list()
   357  				obtained := t.Changes()
   358  				c.Check(obtained, DeepEquals, expected)
   359  
   360  			case "setunder":
   361  				var config map[string]map[string]interface{}
   362  				s.state.Get("config", &config)
   363  				if config == nil {
   364  					config = make(map[string]map[string]interface{})
   365  				}
   366  				if config[snap] == nil {
   367  					config[snap] = make(map[string]interface{})
   368  				}
   369  				for k, v := range op.args() {
   370  					if v == "-" {
   371  						delete(config[snap], k)
   372  						if len(config[snap]) == 0 {
   373  							delete(config[snap], snap)
   374  						}
   375  					} else {
   376  						config[snap][k] = v
   377  					}
   378  				}
   379  				s.state.Set("config", config)
   380  
   381  			case "getunder":
   382  				var config map[string]map[string]*json.RawMessage
   383  				s.state.Get("config", &config)
   384  				for k, expected := range op.args() {
   385  					obtained, ok := config[snap][k]
   386  					if expected == "-" {
   387  						if ok {
   388  							c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained)
   389  						}
   390  						continue
   391  					}
   392  					var cfg interface{}
   393  					c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil)
   394  					c.Assert(cfg, DeepEquals, expected)
   395  				}
   396  			case "getroot":
   397  				var obtained interface{}
   398  				err := t.Get(snap, "", &obtained)
   399  				if op.fails() {
   400  					c.Assert(err, ErrorMatches, op.error())
   401  					continue
   402  				}
   403  				c.Assert(err, IsNil)
   404  				c.Assert(obtained, DeepEquals, op.args()[""])
   405  			case "getrootunder":
   406  				var config map[string]*json.RawMessage
   407  				s.state.Get("config", &config)
   408  				for _, expected := range op.args() {
   409  					obtained, ok := config[snap]
   410  					c.Assert(ok, Equals, true)
   411  					var cfg interface{}
   412  					c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil)
   413  					c.Assert(cfg, DeepEquals, expected)
   414  				}
   415  			default:
   416  				panic("unknown test op kind: " + op.kind())
   417  			}
   418  		}
   419  	}
   420  }
   421  
   422  type brokenType struct {
   423  	on string
   424  }
   425  
   426  func (b *brokenType) UnmarshalJSON(data []byte) error {
   427  	if b.on == string(data) {
   428  		return fmt.Errorf("BAM!")
   429  	}
   430  	return nil
   431  }
   432  
   433  func (s *transactionSuite) TestGetUnmarshalError(c *C) {
   434  	s.state.Lock()
   435  	defer s.state.Unlock()
   436  	c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil)
   437  	s.transaction.Commit()
   438  
   439  	tr := config.NewTransaction(s.state)
   440  	c.Check(tr.Set("test-snap", "foo", "break"), IsNil)
   441  
   442  	// Pristine state is good, value in the transaction breaks.
   443  	broken := brokenType{`"break"`}
   444  	err := tr.Get("test-snap", "foo", &broken)
   445  	c.Assert(err, ErrorMatches, ".*BAM!.*")
   446  
   447  	// Pristine state breaks, nothing in the transaction.
   448  	tr.Commit()
   449  	err = tr.Get("test-snap", "foo", &broken)
   450  	c.Assert(err, ErrorMatches, ".*BAM!.*")
   451  }
   452  
   453  func (s *transactionSuite) TestNoConfiguration(c *C) {
   454  	s.state.Lock()
   455  	defer s.state.Unlock()
   456  
   457  	var res interface{}
   458  	tr := config.NewTransaction(s.state)
   459  	err := tr.Get("some-snap", "", &res)
   460  	c.Assert(err, NotNil)
   461  	c.Assert(config.IsNoOption(err), Equals, true)
   462  	c.Assert(err, ErrorMatches, `snap "some-snap" has no configuration`)
   463  }
   464  
   465  func (s *transactionSuite) TestState(c *C) {
   466  	s.state.Lock()
   467  	defer s.state.Unlock()
   468  
   469  	tr := config.NewTransaction(s.state)
   470  	c.Check(tr.State(), DeepEquals, s.state)
   471  }
   472  
   473  func (s *transactionSuite) TestPristineIsNotTainted(c *C) {
   474  	s.state.Lock()
   475  	defer s.state.Unlock()
   476  
   477  	tr := config.NewTransaction(s.state)
   478  	c.Check(tr.Set("test-snap", "foo.a.a", "a"), IsNil)
   479  	tr.Commit()
   480  
   481  	var data interface{}
   482  	var result interface{}
   483  	tr = config.NewTransaction(s.state)
   484  	c.Check(tr.Set("test-snap", "foo.b", "b"), IsNil)
   485  	c.Check(tr.Set("test-snap", "foo.a.a", "b"), IsNil)
   486  	c.Assert(tr.Get("test-snap", "foo", &result), IsNil)
   487  	c.Check(result, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "b"}, "b": "b"})
   488  
   489  	pristine := tr.PristineConfig()
   490  	c.Assert(json.Unmarshal([]byte(*pristine["test-snap"]["foo"]), &data), IsNil)
   491  	c.Assert(data, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "a"}})
   492  }
   493  
   494  func (s *transactionSuite) TestPristineGet(c *C) {
   495  	s.state.Lock()
   496  	defer s.state.Unlock()
   497  
   498  	// start with a pristine config
   499  	s.state.Set("config", map[string]map[string]interface{}{
   500  		"some-snap": {"opt1": "pristine-value"},
   501  	})
   502  
   503  	// change the config
   504  	var res interface{}
   505  	tr := config.NewTransaction(s.state)
   506  	err := tr.Set("some-snap", "opt1", "changed-value")
   507  	c.Assert(err, IsNil)
   508  
   509  	// and get will get the changed value
   510  	err = tr.Get("some-snap", "opt1", &res)
   511  	c.Assert(err, IsNil)
   512  	c.Assert(res, Equals, "changed-value")
   513  
   514  	// but GetPristine will get the pristine value
   515  	err = tr.GetPristine("some-snap", "opt1", &res)
   516  	c.Assert(err, IsNil)
   517  	c.Assert(res, Equals, "pristine-value")
   518  
   519  	// and GetPristine errors for options that don't exist in pristine
   520  	var res2 interface{}
   521  	err = tr.Set("some-snap", "opt2", "other-value")
   522  	c.Assert(err, IsNil)
   523  	err = tr.GetPristine("some-snap", "opt2", &res2)
   524  	c.Assert(err, ErrorMatches, `snap "some-snap" has no "opt2" configuration option`)
   525  	// but GetPristineMaybe does not error but also give no value
   526  	err = tr.GetPristineMaybe("some-snap", "opt2", &res2)
   527  	c.Assert(err, IsNil)
   528  	c.Assert(res2, IsNil)
   529  	// but regular get works
   530  	err = tr.Get("some-snap", "opt2", &res2)
   531  	c.Assert(err, IsNil)
   532  	c.Assert(res2, Equals, "other-value")
   533  }