Skip to content

Commit f291489

Browse files
committed
chore: merge 3.5 into 3.6
2 parents 3937339 + 7322356 commit f291489

File tree

2 files changed

+114
-18
lines changed

2 files changed

+114
-18
lines changed

api/client/charms/client_test.go

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package charms_test
55

66
import (
7+
"archive/zip"
78
"os"
89

910
"github.com/juju/charm/v12"
@@ -287,7 +288,7 @@ func (s *addCharmSuite) TestAddCharm(c *gc.C) {
287288
c.Assert(got, gc.DeepEquals, origin)
288289
}
289290

290-
func (s charmsMockSuite) TestCheckCharmPlacement(c *gc.C) {
291+
func (s *charmsMockSuite) TestCheckCharmPlacement(c *gc.C) {
291292
ctrl := gomock.NewController(c)
292293
defer ctrl.Finish()
293294

@@ -311,7 +312,7 @@ func (s charmsMockSuite) TestCheckCharmPlacement(c *gc.C) {
311312
c.Assert(err, jc.ErrorIsNil)
312313
}
313314

314-
func (s charmsMockSuite) TestCheckCharmPlacementError(c *gc.C) {
315+
func (s *charmsMockSuite) TestCheckCharmPlacementError(c *gc.C) {
315316
ctrl := gomock.NewController(c)
316317
defer ctrl.Finish()
317318

@@ -411,7 +412,7 @@ func (s *charmsMockSuite) TestZipHasDispatchFileOnly(c *gc.C) {
411412
c.Assert(hasDispatch, jc.IsTrue)
412413
}
413414

414-
func (s *charmsMockSuite) TestZipHasNoHooksNorDispath(c *gc.C) {
415+
func (s *charmsMockSuite) TestZipHasNoHooksNorDispatch(c *gc.C) {
415416
ch := testcharms.Repo.CharmDir("category") // has no hooks nor dispatch file
416417
tempFile, err := os.CreateTemp(c.MkDir(), "charm")
417418
c.Assert(err, jc.ErrorIsNil)
@@ -424,3 +425,91 @@ func (s *charmsMockSuite) TestZipHasNoHooksNorDispath(c *gc.C) {
424425
c.Assert(err, jc.ErrorIsNil)
425426
c.Assert(hasHooks, jc.IsFalse)
426427
}
428+
429+
// TestZipHasSingleHook tests that an archive containing only a single hook
430+
// file (and no zip entry for the hooks directory) is still validated as a
431+
// charm with hooks.
432+
func (s *charmsMockSuite) TestZipHasSingleHook(c *gc.C) {
433+
tempFile, err := os.CreateTemp(c.MkDir(), "charm")
434+
c.Assert(err, jc.ErrorIsNil)
435+
defer tempFile.Close()
436+
437+
zipWriter := zip.NewWriter(tempFile)
438+
// add a single install hook
439+
_, err = zipWriter.Create("hooks/install")
440+
c.Assert(err, jc.ErrorIsNil)
441+
err = zipWriter.Close()
442+
c.Assert(err, jc.ErrorIsNil)
443+
444+
// Verify created zip is as expected
445+
zipReader, err := zip.OpenReader(tempFile.Name())
446+
c.Assert(err, jc.ErrorIsNil)
447+
c.Assert(len(zipReader.File), gc.Equals, 1)
448+
c.Assert(zipReader.File[0].Name, gc.Equals, "hooks/install")
449+
c.Assert(zipReader.File[0].Mode().IsRegular(), jc.IsTrue)
450+
451+
// Verify this is validated as having a hook
452+
hasHooks, err := (*charms.HasHooksOrDispatch)(tempFile.Name())
453+
c.Check(err, jc.ErrorIsNil)
454+
c.Check(hasHooks, jc.IsTrue)
455+
}
456+
457+
// TestZipEmptyHookDir tests that an archive containing only an empty hooks
458+
// directory is not validated as a charm with hooks.
459+
func (s *charmsMockSuite) TestZipEmptyHookDir(c *gc.C) {
460+
tempFile, err := os.CreateTemp(c.MkDir(), "charm")
461+
c.Assert(err, jc.ErrorIsNil)
462+
defer tempFile.Close()
463+
464+
zipWriter := zip.NewWriter(tempFile)
465+
// add an empty hooks directory
466+
_, err = zipWriter.Create("hooks/")
467+
c.Assert(err, jc.ErrorIsNil)
468+
err = zipWriter.Close()
469+
c.Assert(err, jc.ErrorIsNil)
470+
471+
// Verify created zip is as expected
472+
zipReader, err := zip.OpenReader(tempFile.Name())
473+
c.Assert(err, jc.ErrorIsNil)
474+
c.Assert(len(zipReader.File), gc.Equals, 1)
475+
c.Assert(zipReader.File[0].Name, gc.Equals, "hooks/")
476+
c.Assert(zipReader.File[0].Mode().IsDir(), jc.IsTrue)
477+
478+
// Verify this is validated as having no hooks
479+
hasHooks, err := (*charms.HasHooksOrDispatch)(tempFile.Name())
480+
c.Check(err, jc.ErrorIsNil)
481+
c.Check(hasHooks, jc.IsFalse)
482+
}
483+
484+
// TestZipSubfileHook tests that an archive containing nested subfiles inside
485+
// the hooks directory (i.e. not in the top level) is not validated as a charm
486+
// with hooks.
487+
func (s *charmsMockSuite) TestZipSubfileHook(c *gc.C) {
488+
tempFile, err := os.CreateTemp(c.MkDir(), "charm")
489+
c.Assert(err, jc.ErrorIsNil)
490+
defer tempFile.Close()
491+
492+
zipWriter := zip.NewWriter(tempFile)
493+
// add some files inside a subdir of hooks
494+
_, err = zipWriter.Create("hooks/foo/bar.sh")
495+
c.Assert(err, jc.ErrorIsNil)
496+
_, err = zipWriter.Create("hooks/hooks/install")
497+
c.Assert(err, jc.ErrorIsNil)
498+
_, err = zipWriter.Create("foo/hooks/install")
499+
c.Assert(err, jc.ErrorIsNil)
500+
err = zipWriter.Close()
501+
c.Assert(err, jc.ErrorIsNil)
502+
503+
// Verify created zip is as expected
504+
zipReader, err := zip.OpenReader(tempFile.Name())
505+
c.Assert(err, jc.ErrorIsNil)
506+
c.Assert(len(zipReader.File), gc.Equals, 3)
507+
for _, f := range zipReader.File {
508+
c.Assert(f.Mode().IsRegular(), jc.IsTrue)
509+
}
510+
511+
// Verify this is not validated as having a hook
512+
hasHooks, err := (*charms.HasHooksOrDispatch)(tempFile.Name())
513+
c.Check(err, jc.ErrorIsNil)
514+
c.Check(hasHooks, jc.IsFalse)
515+
}

api/client/charms/localcharmclient.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
"encoding/hex"
1111
"fmt"
1212
"io"
13+
"io/fs"
1314
"os"
15+
"path/filepath"
1416
"strings"
1517

1618
"github.com/juju/charm/v12"
@@ -149,24 +151,10 @@ func hasHooksFolderOrDispatchFile(name string) (bool, error) {
149151
return false, err
150152
}
151153
defer zipr.Close()
152-
count := 0
153154
// zip file spec 4.4.17.1 says that separators are always "/" even on Windows.
154-
hooksPath := "hooks/"
155155
dispatchPath := "dispatch"
156156
for _, f := range zipr.File {
157-
if strings.Contains(f.Name, hooksPath) {
158-
count++
159-
}
160-
if count > 1 {
161-
// 1 is the magic number here.
162-
// Charm zip archive is expected to contain several files and folders.
163-
// All properly built charms will have a non-empty "hooks" folders OR
164-
// a dispatch file.
165-
// File names in the archive will be of the form "hooks/" - for hooks folder; and
166-
// "hooks/*" for the actual charm hooks implementations.
167-
// For example, install hook may have a file with a name "hooks/install".
168-
// Once we know that there are, at least, 2 files that have names that start with "hooks/", we
169-
// know for sure that the charm has a non-empty hooks folder.
157+
if isHook(f) {
170158
return true, nil
171159
}
172160
if strings.Contains(f.Name, dispatchPath) {
@@ -176,6 +164,25 @@ func hasHooksFolderOrDispatchFile(name string) (bool, error) {
176164
return false, nil
177165
}
178166

167+
// isHook verifies whether the given file inside a zip archive is a valid hook
168+
// file.
169+
func isHook(f *zip.File) bool {
170+
// Hook files must be in the top level of the 'hooks' directory
171+
if filepath.Dir(f.Name) != "hooks" {
172+
return false
173+
}
174+
// Valid file modes are regular files or symlinks. All others (directories,
175+
// sockets, devices, etc.) are considered invalid file modes.
176+
switch mode := f.Mode(); {
177+
case mode.IsRegular():
178+
return true
179+
case mode&fs.ModeSymlink != 0:
180+
return true
181+
default:
182+
return false
183+
}
184+
}
185+
179186
func (c *LocalCharmClient) validateCharmVersion(ch charm.Charm, agentVersion version.Number) error {
180187
minver := ch.Meta().MinJujuVersion
181188
if minver != version.Zero {

0 commit comments

Comments
 (0)