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  }