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 }