gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/hookstate/ctlcmd/refresh_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 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 "fmt" 24 "time" 25 26 . "gopkg.in/check.v1" 27 28 "github.com/snapcore/snapd/dirs" 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/ifacestate/ifacerepo" 35 "github.com/snapcore/snapd/overlord/snapstate" 36 "github.com/snapcore/snapd/overlord/state" 37 "github.com/snapcore/snapd/snap" 38 "github.com/snapcore/snapd/testutil" 39 ) 40 41 type refreshSuite struct { 42 testutil.BaseTest 43 st *state.State 44 mockHandler *hooktest.MockHandler 45 } 46 47 var _ = Suite(&refreshSuite{}) 48 49 func mockRefreshCandidate(snapName, instanceKey, channel, version string, revision snap.Revision) interface{} { 50 sup := &snapstate.SnapSetup{ 51 Channel: channel, 52 InstanceKey: instanceKey, 53 SideInfo: &snap.SideInfo{ 54 Revision: revision, 55 RealName: snapName, 56 }, 57 } 58 return snapstate.MockRefreshCandidate(sup, version) 59 } 60 61 func (s *refreshSuite) SetUpTest(c *C) { 62 s.BaseTest.SetUpTest(c) 63 dirs.SetRootDir(c.MkDir()) 64 s.AddCleanup(func() { dirs.SetRootDir("/") }) 65 s.st = state.New(nil) 66 s.mockHandler = hooktest.NewMockHandler() 67 68 // snapstate.AffectedByRefreshCandidates needs a cached iface repo 69 repo := interfaces.NewRepository() 70 // no interfaces needed for this test suite 71 s.st.Lock() 72 defer s.st.Unlock() 73 ifacerepo.Replace(s.st, repo) 74 } 75 76 var refreshFromHookTests = []struct { 77 args []string 78 base, restart bool 79 inhibited bool 80 refreshCandidates map[string]interface{} 81 stdout, stderr, err string 82 exitCode int 83 }{{ 84 args: []string{"refresh", "--proceed", "--hold"}, 85 err: "cannot use --proceed and --hold together", 86 }, { 87 args: []string{"refresh", "--pending"}, 88 refreshCandidates: map[string]interface{}{ 89 "snap1": mockRefreshCandidate("snap1", "", "edge", "v1", snap.Revision{N: 3}), 90 }, 91 stdout: "pending: ready\nchannel: edge\nversion: v1\nrevision: 3\nbase: false\nrestart: false\n", 92 }, { 93 args: []string{"refresh", "--pending"}, 94 stdout: "pending: none\nchannel: stable\nbase: false\nrestart: false\n", 95 }, { 96 args: []string{"refresh", "--pending"}, 97 refreshCandidates: map[string]interface{}{ 98 "snap1-base": mockRefreshCandidate("snap1-base", "", "edge", "v1", snap.Revision{N: 3}), 99 }, 100 stdout: "pending: none\nchannel: stable\nbase: true\nrestart: false\n", 101 }, { 102 args: []string{"refresh", "--pending"}, 103 refreshCandidates: map[string]interface{}{ 104 "kernel": mockRefreshCandidate("kernel", "", "edge", "v1", snap.Revision{N: 3}), 105 }, 106 stdout: "pending: none\nchannel: stable\nbase: false\nrestart: true\n", 107 }, { 108 args: []string{"refresh", "--pending"}, 109 inhibited: true, 110 stdout: "pending: inhibited\nchannel: stable\nbase: false\nrestart: false\n", 111 }, { 112 args: []string{"refresh", "--hold"}, 113 err: `internal error: snap "snap1" is not affected by any snaps`, 114 }} 115 116 func (s *refreshSuite) TestRefreshFromHook(c *C) { 117 s.st.Lock() 118 task := s.st.NewTask("test-task", "my test task") 119 setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"} 120 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 121 c.Check(err, IsNil) 122 mockInstalledSnap(c, s.st, `name: snap1 123 base: snap1-base 124 version: 1 125 hooks: 126 gate-auto-refresh: 127 `) 128 mockInstalledSnap(c, s.st, `name: snap1-base 129 type: base 130 version: 1 131 `) 132 mockInstalledSnap(c, s.st, `name: kernel 133 type: kernel 134 version: 1 135 `) 136 s.st.Unlock() 137 138 for _, test := range refreshFromHookTests { 139 s.st.Lock() 140 s.st.Set("refresh-candidates", test.refreshCandidates) 141 if test.inhibited { 142 var snapst snapstate.SnapState 143 c.Assert(snapstate.Get(s.st, "snap1", &snapst), IsNil) 144 snapst.RefreshInhibitedTime = &time.Time{} 145 snapstate.Set(s.st, "snap1", &snapst) 146 } 147 s.st.Unlock() 148 149 stdout, stderr, err := ctlcmd.Run(mockContext, test.args, 0) 150 comment := Commentf("%s", test.args) 151 if test.exitCode > 0 { 152 c.Check(err, DeepEquals, &ctlcmd.UnsuccessfulError{ExitCode: test.exitCode}, comment) 153 } else { 154 if test.err == "" { 155 c.Check(err, IsNil, comment) 156 } else { 157 c.Check(err, ErrorMatches, test.err, comment) 158 } 159 } 160 161 c.Check(string(stdout), Equals, test.stdout, comment) 162 c.Check(string(stderr), Equals, "", comment) 163 } 164 } 165 166 func (s *refreshSuite) TestRefreshHold(c *C) { 167 s.st.Lock() 168 task := s.st.NewTask("test-task", "my test task") 169 setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"} 170 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 171 c.Check(err, IsNil) 172 173 mockInstalledSnap(c, s.st, `name: snap1 174 base: snap1-base 175 version: 1 176 hooks: 177 gate-auto-refresh: 178 `) 179 mockInstalledSnap(c, s.st, `name: snap1-base 180 type: base 181 version: 1 182 `) 183 184 candidates := map[string]interface{}{ 185 "snap1-base": mockRefreshCandidate("snap1-base", "", "edge", "v1", snap.Revision{N: 3}), 186 } 187 s.st.Set("refresh-candidates", candidates) 188 189 s.st.Unlock() 190 191 stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--hold"}, 0) 192 c.Assert(err, IsNil) 193 c.Check(string(stdout), Equals, "") 194 c.Check(string(stderr), Equals, "") 195 196 mockContext.Lock() 197 defer mockContext.Unlock() 198 action := mockContext.Cached("action") 199 c.Assert(action, NotNil) 200 c.Check(action, Equals, snapstate.GateAutoRefreshHold) 201 202 var gating map[string]map[string]interface{} 203 c.Assert(s.st.Get("snaps-hold", &gating), IsNil) 204 c.Check(gating["snap1-base"]["snap1"], NotNil) 205 } 206 207 func (s *refreshSuite) TestRefreshProceed(c *C) { 208 s.st.Lock() 209 task := s.st.NewTask("test-task", "my test task") 210 setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"} 211 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 212 c.Check(err, IsNil) 213 214 mockInstalledSnap(c, s.st, `name: foo 215 version: 1 216 `) 217 218 // pretend snap foo is held initially 219 c.Check(snapstate.HoldRefresh(s.st, "snap1", 0, "foo"), IsNil) 220 s.st.Unlock() 221 222 // sanity check 223 var gating map[string]map[string]interface{} 224 s.st.Lock() 225 snapsHold := s.st.Get("snaps-hold", &gating) 226 s.st.Unlock() 227 c.Assert(snapsHold, IsNil) 228 c.Check(gating["foo"]["snap1"], NotNil) 229 230 mockContext.Lock() 231 mockContext.Set("affecting-snaps", []string{"foo"}) 232 mockContext.Unlock() 233 234 stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0) 235 c.Assert(err, IsNil) 236 c.Check(string(stdout), Equals, "") 237 c.Check(string(stderr), Equals, "") 238 239 mockContext.Lock() 240 defer mockContext.Unlock() 241 action := mockContext.Cached("action") 242 c.Assert(action, NotNil) 243 c.Check(action, Equals, snapstate.GateAutoRefreshProceed) 244 245 // and it is still held (for hook handler to execute actual proceed logic). 246 gating = nil 247 c.Assert(s.st.Get("snaps-hold", &gating), IsNil) 248 c.Check(gating["foo"]["snap1"], NotNil) 249 250 mockContext.Cache("action", nil) 251 252 mockContext.Unlock() 253 defer mockContext.Lock() 254 255 // refresh --pending --proceed is the same as just saying --proceed. 256 stdout, stderr, err = ctlcmd.Run(mockContext, []string{"refresh", "--pending", "--proceed"}, 0) 257 c.Assert(err, IsNil) 258 c.Check(string(stdout), Equals, "") 259 c.Check(string(stderr), Equals, "") 260 261 mockContext.Lock() 262 defer mockContext.Unlock() 263 action = mockContext.Cached("action") 264 c.Assert(action, NotNil) 265 c.Check(action, Equals, snapstate.GateAutoRefreshProceed) 266 } 267 268 func (s *refreshSuite) TestRefreshFromUnsupportedHook(c *C) { 269 s.st.Lock() 270 271 task := s.st.NewTask("test-task", "my test task") 272 setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1), Hook: "install"} 273 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 274 c.Check(err, IsNil) 275 s.st.Unlock() 276 277 _, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 0) 278 c.Check(err, ErrorMatches, `can only be used from gate-auto-refresh hook`) 279 } 280 281 func (s *refreshSuite) TestRefreshProceedFromSnap(c *C) { 282 var called bool 283 restore := ctlcmd.MockAutoRefreshForGatingSnap(func(st *state.State, gatingSnap string) error { 284 called = true 285 c.Check(gatingSnap, Equals, "foo") 286 return nil 287 }) 288 defer restore() 289 290 s.st.Lock() 291 defer s.st.Unlock() 292 mockInstalledSnap(c, s.st, `name: foo 293 version: 1 294 `) 295 296 // enable gate-auto-refresh-hook feature 297 tr := config.NewTransaction(s.st) 298 tr.Set("core", "experimental.gate-auto-refresh-hook", true) 299 tr.Commit() 300 301 // foo is the snap that is going to call --proceed. 302 setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)} 303 mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "") 304 c.Check(err, IsNil) 305 s.st.Unlock() 306 defer s.st.Lock() 307 308 _, _, err = ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0) 309 c.Assert(err, IsNil) 310 c.Check(called, Equals, true) 311 } 312 313 func (s *refreshSuite) TestPendingFromSnapNoRefreshCandidates(c *C) { 314 s.st.Lock() 315 defer s.st.Unlock() 316 317 mockInstalledSnap(c, s.st, `name: foo 318 version: 1 319 `) 320 321 setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)} 322 mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "") 323 c.Check(err, IsNil) 324 s.st.Unlock() 325 defer s.st.Lock() 326 327 stdout, _, err := ctlcmd.Run(mockContext, []string{"refresh", "--pending"}, 0) 328 c.Assert(err, IsNil) 329 c.Check(string(stdout), Equals, "pending: none\nchannel: stable\nbase: false\nrestart: false\n") 330 } 331 332 func (s *refreshSuite) TestRefreshProceedFromSnapError(c *C) { 333 restore := ctlcmd.MockAutoRefreshForGatingSnap(func(st *state.State, gatingSnap string) error { 334 c.Check(gatingSnap, Equals, "foo") 335 return fmt.Errorf("boom") 336 }) 337 defer restore() 338 339 s.st.Lock() 340 defer s.st.Unlock() 341 mockInstalledSnap(c, s.st, `name: foo 342 version: 1 343 `) 344 345 // enable gate-auto-refresh-hook feature 346 tr := config.NewTransaction(s.st) 347 tr.Set("core", "experimental.gate-auto-refresh-hook", true) 348 tr.Commit() 349 350 // foo is the snap that is going to call --proceed. 351 setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)} 352 mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "") 353 c.Check(err, IsNil) 354 s.st.Unlock() 355 defer s.st.Lock() 356 357 _, _, err = ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0) 358 c.Assert(err, ErrorMatches, "boom") 359 } 360 361 func (s *refreshSuite) TestRefreshRegularUserForbidden(c *C) { 362 s.st.Lock() 363 setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1)} 364 s.st.Unlock() 365 366 mockContext, err := hookstate.NewContext(nil, s.st, setup, s.mockHandler, "") 367 c.Assert(err, IsNil) 368 _, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 1000) 369 c.Assert(err, ErrorMatches, `cannot use "refresh" with uid 1000, try with sudo`) 370 forbidden, _ := err.(*ctlcmd.ForbiddenCommandError) 371 c.Assert(forbidden, NotNil) 372 }