github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/snap/snaptest/snaptest.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 snaptest contains helper functions for mocking snaps. 21 package snaptest 22 23 import ( 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/osutil" 33 "github.com/snapcore/snapd/snap" 34 "github.com/snapcore/snapd/snap/channel" 35 "github.com/snapcore/snapd/snap/pack" 36 "github.com/snapcore/snapd/snap/snapdir" 37 ) 38 39 func mockSnap(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 40 c.Assert(sideInfo, check.Not(check.IsNil)) 41 42 restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 43 defer restoreSanitize() 44 45 // Parse the yaml (we need the Name). 46 snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) 47 c.Assert(err, check.IsNil) 48 49 // Set SideInfo so that we can use MountDir below 50 snapInfo.SideInfo = *sideInfo 51 52 if instanceName != "" { 53 // Set the snap instance name 54 snapName, instanceKey := snap.SplitInstanceName(instanceName) 55 snapInfo.InstanceKey = instanceKey 56 57 // Make sure snap name/instance name checks out 58 c.Assert(snapInfo.InstanceName(), check.Equals, instanceName) 59 c.Assert(snapInfo.SnapName(), check.Equals, snapName) 60 } 61 62 // Put the YAML on disk, in the right spot. 63 metaDir := filepath.Join(snapInfo.MountDir(), "meta") 64 err = os.MkdirAll(metaDir, 0755) 65 c.Assert(err, check.IsNil) 66 err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(yamlText), 0644) 67 c.Assert(err, check.IsNil) 68 69 // Write the .snap to disk 70 err = os.MkdirAll(filepath.Dir(snapInfo.MountFile()), 0755) 71 c.Assert(err, check.IsNil) 72 snapContents := fmt.Sprintf("%s-%s-%s", sideInfo.RealName, sideInfo.SnapID, sideInfo.Revision) 73 err = ioutil.WriteFile(snapInfo.MountFile(), []byte(snapContents), 0644) 74 c.Assert(err, check.IsNil) 75 snapInfo.Size = int64(len(snapContents)) 76 77 return snapInfo 78 } 79 80 // MockSnap puts a snap.yaml file on disk so to mock an installed snap, based on the provided arguments. 81 // 82 // The caller is responsible for mocking root directory with dirs.SetRootDir() 83 // and for altering the overlord state if required. 84 func MockSnap(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 85 return mockSnap(c, "", yamlText, sideInfo) 86 } 87 88 // MockSnapInstance puts a snap.yaml file on disk so to mock an installed snap 89 // instance, based on the provided arguments. 90 // 91 // The caller is responsible for mocking root directory with dirs.SetRootDir() 92 // and for altering the overlord state if required. 93 func MockSnapInstance(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 94 return mockSnap(c, instanceName, yamlText, sideInfo) 95 } 96 97 // MockSnapCurrent does the same as MockSnap but additionally creates the 98 // 'current' symlink. 99 // 100 // The caller is responsible for mocking root directory with dirs.SetRootDir() 101 // and for altering the overlord state if required. 102 func MockSnapCurrent(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 103 si := MockSnap(c, yamlText, sideInfo) 104 err := os.Symlink(filepath.Base(si.MountDir()), filepath.Join(si.MountDir(), "../current")) 105 c.Assert(err, check.IsNil) 106 return si 107 } 108 109 // MockSnapInstanceCurrent does the same as MockSnapInstance but additionally 110 // creates the 'current' symlink. 111 // 112 // The caller is responsible for mocking root directory with dirs.SetRootDir() 113 // and for altering the overlord state if required. 114 func MockSnapInstanceCurrent(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 115 si := MockSnapInstance(c, instanceName, yamlText, sideInfo) 116 err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) 117 c.Assert(err, check.IsNil) 118 return si 119 } 120 121 // MockInfo parses the given snap.yaml text and returns a validated snap.Info object including the optional SideInfo. 122 // 123 // The result is just kept in memory, there is nothing kept on disk. If that is 124 // desired please use MockSnap instead. 125 func MockInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 126 if sideInfo == nil { 127 sideInfo = &snap.SideInfo{} 128 } 129 130 restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 131 defer restoreSanitize() 132 snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) 133 c.Assert(err, check.IsNil) 134 if snapInfo.InstanceName() == "core" && snapInfo.Type() != snap.TypeOS { 135 panic("core snap must use type: os") 136 } 137 if snapInfo.InstanceName() == "snapd" && snapInfo.Type() != snap.TypeSnapd { 138 panic("snapd snap must use type: snapd") 139 } 140 141 snapInfo.SideInfo = *sideInfo 142 err = snap.Validate(snapInfo) 143 c.Assert(err, check.IsNil) 144 return snapInfo 145 } 146 147 // MockInvalidInfo parses the given snap.yaml text and returns the snap.Info object including the optional SideInfo. 148 // 149 // The result is just kept in memory, there is nothing kept on disk. If that is 150 // desired please use MockSnap instead. 151 func MockInvalidInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { 152 if sideInfo == nil { 153 sideInfo = &snap.SideInfo{} 154 } 155 156 restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 157 defer restoreSanitize() 158 159 snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) 160 c.Assert(err, check.IsNil) 161 snapInfo.SideInfo = *sideInfo 162 err = snap.Validate(snapInfo) 163 c.Assert(err, check.NotNil) 164 return snapInfo 165 } 166 167 // MockSnapWithFiles does the same as MockSnap, but also populates the snap 168 // directory with given content 169 // 170 // The caller is responsible for mocking root directory with dirs.SetRootDir() 171 //and for altering the overlord state if required. 172 func MockSnapWithFiles(c *check.C, yamlText string, si *snap.SideInfo, files [][]string) *snap.Info { 173 info := MockSnap(c, yamlText, si) 174 175 PopulateDir(info.MountDir(), files) 176 return info 177 } 178 179 // PopulateDir populates the directory with files specified as pairs of relative file path and its content. Useful to add extra files to a snap. 180 func PopulateDir(dir string, files [][]string) { 181 for _, filenameAndContent := range files { 182 filename := filenameAndContent[0] 183 content := filenameAndContent[1] 184 fpath := filepath.Join(dir, filename) 185 err := os.MkdirAll(filepath.Dir(fpath), 0755) 186 if err != nil { 187 panic(err) 188 } 189 err = ioutil.WriteFile(fpath, []byte(content), 0755) 190 if err != nil { 191 panic(err) 192 } 193 } 194 } 195 196 func AssertedSnapID(snapName string) string { 197 cleanedName := strings.Replace(snapName, "-", "", -1) 198 return (cleanedName + strings.Repeat("id", 16)[len(cleanedName):]) 199 } 200 201 // MakeTestSnapWithFiles makes a squashfs snap file with the given 202 // snap.yaml content and optional extras files specified as pairs of 203 // relative file path and its content. 204 func MakeTestSnapWithFiles(c *check.C, snapYamlContent string, files [][]string) (snapFilePath string) { 205 path, _ := MakeTestSnapInfoWithFiles(c, snapYamlContent, files, nil) 206 return path 207 } 208 209 // MakeTestSnapInfoWithFiles makes a squashfs snap file with the given snap.yaml 210 // content and optional extra files specified as pairs of relative file path and 211 // it's contents, and returns the path to the snap file and a suitable snap.Info 212 // for the snap 213 func MakeTestSnapInfoWithFiles(c *check.C, snapYamlContent string, files [][]string, si *snap.SideInfo) (snapFilePath string, info *snap.Info) { 214 tmpdir := c.MkDir() 215 snapSource := filepath.Join(tmpdir, "snapsrc") 216 err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755) 217 c.Assert(err, check.IsNil) 218 snapYamlFn := filepath.Join(snapSource, "meta", "snap.yaml") 219 err = ioutil.WriteFile(snapYamlFn, []byte(snapYamlContent), 0644) 220 c.Assert(err, check.IsNil) 221 PopulateDir(snapSource, files) 222 restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 223 defer restoreSanitize() 224 225 // Parse the yaml (we need the Name). 226 snapInfo, err := snap.InfoFromSnapYaml([]byte(snapYamlContent)) 227 c.Assert(err, check.IsNil) 228 if si != nil { 229 snapInfo.SideInfo = *si 230 } 231 err = osutil.ChDir(snapSource, func() error { 232 var err error 233 snapFilePath, err = pack.Snap(snapSource, nil) 234 return err 235 }) 236 c.Assert(err, check.IsNil) 237 return filepath.Join(snapSource, snapFilePath), snapInfo 238 239 } 240 241 // MakeSnapFileWithDir makes a squashfs snap file and a directory under 242 // /snaps/<snap>/<rev> with the given contents. It's a combined effect of 243 // MakeTestSnapInfoWithFiles and MockSnapWithFiles. 244 func MakeSnapFileAndDir(c *check.C, snapYamlContent string, files [][]string, si *snap.SideInfo) *snap.Info { 245 info := MockSnapWithFiles(c, snapYamlContent, si, files) 246 247 restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 248 defer restoreSanitize() 249 250 err := osutil.ChDir(info.MountDir(), func() error { 251 snapName, err := pack.Snap(info.MountDir(), &pack.Options{ 252 SnapName: info.MountFile(), 253 }) 254 c.Check(snapName, check.Equals, info.MountFile()) 255 return err 256 }) 257 c.Assert(err, check.IsNil) 258 return info 259 } 260 261 // MustParseChannel parses a string representing a store channel and 262 // includes the given architecture, if architecture is "" the system 263 // architecture is included. It panics on error. 264 func MustParseChannel(s string, architecture string) channel.Channel { 265 c, err := channel.Parse(s, architecture) 266 if err != nil { 267 panic(err) 268 } 269 return c 270 } 271 272 // RenameSlot renames gives an existing slot a new name. 273 // 274 // The new slot name cannot clash with an existing plug or slot and must 275 // be a valid slot name. 276 func RenameSlot(snapInfo *snap.Info, oldName, newName string) error { 277 if snapInfo.Slots[oldName] == nil { 278 return fmt.Errorf("cannot rename slot %q to %q: no such slot", oldName, newName) 279 } 280 if err := snap.ValidateSlotName(newName); err != nil { 281 return fmt.Errorf("cannot rename slot %q to %q: %s", oldName, newName, err) 282 } 283 if oldName == newName { 284 return nil 285 } 286 if snapInfo.Slots[newName] != nil { 287 return fmt.Errorf("cannot rename slot %q to %q: existing slot with that name", oldName, newName) 288 } 289 if snapInfo.Plugs[newName] != nil { 290 return fmt.Errorf("cannot rename slot %q to %q: existing plug with that name", oldName, newName) 291 } 292 293 // Rename the slot. 294 slotInfo := snapInfo.Slots[oldName] 295 snapInfo.Slots[newName] = slotInfo 296 delete(snapInfo.Slots, oldName) 297 slotInfo.Name = newName 298 299 // Update references to the slot in all applications and hooks. 300 for _, appInfo := range snapInfo.Apps { 301 if _, ok := appInfo.Slots[oldName]; ok { 302 delete(appInfo.Slots, oldName) 303 appInfo.Slots[newName] = slotInfo 304 } 305 } 306 for _, hookInfo := range snapInfo.Hooks { 307 if _, ok := hookInfo.Slots[oldName]; ok { 308 delete(hookInfo.Slots, oldName) 309 hookInfo.Slots[newName] = slotInfo 310 } 311 } 312 313 return nil 314 } 315 316 // MockContainer returns a mock snap.Container with the given content. 317 // If files is empty it still produces a minimal container that passes 318 // ValidateContainer: / and /meta exist and are 0755, and 319 // /meta/snap.yaml is a regular world-readable file. 320 func MockContainer(c *check.C, files [][]string) snap.Container { 321 d := c.MkDir() 322 c.Assert(os.Chmod(d, 0755), check.IsNil) 323 files = append([][]string{{"meta/snap.yaml", ""}}, files...) 324 PopulateDir(d, files) 325 return snapdir.New(d) 326 }