gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/hookstate/ctlcmd/set_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 ctlcmd_test 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "strings" 26 27 . "gopkg.in/check.v1" 28 29 "github.com/snapcore/snapd/interfaces" 30 "github.com/snapcore/snapd/overlord/configstate/config" 31 "github.com/snapcore/snapd/overlord/hookstate" 32 "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" 33 "github.com/snapcore/snapd/overlord/hookstate/hooktest" 34 "github.com/snapcore/snapd/overlord/state" 35 "github.com/snapcore/snapd/snap" 36 ) 37 38 type setSuite struct { 39 mockContext *hookstate.Context 40 mockHandler *hooktest.MockHandler 41 } 42 43 type setAttrSuite struct { 44 mockPlugHookContext *hookstate.Context 45 mockSlotHookContext *hookstate.Context 46 mockHandler *hooktest.MockHandler 47 } 48 49 var _ = Suite(&setSuite{}) 50 var _ = Suite(&setAttrSuite{}) 51 52 func (s *setSuite) SetUpTest(c *C) { 53 s.mockHandler = hooktest.NewMockHandler() 54 55 state := state.New(nil) 56 state.Lock() 57 defer state.Unlock() 58 59 task := state.NewTask("test-task", "my test task") 60 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"} 61 62 var err error 63 s.mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "") 64 c.Assert(err, IsNil) 65 } 66 67 func (s *setSuite) TestInvalidArguments(c *C) { 68 _, _, err := ctlcmd.Run(s.mockContext, []string{"set"}, 0) 69 c.Check(err, ErrorMatches, "set which option.*") 70 _, _, err = ctlcmd.Run(s.mockContext, []string{"set", "foo", "bar"}, 0) 71 c.Check(err, ErrorMatches, ".*invalid parameter.*want key=value.*") 72 _, _, err = ctlcmd.Run(s.mockContext, []string{"set", ":foo", "bar=baz"}, 0) 73 c.Check(err, ErrorMatches, ".*interface attributes can only be set during the execution of prepare hooks.*") 74 } 75 76 func (s *setSuite) TestCommand(c *C) { 77 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "foo=bar", "baz=qux"}, 0) 78 c.Check(err, IsNil) 79 c.Check(string(stdout), Equals, "") 80 c.Check(string(stderr), Equals, "") 81 82 // Verify that the previous set doesn't modify the global state 83 s.mockContext.State().Lock() 84 tr := config.NewTransaction(s.mockContext.State()) 85 s.mockContext.State().Unlock() 86 var value string 87 c.Check(tr.Get("test-snap", "foo", &value), ErrorMatches, ".*snap.*has no.*configuration.*") 88 c.Check(tr.Get("test-snap", "baz", &value), ErrorMatches, ".*snap.*has no.*configuration.*") 89 90 // Notify the context that we're done. This should save the config. 91 s.mockContext.Lock() 92 defer s.mockContext.Unlock() 93 c.Check(s.mockContext.Done(), IsNil) 94 95 // Verify that the global config has been updated. 96 tr = config.NewTransaction(s.mockContext.State()) 97 c.Check(tr.Get("test-snap", "foo", &value), IsNil) 98 c.Check(value, Equals, "bar") 99 c.Check(tr.Get("test-snap", "baz", &value), IsNil) 100 c.Check(value, Equals, "qux") 101 } 102 103 func (s *setSuite) TestSetRegularUserForbidden(c *C) { 104 _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key1"}, 1000) 105 c.Assert(err, ErrorMatches, `cannot use "set" with uid 1000, try with sudo`) 106 forbidden, _ := err.(*ctlcmd.ForbiddenCommandError) 107 c.Assert(forbidden, NotNil) 108 } 109 110 func (s *setSuite) TestSetHelpRegularUserAllowed(c *C) { 111 _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "-h"}, 1000) 112 c.Assert(err, NotNil) 113 c.Assert(strings.HasPrefix(err.Error(), "Usage:"), Equals, true) 114 } 115 116 func (s *setSuite) TestSetConfigOptionWithColon(c *C) { 117 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "device-service.url=192.168.0.1:5555"}, 0) 118 c.Check(err, IsNil) 119 c.Check(string(stdout), Equals, "") 120 c.Check(string(stderr), Equals, "") 121 122 // Notify the context that we're done. This should save the config. 123 s.mockContext.Lock() 124 defer s.mockContext.Unlock() 125 c.Check(s.mockContext.Done(), IsNil) 126 127 // Verify that the global config has been updated. 128 var value string 129 tr := config.NewTransaction(s.mockContext.State()) 130 c.Check(tr.Get("test-snap", "device-service.url", &value), IsNil) 131 c.Check(value, Equals, "192.168.0.1:5555") 132 } 133 134 func (s *setSuite) TestUnsetConfigOptionWithInitialConfiguration(c *C) { 135 // Setup an initial configuration 136 s.mockContext.State().Lock() 137 tr := config.NewTransaction(s.mockContext.State()) 138 tr.Set("test-snap", "test-key1", "test-value1") 139 tr.Set("test-snap", "test-key2", "test-value2") 140 tr.Set("test-snap", "test-key3.foo", "foo-value") 141 tr.Set("test-snap", "test-key3.bar", "bar-value") 142 tr.Commit() 143 s.mockContext.State().Unlock() 144 145 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key1!", "test-key3.foo!"}, 0) 146 c.Check(err, IsNil) 147 c.Check(string(stdout), Equals, "") 148 c.Check(string(stderr), Equals, "") 149 150 // Notify the context that we're done. This should save the config. 151 s.mockContext.Lock() 152 defer s.mockContext.Unlock() 153 c.Check(s.mockContext.Done(), IsNil) 154 155 // Verify that the global config has been updated. 156 var value string 157 tr = config.NewTransaction(s.mockContext.State()) 158 c.Check(tr.Get("test-snap", "test-key2", &value), IsNil) 159 c.Check(value, Equals, "test-value2") 160 c.Check(tr.Get("test-snap", "test-key1", &value), ErrorMatches, `snap "test-snap" has no "test-key1" configuration option`) 161 var value2 interface{} 162 c.Check(tr.Get("test-snap", "test-key3", &value2), IsNil) 163 c.Check(value2, DeepEquals, map[string]interface{}{"bar": "bar-value"}) 164 } 165 166 func (s *setSuite) TestUnsetConfigOptionWithNoInitialConfiguration(c *C) { 167 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key.key1=value1", "test-key.key2=value2", "test-key.key1!"}, 0) 168 c.Check(err, IsNil) 169 c.Check(string(stdout), Equals, "") 170 c.Check(string(stderr), Equals, "") 171 172 // Notify the context that we're done. This should save the config. 173 s.mockContext.Lock() 174 defer s.mockContext.Unlock() 175 c.Check(s.mockContext.Done(), IsNil) 176 177 // Verify that the global config has been updated. 178 var value interface{} 179 tr := config.NewTransaction(s.mockContext.State()) 180 c.Check(tr.Get("test-snap", "test-key.key2", &value), IsNil) 181 c.Check(value, DeepEquals, "value2") 182 c.Check(tr.Get("test-snap", "test-key.key1", &value), ErrorMatches, `snap "test-snap" has no "test-key.key1" configuration option`) 183 c.Check(value, DeepEquals, "value2") 184 } 185 186 func (s *setSuite) TestSetNumbers(c *C) { 187 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "foo=1234567890", "bar=123456.7890"}, 0) 188 c.Check(err, IsNil) 189 c.Check(string(stdout), Equals, "") 190 c.Check(string(stderr), Equals, "") 191 192 // Notify the context that we're done. This should save the config. 193 s.mockContext.Lock() 194 defer s.mockContext.Unlock() 195 c.Check(s.mockContext.Done(), IsNil) 196 197 // Verify that the global config has been updated. 198 var value interface{} 199 tr := config.NewTransaction(s.mockContext.State()) 200 c.Check(tr.Get("test-snap", "foo", &value), IsNil) 201 c.Check(value, Equals, json.Number("1234567890")) 202 203 c.Check(tr.Get("test-snap", "bar", &value), IsNil) 204 c.Check(value, Equals, json.Number("123456.7890")) 205 } 206 207 func (s *setSuite) TestSetStrictJSON(c *C) { 208 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "-t", `key={"a":"b", "c": 1, "d": {"e":"f"}}`}, 0) 209 c.Assert(err, IsNil) 210 c.Check(string(stdout), Equals, "") 211 c.Check(string(stderr), Equals, "") 212 213 // Notify the context that we're done. This should save the config. 214 s.mockContext.Lock() 215 defer s.mockContext.Unlock() 216 c.Check(s.mockContext.Done(), IsNil) 217 218 // Verify that the global config has been updated. 219 var value interface{} 220 tr := config.NewTransaction(s.mockContext.State()) 221 c.Assert(tr.Get("test-snap", "key", &value), IsNil) 222 c.Check(value, DeepEquals, map[string]interface{}{"a": "b", "c": json.Number("1"), "d": map[string]interface{}{"e": "f"}}) 223 } 224 225 func (s *setSuite) TestSetFailWithStrictJSON(c *C) { 226 _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "-t", `key=a`}, 0) 227 c.Assert(err, ErrorMatches, "failed to parse JSON:.*") 228 } 229 230 func (s *setSuite) TestSetAsString(c *C) { 231 expected := `{"a":"b", "c": 1, "d": {"e": "f"}}` 232 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "-s", fmt.Sprintf("key=%s", expected)}, 0) 233 c.Assert(err, IsNil) 234 c.Check(string(stdout), Equals, "") 235 c.Check(string(stderr), Equals, "") 236 237 // Notify the context that we're done. This should save the config. 238 s.mockContext.Lock() 239 defer s.mockContext.Unlock() 240 c.Check(s.mockContext.Done(), IsNil) 241 242 // Verify that the global config has been updated. 243 var value interface{} 244 tr := config.NewTransaction(s.mockContext.State()) 245 c.Assert(tr.Get("test-snap", "key", &value), IsNil) 246 c.Check(value, Equals, expected) 247 } 248 249 func (s *setSuite) TestSetErrorOnStrictJSONAndString(c *C) { 250 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "-s", "-t", `{"a":"b"}`}, 0) 251 c.Assert(err, ErrorMatches, "cannot use -t and -s together") 252 c.Check(string(stdout), Equals, "") 253 c.Check(string(stderr), Equals, "") 254 } 255 256 func (s *setSuite) TestCommandSavesDeltasOnly(c *C) { 257 // Setup an initial configuration 258 s.mockContext.State().Lock() 259 tr := config.NewTransaction(s.mockContext.State()) 260 tr.Set("test-snap", "test-key1", "test-value1") 261 tr.Set("test-snap", "test-key2", "test-value2") 262 tr.Commit() 263 s.mockContext.State().Unlock() 264 265 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key2=test-value3"}, 0) 266 c.Check(err, IsNil) 267 c.Check(string(stdout), Equals, "") 268 c.Check(string(stderr), Equals, "") 269 270 // Notify the context that we're done. This should save the config. 271 s.mockContext.Lock() 272 defer s.mockContext.Unlock() 273 c.Check(s.mockContext.Done(), IsNil) 274 275 // Verify that the global config has been updated, but only test-key2 276 tr = config.NewTransaction(s.mockContext.State()) 277 var value string 278 c.Check(tr.Get("test-snap", "test-key1", &value), IsNil) 279 c.Check(value, Equals, "test-value1") 280 c.Check(tr.Get("test-snap", "test-key2", &value), IsNil) 281 c.Check(value, Equals, "test-value3") 282 } 283 284 func (s *setSuite) TestCommandWithoutContext(c *C) { 285 _, _, err := ctlcmd.Run(nil, []string{"set", "foo=bar"}, 0) 286 c.Check(err, ErrorMatches, `cannot invoke snapctl operation commands \(here "set"\) from outside of a snap`) 287 } 288 289 func (s *setAttrSuite) SetUpTest(c *C) { 290 s.mockHandler = hooktest.NewMockHandler() 291 state := state.New(nil) 292 state.Lock() 293 ch := state.NewChange("mychange", "mychange") 294 295 attrsTask := state.NewTask("connect-task", "my connect task") 296 attrsTask.Set("plug", &interfaces.PlugRef{Snap: "a", Name: "aplug"}) 297 attrsTask.Set("slot", &interfaces.SlotRef{Snap: "b", Name: "bslot"}) 298 staticAttrs := map[string]interface{}{ 299 "lorem": "ipsum", 300 "nested": map[string]interface{}{ 301 "x": "y", 302 }, 303 } 304 dynamicAttrs := make(map[string]interface{}) 305 attrsTask.Set("plug-static", staticAttrs) 306 attrsTask.Set("plug-dynamic", dynamicAttrs) 307 attrsTask.Set("slot-static", staticAttrs) 308 attrsTask.Set("slot-dynamic", dynamicAttrs) 309 ch.AddTask(attrsTask) 310 state.Unlock() 311 312 var err error 313 314 // setup plug hook task 315 state.Lock() 316 plugHookTask := state.NewTask("run-hook", "my test task") 317 state.Unlock() 318 plugTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "prepare-plug-aplug"} 319 s.mockPlugHookContext, err = hookstate.NewContext(plugHookTask, plugHookTask.State(), plugTaskSetup, s.mockHandler, "") 320 c.Assert(err, IsNil) 321 322 s.mockPlugHookContext.Lock() 323 s.mockPlugHookContext.Set("attrs-task", attrsTask.ID()) 324 s.mockPlugHookContext.Unlock() 325 state.Lock() 326 ch.AddTask(plugHookTask) 327 state.Unlock() 328 329 // setup slot hook task 330 state.Lock() 331 slotHookTask := state.NewTask("run-hook", "my test task") 332 state.Unlock() 333 slotTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "prepare-slot-aplug"} 334 s.mockSlotHookContext, err = hookstate.NewContext(slotHookTask, slotHookTask.State(), slotTaskSetup, s.mockHandler, "") 335 c.Assert(err, IsNil) 336 337 s.mockSlotHookContext.Lock() 338 s.mockSlotHookContext.Set("attrs-task", attrsTask.ID()) 339 s.mockSlotHookContext.Unlock() 340 341 state.Lock() 342 defer state.Unlock() 343 ch.AddTask(slotHookTask) 344 } 345 346 func (s *setAttrSuite) TestSetPlugAttributesInPlugHook(c *C) { 347 stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":aplug", "foo=bar"}, 0) 348 c.Check(err, IsNil) 349 c.Check(string(stdout), Equals, "") 350 c.Check(string(stderr), Equals, "") 351 352 attrsTask, err := ctlcmd.AttributesTask(s.mockPlugHookContext) 353 c.Assert(err, IsNil) 354 st := s.mockPlugHookContext.State() 355 st.Lock() 356 defer st.Unlock() 357 dynattrs := make(map[string]interface{}) 358 err = attrsTask.Get("plug-dynamic", &dynattrs) 359 c.Assert(err, IsNil) 360 c.Check(dynattrs["foo"], Equals, "bar") 361 } 362 363 func (s *setAttrSuite) TestSetPlugAttributesSupportsDottedSyntax(c *C) { 364 stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":aplug", "my.attr1=foo", "my.attr2=bar"}, 0) 365 c.Check(err, IsNil) 366 c.Check(string(stdout), Equals, "") 367 c.Check(string(stderr), Equals, "") 368 369 attrsTask, err := ctlcmd.AttributesTask(s.mockPlugHookContext) 370 c.Assert(err, IsNil) 371 st := s.mockPlugHookContext.State() 372 st.Lock() 373 defer st.Unlock() 374 dynattrs := make(map[string]interface{}) 375 err = attrsTask.Get("plug-dynamic", &dynattrs) 376 c.Assert(err, IsNil) 377 c.Check(dynattrs["my"], DeepEquals, map[string]interface{}{"attr1": "foo", "attr2": "bar"}) 378 } 379 380 func (s *setAttrSuite) TestPlugOrSlotEmpty(c *C) { 381 stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":", "foo=bar"}, 0) 382 c.Check(err, ErrorMatches, "plug or slot name not provided") 383 c.Check(string(stdout), Equals, "") 384 c.Check(string(stderr), Equals, "") 385 } 386 387 func (s *setAttrSuite) TestSetCommandFailsOutsideOfValidContext(c *C) { 388 var err error 389 var mockContext *hookstate.Context 390 391 state := state.New(nil) 392 state.Lock() 393 defer state.Unlock() 394 395 task := state.NewTask("test-task", "my test task") 396 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "not-a-connect-hook"} 397 mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "") 398 c.Assert(err, IsNil) 399 400 stdout, stderr, err := ctlcmd.Run(mockContext, []string{"set", ":aplug", "foo=bar"}, 0) 401 c.Check(err, ErrorMatches, `interface attributes can only be set during the execution of prepare hooks`) 402 c.Check(string(stdout), Equals, "") 403 c.Check(string(stderr), Equals, "") 404 }