Skip to content

Commit 5ee50f8

Browse files
committed
snap/pack: ensure layout sources presence for core26 and later WIP
Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com>
1 parent ca8e2aa commit 5ee50f8

2 files changed

Lines changed: 184 additions & 3 deletions

File tree

snap/pack/pack.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"io"
2525
"os"
2626
"path/filepath"
27+
"sort"
2728
"strings"
2829

2930
"github.com/snapcore/snapd/gadget"
@@ -120,9 +121,6 @@ func validateContentPlugTargets(container snap.Container, info *snap.Info) error
120121
if err := plug.Attr("target", &target); err != nil || target == "" {
121122
continue
122123
}
123-
// Only $SNAP paths (or paths with no variable prefix) can be checked at
124-
// pack time; $SNAP_DATA and $SNAP_COMMON are read-write runtime
125-
// directories and mount points can be created as needed.
126124
if strings.HasPrefix(target, "$SNAP_DATA") || strings.HasPrefix(target, "$SNAP_COMMON") {
127125
continue
128126
}
@@ -142,6 +140,67 @@ func validateContentPlugTargets(container snap.Container, info *snap.Info) error
142140
return nil
143141
}
144142

143+
// validateLayoutSources checks that layout bind and bind-file source paths
144+
// exist in the snap source tree. This check only applies to snaps with base
145+
// core26 or later.
146+
func validateLayoutSources(container snap.Container, info *snap.Info) error {
147+
// An empty base is equivalent to "core".
148+
base := info.Base
149+
if base == "" {
150+
base = "core"
151+
}
152+
// Layout source validation does not apply to bases before core26.
153+
excluded := []string{
154+
"bare", "core", "core18", "core20", "core22", "core24",
155+
}
156+
if strutil.ListContains(excluded, base) {
157+
return nil
158+
}
159+
160+
// Sort layout paths for deterministic error reporting.
161+
layoutPaths := make([]string, 0, len(info.Layout))
162+
for p := range info.Layout {
163+
layoutPaths = append(layoutPaths, p)
164+
}
165+
sort.Strings(layoutPaths)
166+
167+
for _, layoutPath := range layoutPaths {
168+
layout := info.Layout[layoutPath]
169+
170+
// Determine the source path. Only check bind and bind-file;
171+
// symlink targets and tmpfs layouts are not validated.
172+
source := layout.Bind
173+
if source == "" {
174+
source = layout.BindFile
175+
}
176+
if source == "" {
177+
continue
178+
}
179+
180+
// Only $SNAP paths can be checked at pack time.
181+
if strings.HasPrefix(source, "$SNAP_DATA") || strings.HasPrefix(source, "$SNAP_COMMON") {
182+
continue
183+
}
184+
relPath := strings.TrimPrefix(source, "$SNAP")
185+
relPath = strings.TrimPrefix(relPath, "/")
186+
if relPath == "" {
187+
continue
188+
}
189+
190+
fi, err := container.Lstat(relPath)
191+
if err != nil {
192+
return fmt.Errorf("layout %q source %q does not exist", layoutPath, source)
193+
}
194+
if layout.Bind != "" && !fi.IsDir() {
195+
return fmt.Errorf("layout %q source %q must be a directory", layoutPath, source)
196+
}
197+
if layout.BindFile != "" && fi.IsDir() {
198+
return fmt.Errorf("layout %q source %q must be a file", layoutPath, source)
199+
}
200+
}
201+
return nil
202+
}
203+
145204
// CheckSkeleton attempts to validate snap data in source directory
146205
func CheckSkeleton(w io.Writer, sourceDir string) error {
147206
yaml, err := os.ReadFile(filepath.Join(sourceDir, "meta", "snap.yaml"))
@@ -177,6 +236,9 @@ func loadAndValidate(sourceDir string, yaml []byte) (*snap.Info, error) {
177236
if err := validateContentPlugTargets(container, info); err != nil {
178237
return nil, err
179238
}
239+
if err := validateLayoutSources(container, info); err != nil {
240+
return nil, err
241+
}
180242
if _, err := snap.ReadSnapshotYamlFromSnapFile(container); err != nil {
181243
return nil, err
182244
}

snap/pack/pack_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,3 +745,122 @@ plugs:
745745
err := pack.CheckSkeleton(&buf, sourceDir)
746746
c.Assert(err, ErrorMatches, `content interface plug "plug-missing" target "\$SNAP/missing" does not exist`)
747747
}
748+
749+
type layoutSourceTestCase struct {
750+
summary string
751+
base string
752+
layout string // layout fragment (indented, under "layout:" key)
753+
plugs string // plugs fragment (indented, under "plugs:" key, optional)
754+
files []string // paths to create: trailing "/" = dir, otherwise file
755+
errMatch string // expected error regex, "" = no error expected
756+
}
757+
758+
func (s *packSuite) checkSkeletonLayoutSource(c *C, tc layoutSourceTestCase) {
759+
yamlStr := fmt.Sprintf("name: hello\nversion: 0\nbase: %s\n", tc.base)
760+
if tc.plugs != "" {
761+
yamlStr += "plugs:\n" + tc.plugs
762+
}
763+
yamlStr += "layout:\n" + tc.layout
764+
765+
sourceDir := makeExampleSnapSourceDir(c, yamlStr)
766+
for _, f := range tc.files {
767+
path := filepath.Join(sourceDir, f)
768+
if strings.HasSuffix(f, "/") {
769+
c.Assert(os.MkdirAll(path, 0755), IsNil)
770+
} else {
771+
c.Assert(os.MkdirAll(filepath.Dir(path), 0755), IsNil)
772+
c.Assert(os.WriteFile(path, []byte(""), 0644), IsNil)
773+
}
774+
}
775+
var buf bytes.Buffer
776+
err := pack.CheckSkeleton(&buf, sourceDir)
777+
if tc.errMatch == "" {
778+
c.Assert(err, IsNil, Commentf("test: %s", tc.summary))
779+
} else {
780+
c.Assert(err, ErrorMatches, tc.errMatch, Commentf("test: %s", tc.summary))
781+
}
782+
}
783+
784+
func (s *packSuite) TestCheckSkeletonLayoutSourceValid(c *C) {
785+
for _, tc := range []layoutSourceTestCase{
786+
{
787+
summary: "bind source directory exists",
788+
base: "core26",
789+
layout: " /opt/lib:\n bind: $SNAP/lib\n",
790+
files: []string{"lib/"},
791+
},
792+
{
793+
summary: "bind-file source file exists",
794+
base: "core26",
795+
layout: " /opt/foo.conf:\n bind-file: $SNAP/foo.conf\n",
796+
files: []string{"foo.conf"},
797+
},
798+
{
799+
summary: "source under content target with full path present",
800+
base: "core26",
801+
plugs: " gnome:\n interface: content\n target: $SNAP/gnome-platform\n",
802+
layout: " /usr/lib/webkit:\n bind: $SNAP/gnome-platform/usr/lib/webkit\n",
803+
files: []string{"gnome-platform/usr/lib/webkit/"},
804+
},
805+
{
806+
summary: "source is content target directory",
807+
base: "core26",
808+
layout: " /opt/gnome:\n bind: $SNAP/gnome-platform\n",
809+
files: []string{"gnome-platform/"},
810+
},
811+
{
812+
summary: "symlink layout not checked",
813+
base: "core26",
814+
layout: " /opt/data:\n symlink: $SNAP/data\n",
815+
},
816+
{
817+
summary: "$SNAP_DATA source skipped",
818+
base: "core26",
819+
layout: " /opt/lib:\n bind: $SNAP_DATA/lib\n",
820+
},
821+
{
822+
summary: "$SNAP_COMMON source skipped",
823+
base: "core26",
824+
layout: " /opt/lib:\n bind: $SNAP_COMMON/lib\n",
825+
},
826+
} {
827+
s.checkSkeletonLayoutSource(c, tc)
828+
}
829+
}
830+
831+
func (s *packSuite) TestCheckSkeletonLayoutSourceInvalid(c *C) {
832+
for _, tc := range []layoutSourceTestCase{
833+
{
834+
summary: "bind source missing",
835+
base: "core26",
836+
layout: " /opt/lib:\n bind: $SNAP/lib\n",
837+
errMatch: `layout "/opt/lib" source "\$SNAP/lib" does not exist`,
838+
},
839+
{
840+
summary: "bind source is a file not directory",
841+
base: "core26",
842+
layout: " /opt/lib:\n bind: $SNAP/lib\n",
843+
files: []string{"lib"},
844+
errMatch: `layout "/opt/lib" source "\$SNAP/lib" must be a directory`,
845+
},
846+
{
847+
summary: "bind-file source is a directory not file",
848+
base: "core26",
849+
layout: " /opt/foo.conf:\n bind-file: $SNAP/foo.conf\n",
850+
files: []string{"foo.conf/"},
851+
errMatch: `layout "/opt/foo.conf" source "\$SNAP/foo.conf" must be a file`,
852+
},
853+
} {
854+
s.checkSkeletonLayoutSource(c, tc)
855+
}
856+
}
857+
858+
func (s *packSuite) TestCheckSkeletonLayoutSourceOldBaseSkipped(c *C) {
859+
for _, base := range []string{"bare", "core", "core18", "core20", "core22", "core24"} {
860+
s.checkSkeletonLayoutSource(c, layoutSourceTestCase{
861+
summary: "old base " + base,
862+
base: base,
863+
layout: " /opt/lib:\n bind: $SNAP/lib\n",
864+
})
865+
}
866+
}

0 commit comments

Comments
 (0)