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