github.com/juju/charm/v11@v11.2.0/charmarchive_test.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package charm_test
     5  
     6  import (
     7  	"archive/zip"
     8  	"bytes"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strconv"
    16  	"syscall"
    17  
    18  	"github.com/juju/collections/set"
    19  	"github.com/juju/testing"
    20  	jc "github.com/juju/testing/checkers"
    21  	gc "gopkg.in/check.v1"
    22  	"gopkg.in/yaml.v2"
    23  
    24  	"github.com/juju/charm/v11"
    25  )
    26  
    27  type CharmArchiveSuite struct {
    28  	testing.IsolationSuite
    29  	archivePath string
    30  }
    31  
    32  var _ = gc.Suite(&CharmArchiveSuite{})
    33  
    34  func (s *CharmArchiveSuite) SetUpSuite(c *gc.C) {
    35  	s.IsolationSuite.SetUpSuite(c)
    36  	s.archivePath = archivePath(c, readCharmDir(c, "dummy"))
    37  }
    38  
    39  var dummyArchiveMembersCommon = []string{
    40  	"config.yaml",
    41  	"empty",
    42  	"empty/.gitkeep",
    43  	"hooks",
    44  	"hooks/install",
    45  	"lxd-profile.yaml",
    46  	"manifest.yaml",
    47  	"metadata.yaml",
    48  	"revision",
    49  	"src",
    50  	"src/hello.c",
    51  	".notignored",
    52  }
    53  
    54  var dummyArchiveMembers = append(dummyArchiveMembersCommon, "actions.yaml")
    55  var dummyArchiveMembersActions = append(dummyArchiveMembersCommon, []string{
    56  	"actions.yaml",
    57  	"actions/snapshot",
    58  	"actions",
    59  }...)
    60  
    61  func (s *CharmArchiveSuite) TestReadCharmArchive(c *gc.C) {
    62  	archive, err := charm.ReadCharmArchive(s.archivePath)
    63  	c.Assert(err, gc.IsNil)
    64  	checkDummy(c, archive, s.archivePath)
    65  }
    66  
    67  func (s *CharmArchiveSuite) TestReadCharmArchiveWithoutConfig(c *gc.C) {
    68  	// Technically varnish has no config AND no actions.
    69  	// Perhaps we should make this more orthogonal?
    70  	path := archivePath(c, readCharmDir(c, "varnish"))
    71  	archive, err := charm.ReadCharmArchive(path)
    72  	c.Assert(err, gc.IsNil)
    73  
    74  	// A lacking config.yaml file still causes a proper
    75  	// Config value to be returned.
    76  	c.Assert(archive.Config().Options, gc.HasLen, 0)
    77  }
    78  
    79  func (s *CharmArchiveSuite) TestReadCharmArchiveManifest(c *gc.C) {
    80  	path := archivePath(c, readCharmDir(c, "dummy"))
    81  	dir, err := charm.ReadCharmArchive(path)
    82  	c.Assert(err, gc.IsNil)
    83  
    84  	c.Assert(dir.Manifest().Bases, gc.DeepEquals, []charm.Base{{
    85  		Name: "ubuntu",
    86  		Channel: charm.Channel{
    87  
    88  			Track: "18.04",
    89  			Risk:  "stable",
    90  		},
    91  	}, {
    92  		Name: "ubuntu",
    93  		Channel: charm.Channel{
    94  			Track: "20.04",
    95  			Risk:  "stable",
    96  		},
    97  	}})
    98  }
    99  
   100  func (s *CharmArchiveSuite) TestReadCharmArchiveWithoutManifest(c *gc.C) {
   101  	path := archivePath(c, readCharmDir(c, "mysql"))
   102  	dir, err := charm.ReadCharmArchive(path)
   103  	c.Assert(err, gc.IsNil)
   104  	c.Assert(dir.Manifest(), gc.IsNil)
   105  }
   106  
   107  func (s *CharmArchiveSuite) TestReadCharmArchiveWithoutMetrics(c *gc.C) {
   108  	path := archivePath(c, readCharmDir(c, "varnish"))
   109  	dir, err := charm.ReadCharmArchive(path)
   110  	c.Assert(err, gc.IsNil)
   111  
   112  	// A lacking metrics.yaml file indicates the unit will not
   113  	// be metered.
   114  	c.Assert(dir.Metrics(), gc.IsNil)
   115  }
   116  
   117  func (s *CharmArchiveSuite) TestReadCharmArchiveWithEmptyMetrics(c *gc.C) {
   118  	path := archivePath(c, readCharmDir(c, "metered-empty"))
   119  	dir, err := charm.ReadCharmArchive(path)
   120  	c.Assert(err, gc.IsNil)
   121  	c.Assert(Keys(dir.Metrics()), gc.HasLen, 0)
   122  }
   123  
   124  func (s *CharmArchiveSuite) TestReadCharmArchiveWithCustomMetrics(c *gc.C) {
   125  	path := archivePath(c, readCharmDir(c, "metered"))
   126  	dir, err := charm.ReadCharmArchive(path)
   127  	c.Assert(err, gc.IsNil)
   128  
   129  	c.Assert(dir.Metrics(), gc.NotNil)
   130  	c.Assert(Keys(dir.Metrics()), gc.DeepEquals, []string{"juju-unit-time", "pings"})
   131  }
   132  
   133  func (s *CharmArchiveSuite) TestReadCharmArchiveWithoutActions(c *gc.C) {
   134  	// Wordpress has config but no actions.
   135  	path := archivePath(c, readCharmDir(c, "wordpress"))
   136  	archive, err := charm.ReadCharmArchive(path)
   137  	c.Assert(err, gc.IsNil)
   138  
   139  	// A lacking actions.yaml file still causes a proper
   140  	// Actions value to be returned.
   141  	c.Assert(archive.Actions().ActionSpecs, gc.HasLen, 0)
   142  }
   143  
   144  func (s *CharmArchiveSuite) TestReadCharmArchiveWithActions(c *gc.C) {
   145  	path := archivePath(c, readCharmDir(c, "dummy-actions"))
   146  	archive, err := charm.ReadCharmArchive(path)
   147  	c.Assert(err, gc.IsNil)
   148  	c.Assert(archive.Actions().ActionSpecs, gc.HasLen, 1)
   149  }
   150  
   151  func (s *CharmDirSuite) TestReadCharmArchiveWithJujuActions(c *gc.C) {
   152  	path := archivePath(c, readCharmDir(c, "juju-charm"))
   153  	archive, err := charm.ReadCharmArchive(path)
   154  	c.Assert(err, gc.IsNil)
   155  	c.Assert(archive.Actions().ActionSpecs, gc.HasLen, 1)
   156  }
   157  
   158  func (s *CharmArchiveSuite) TestReadCharmArchiveBytes(c *gc.C) {
   159  	data, err := ioutil.ReadFile(s.archivePath)
   160  	c.Assert(err, gc.IsNil)
   161  
   162  	archive, err := charm.ReadCharmArchiveBytes(data)
   163  	c.Assert(err, gc.IsNil)
   164  	checkDummy(c, archive, "")
   165  }
   166  
   167  func (s *CharmArchiveSuite) TestReadCharmArchiveFromReader(c *gc.C) {
   168  	f, err := os.Open(s.archivePath)
   169  	c.Assert(err, gc.IsNil)
   170  	defer func() { _ = f.Close() }()
   171  	info, err := f.Stat()
   172  	c.Assert(err, gc.IsNil)
   173  
   174  	archive, err := charm.ReadCharmArchiveFromReader(f, info.Size())
   175  	c.Assert(err, gc.IsNil)
   176  	checkDummy(c, archive, "")
   177  }
   178  
   179  func (s *CharmArchiveSuite) TestArchiveMembers(c *gc.C) {
   180  	archive, err := charm.ReadCharmArchive(s.archivePath)
   181  	c.Assert(err, gc.IsNil)
   182  	manifest, err := archive.ArchiveMembers()
   183  	c.Assert(err, gc.IsNil)
   184  	c.Assert(manifest, jc.DeepEquals, set.NewStrings(dummyArchiveMembers...))
   185  }
   186  
   187  func (s *CharmArchiveSuite) TestArchiveMembersActions(c *gc.C) {
   188  	path := archivePath(c, readCharmDir(c, "dummy-actions"))
   189  	archive, err := charm.ReadCharmArchive(path)
   190  	c.Assert(err, gc.IsNil)
   191  	manifest, err := archive.ArchiveMembers()
   192  	c.Assert(err, gc.IsNil)
   193  	c.Assert(manifest, jc.DeepEquals, set.NewStrings(dummyArchiveMembersActions...))
   194  }
   195  
   196  func (s *CharmArchiveSuite) TestArchiveMembersNoRevision(c *gc.C) {
   197  	archive, err := charm.ReadCharmArchive(s.archivePath)
   198  	c.Assert(err, gc.IsNil)
   199  	dirPath := c.MkDir()
   200  	err = archive.ExpandTo(dirPath)
   201  	c.Assert(err, gc.IsNil)
   202  	err = os.Remove(filepath.Join(dirPath, "revision"))
   203  	c.Assert(err, gc.IsNil)
   204  
   205  	archive = extCharmArchiveDir(c, dirPath)
   206  	manifest, err := archive.ArchiveMembers()
   207  	c.Assert(err, gc.IsNil)
   208  	c.Assert(manifest, gc.DeepEquals, set.NewStrings(dummyArchiveMembers...))
   209  }
   210  
   211  func (s *CharmArchiveSuite) TestArchiveMembersSymlink(c *gc.C) {
   212  	srcPath := cloneDir(c, charmDirPath(c, "dummy"))
   213  	if err := os.Symlink("../target", filepath.Join(srcPath, "hooks/symlink")); err != nil {
   214  		c.Skip("cannot symlink")
   215  	}
   216  	expected := append([]string{"hooks/symlink"}, dummyArchiveMembers...)
   217  
   218  	archive := archiveDir(c, srcPath)
   219  	manifest, err := archive.ArchiveMembers()
   220  	c.Assert(err, gc.IsNil)
   221  	c.Assert(manifest, gc.DeepEquals, set.NewStrings(expected...))
   222  }
   223  
   224  func (s *CharmArchiveSuite) TestExpandTo(c *gc.C) {
   225  	archive, err := charm.ReadCharmArchive(s.archivePath)
   226  	c.Assert(err, gc.IsNil)
   227  
   228  	path := filepath.Join(c.MkDir(), "charm")
   229  	err = archive.ExpandTo(path)
   230  	c.Assert(err, gc.IsNil)
   231  
   232  	dir, err := charm.ReadCharmDir(path)
   233  	c.Assert(err, gc.IsNil)
   234  	checkDummy(c, dir, path)
   235  }
   236  
   237  func (s *CharmArchiveSuite) TestReadCharmArchiveWithVersion(c *gc.C) {
   238  	clonedPath := cloneDir(c, charmDirPath(c, "versioned"))
   239  	_, err := os.Create(filepath.Join(clonedPath, ".git"))
   240  	c.Assert(err, gc.IsNil)
   241  
   242  	// NOTE(achilleasa) Initially, I tried using PatchExecutableAsEchoArgs
   243  	// but it doesn't work as expected on my bionic box so I reverted to
   244  	// the following less elegant approach to stubbing git output.
   245  	var gitOutput string
   246  	switch runtime.GOOS {
   247  	case "windows":
   248  		gitOutput = "@echo off\r\necho c0ffee"
   249  	default:
   250  		gitOutput = "#!/bin/bash -norc\necho c0ffee"
   251  	}
   252  	testing.PatchExecutable(c, s, "git", gitOutput)
   253  
   254  	// Read cloned path and archive it; the archive should now include
   255  	// the version fetched from our mocked call to git
   256  	cd, err := charm.ReadCharmDir(clonedPath)
   257  	c.Assert(err, gc.IsNil)
   258  	path := archivePath(c, cd)
   259  
   260  	// Read back the archive and verify the correct version
   261  	archive, err := charm.ReadCharmArchive(path)
   262  	c.Assert(err, gc.IsNil)
   263  	c.Assert(archive.Version(), gc.Equals, "c0ffee")
   264  }
   265  
   266  func (s *CharmArchiveSuite) prepareCharmArchive(c *gc.C, charmDir *charm.CharmDir, archivePath string) {
   267  	file, err := os.Create(archivePath)
   268  	c.Assert(err, gc.IsNil)
   269  	defer file.Close()
   270  	zipw := zip.NewWriter(file)
   271  	defer zipw.Close()
   272  
   273  	h := &zip.FileHeader{Name: "revision"}
   274  	h.SetMode(syscall.S_IFREG | 0644)
   275  	w, err := zipw.CreateHeader(h)
   276  	c.Assert(err, gc.IsNil)
   277  	_, err = w.Write([]byte(strconv.Itoa(charmDir.Revision())))
   278  
   279  	h = &zip.FileHeader{Name: "metadata.yaml", Method: zip.Deflate}
   280  	h.SetMode(0644)
   281  	w, err = zipw.CreateHeader(h)
   282  	c.Assert(err, gc.IsNil)
   283  	data, err := yaml.Marshal(charmDir.Meta())
   284  	c.Assert(err, gc.IsNil)
   285  	_, err = w.Write(data)
   286  	c.Assert(err, gc.IsNil)
   287  
   288  	for name := range charmDir.Meta().Hooks() {
   289  		hookName := filepath.Join("hooks", name)
   290  		h = &zip.FileHeader{
   291  			Name:   hookName,
   292  			Method: zip.Deflate,
   293  		}
   294  		// Force it non-executable
   295  		h.SetMode(0644)
   296  		w, err := zipw.CreateHeader(h)
   297  		c.Assert(err, gc.IsNil)
   298  		_, err = w.Write([]byte("not important"))
   299  		c.Assert(err, gc.IsNil)
   300  	}
   301  }
   302  
   303  func (s *CharmArchiveSuite) TestExpandToSetsHooksExecutable(c *gc.C) {
   304  	charmDir, err := charm.ReadCharmDir(cloneDir(c, charmDirPath(c, "all-hooks")))
   305  	c.Assert(err, gc.IsNil)
   306  	// CharmArchive manually, so we can check ExpandTo(), unaffected
   307  	// by ArchiveTo()'s behavior
   308  	archivePath := filepath.Join(c.MkDir(), "archive.charm")
   309  	s.prepareCharmArchive(c, charmDir, archivePath)
   310  	archive, err := charm.ReadCharmArchive(archivePath)
   311  	c.Assert(err, gc.IsNil)
   312  
   313  	path := filepath.Join(c.MkDir(), "charm")
   314  	err = archive.ExpandTo(path)
   315  	c.Assert(err, gc.IsNil)
   316  
   317  	_, err = charm.ReadCharmDir(path)
   318  	c.Assert(err, gc.IsNil)
   319  
   320  	for name := range archive.Meta().Hooks() {
   321  		hookName := string(name)
   322  		info, err := os.Stat(filepath.Join(path, "hooks", hookName))
   323  		c.Assert(err, gc.IsNil)
   324  		perm := info.Mode() & 0777
   325  		c.Assert(perm&0100 != 0, gc.Equals, true, gc.Commentf("hook %q is not executable", hookName))
   326  	}
   327  }
   328  
   329  func (s *CharmArchiveSuite) TestCharmArchiveFileModes(c *gc.C) {
   330  	// Apply subtler mode differences than can be expressed in Bazaar.
   331  	srcPath := cloneDir(c, charmDirPath(c, "dummy"))
   332  	modes := []struct {
   333  		path string
   334  		mode os.FileMode
   335  	}{
   336  		{"hooks/install", 0751},
   337  		{"empty", 0750},
   338  		{"src/hello.c", 0614},
   339  	}
   340  	for _, m := range modes {
   341  		err := os.Chmod(filepath.Join(srcPath, m.path), m.mode)
   342  		c.Assert(err, gc.IsNil)
   343  	}
   344  	var haveSymlinks = true
   345  	if err := os.Symlink("../target", filepath.Join(srcPath, "hooks/symlink")); err != nil {
   346  		haveSymlinks = false
   347  	}
   348  
   349  	// CharmArchive and extract the charm to a new directory.
   350  	archive := archiveDir(c, srcPath)
   351  	path := c.MkDir()
   352  	err := archive.ExpandTo(path)
   353  	c.Assert(err, gc.IsNil)
   354  
   355  	// Check sensible file modes once round-tripped.
   356  	info, err := os.Stat(filepath.Join(path, "src", "hello.c"))
   357  	c.Assert(err, gc.IsNil)
   358  	c.Assert(info.Mode()&0777, gc.Equals, os.FileMode(0644))
   359  	c.Assert(info.Mode()&os.ModeType, gc.Equals, os.FileMode(0))
   360  
   361  	info, err = os.Stat(filepath.Join(path, "hooks", "install"))
   362  	c.Assert(err, gc.IsNil)
   363  	c.Assert(info.Mode()&0777, gc.Equals, os.FileMode(0755))
   364  	c.Assert(info.Mode()&os.ModeType, gc.Equals, os.FileMode(0))
   365  
   366  	info, err = os.Stat(filepath.Join(path, "empty"))
   367  	c.Assert(err, gc.IsNil)
   368  	c.Assert(info.Mode()&0777, gc.Equals, os.FileMode(0755))
   369  
   370  	if haveSymlinks {
   371  		target, err := os.Readlink(filepath.Join(path, "hooks", "symlink"))
   372  		c.Assert(err, gc.IsNil)
   373  		c.Assert(target, gc.Equals, "../target")
   374  	}
   375  }
   376  
   377  func (s *CharmArchiveSuite) TestCharmArchiveRevisionFile(c *gc.C) {
   378  	charmDir := cloneDir(c, charmDirPath(c, "dummy"))
   379  	revPath := filepath.Join(charmDir, "revision")
   380  
   381  	// Missing revision file
   382  	err := os.Remove(revPath)
   383  	c.Assert(err, gc.IsNil)
   384  
   385  	archive := extCharmArchiveDir(c, charmDir)
   386  	c.Assert(archive.Revision(), gc.Equals, 0)
   387  
   388  	// Missing revision file with obsolete old revision in metadata;
   389  	// the revision is ignored.
   390  	file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
   391  	c.Assert(err, gc.IsNil)
   392  	_, err = file.Write([]byte("\nrevision: 1234\n"))
   393  	c.Assert(err, gc.IsNil)
   394  
   395  	archive = extCharmArchiveDir(c, charmDir)
   396  	c.Assert(archive.Revision(), gc.Equals, 0)
   397  
   398  	// Revision file with bad content
   399  	err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
   400  	c.Assert(err, gc.IsNil)
   401  
   402  	path := extCharmArchiveDirPath(c, charmDir)
   403  	archive, err = charm.ReadCharmArchive(path)
   404  	c.Assert(err, gc.ErrorMatches, "invalid revision file")
   405  	c.Assert(archive, gc.IsNil)
   406  }
   407  
   408  func (s *CharmArchiveSuite) TestCharmArchiveSetRevision(c *gc.C) {
   409  	archive, err := charm.ReadCharmArchive(s.archivePath)
   410  	c.Assert(err, gc.IsNil)
   411  
   412  	c.Assert(archive.Revision(), gc.Equals, 1)
   413  	archive.SetRevision(42)
   414  	c.Assert(archive.Revision(), gc.Equals, 42)
   415  
   416  	path := filepath.Join(c.MkDir(), "charm")
   417  	err = archive.ExpandTo(path)
   418  	c.Assert(err, gc.IsNil)
   419  
   420  	dir, err := charm.ReadCharmDir(path)
   421  	c.Assert(err, gc.IsNil)
   422  	c.Assert(dir.Revision(), gc.Equals, 42)
   423  }
   424  
   425  func (s *CharmArchiveSuite) TestExpandToWithBadLink(c *gc.C) {
   426  	charmDir := cloneDir(c, charmDirPath(c, "dummy"))
   427  	badLink := filepath.Join(charmDir, "hooks", "badlink")
   428  
   429  	// Symlink targeting a path outside of the charm.
   430  	err := os.Symlink("../../target", badLink)
   431  	c.Assert(err, gc.IsNil)
   432  
   433  	archive := extCharmArchiveDir(c, charmDir)
   434  	c.Assert(err, gc.IsNil)
   435  
   436  	path := filepath.Join(c.MkDir(), "charm")
   437  	err = archive.ExpandTo(path)
   438  	c.Assert(err, gc.ErrorMatches, `cannot extract "hooks/badlink": symlink "../../target" leads out of scope`)
   439  
   440  	// Symlink targeting an absolute path.
   441  	os.Remove(badLink)
   442  	err = os.Symlink("/target", badLink)
   443  	c.Assert(err, gc.IsNil)
   444  
   445  	archive = extCharmArchiveDir(c, charmDir)
   446  	c.Assert(err, gc.IsNil)
   447  
   448  	path = filepath.Join(c.MkDir(), "charm")
   449  	err = archive.ExpandTo(path)
   450  	c.Assert(err, gc.ErrorMatches, `cannot extract "hooks/badlink": symlink "/target" is absolute`)
   451  }
   452  
   453  func extCharmArchiveDirPath(c *gc.C, dirpath string) string {
   454  	path := filepath.Join(c.MkDir(), "archive.charm")
   455  	cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s; zip --fifo --symlinks -r %s .", dirpath, path))
   456  	output, err := cmd.CombinedOutput()
   457  	c.Assert(err, gc.IsNil, gc.Commentf("Command output: %s", output))
   458  	return path
   459  }
   460  
   461  func extCharmArchiveDir(c *gc.C, dirpath string) *charm.CharmArchive {
   462  	path := extCharmArchiveDirPath(c, dirpath)
   463  	archive, err := charm.ReadCharmArchive(path)
   464  	c.Assert(err, gc.IsNil)
   465  	return archive
   466  }
   467  
   468  func archiveDir(c *gc.C, dirpath string) *charm.CharmArchive {
   469  	dir, err := charm.ReadCharmDir(dirpath)
   470  	c.Assert(err, gc.IsNil)
   471  	buf := new(bytes.Buffer)
   472  	err = dir.ArchiveTo(buf)
   473  	c.Assert(err, gc.IsNil)
   474  	archive, err := charm.ReadCharmArchiveBytes(buf.Bytes())
   475  	c.Assert(err, gc.IsNil)
   476  	return archive
   477  }