Skip to content

Commit 07b5566

Browse files
authored
Use tarfs to implement structure test (#228)
Previously, we scanned through the image filesystem and matched the tar.Header.Name field against the file path given by the test. This works most of the time, but doesn't handle symlinks at all. This change writes the entire layer tar to a temporary file, parses it with tarfs to get an fs.FS, attempts to randomly access each file from the test, and deletes the temporary file afterwards. Signed-off-by: Jon Johnson <[email protected]>
1 parent 5aa4f8f commit 07b5566

File tree

4 files changed

+58
-44
lines changed

4 files changed

+58
-44
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/hashicorp/terraform-plugin-go v0.26.0
1010
github.com/hashicorp/terraform-plugin-log v0.9.0
1111
github.com/hashicorp/terraform-plugin-testing v1.12.0
12+
github.com/jonjohnsonjr/targz v0.0.0-20241113200849-4986e08f3fb4
1213
github.com/spf13/cobra v1.9.1
1314
)
1415

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
145145
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
146146
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
147147
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
148+
github.com/jonjohnsonjr/targz v0.0.0-20241113200849-4986e08f3fb4 h1:yzUKZR6eq4hfKkNLe2KfxOBiVHyjXny7g4bEDuiYCtY=
149+
github.com/jonjohnsonjr/targz v0.0.0-20241113200849-4986e08f3fb4/go.mod h1:vFsMbFCBsTclpEtIkbCOBAJj1mBsqoMtm22ibo1cG2o=
148150
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
149151
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
150152
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=

internal/provider/structure_test_data_source_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ func TestAccStructureTestDataSource(t *testing.T) {
3636
Size: 6,
3737
})
3838
_, _ = tw.Write([]byte("blah!!"))
39+
40+
// Test that /lib -> /usr/lib works.
41+
_ = tw.WriteHeader(&tar.Header{
42+
Name: "symlink",
43+
Typeflag: tar.TypeSymlink,
44+
Mode: 0755,
45+
Linkname: "path",
46+
})
47+
3948
tw.Close()
4049

4150
l, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
@@ -96,6 +105,10 @@ func TestAccStructureTestDataSource(t *testing.T) {
96105
path = "/path/to/baz"
97106
regex = "blah!!"
98107
}
108+
files {
109+
path = "/symlink/to/baz"
110+
regex = "blah!!"
111+
}
99112
}
100113
}`, ref),
101114
Check: resource.ComposeTestCheckFunc(

pkg/structure/structure.go

Lines changed: 42 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package structure
22

33
import (
4-
"archive/tar"
5-
"bytes"
64
"errors"
75
"fmt"
86
"io"
7+
"io/fs"
8+
"os"
99
"regexp"
1010
"strings"
1111

1212
v1 "github.com/google/go-containerregistry/pkg/v1"
1313
"github.com/google/go-containerregistry/pkg/v1/mutate"
14+
"github.com/jonjohnsonjr/targz/tarfs"
1415
)
1516

1617
type Condition interface {
@@ -69,7 +70,6 @@ type FilesCondition struct {
6970
type File struct {
7071
Regex string
7172
// TODO: support filemode
72-
ran bool
7373
}
7474

7575
func (f FilesCondition) Check(i v1.Image) error {
@@ -88,56 +88,54 @@ func (f FilesCondition) Check(i v1.Image) error {
8888
rc = mutate.Extract(i)
8989
}
9090

91+
tmp, err := os.CreateTemp("", "structure-test")
92+
if err != nil {
93+
return err
94+
}
95+
defer os.Remove(tmp.Name())
96+
9197
defer rc.Close()
92-
tr := tar.NewReader(rc)
93-
errs := []error{}
94-
L:
95-
for {
96-
hdr, err := tr.Next()
97-
if err == io.EOF {
98-
break
99-
} else if err != nil {
100-
return err
101-
}
102-
if !strings.HasPrefix(hdr.Name, "/") {
103-
hdr.Name = "/" + hdr.Name
104-
}
10598

106-
if _, found := f.Want[hdr.Name]; !found {
107-
// We don't care about this file at all, on to the next.
99+
size, err := io.Copy(tmp, rc)
100+
if err != nil {
101+
return err
102+
}
103+
104+
fsys, err := tarfs.New(tmp, size)
105+
if err != nil {
106+
return err
107+
}
108+
109+
var errs []error
110+
111+
for path, f := range f.Want {
112+
// https://pkg.go.dev/io/fs#ValidPath
113+
name := strings.TrimPrefix(path, "/")
114+
115+
tf, err := fsys.Open(name)
116+
if err != nil {
117+
if errors.Is(err, fs.ErrNotExist) {
118+
// Avoid breaking backward compatibility.
119+
errs = append(errs, fmt.Errorf("file %q not found", path))
120+
} else {
121+
// Any other error is unexpected, so we want to retain it.
122+
errs = append(errs, fmt.Errorf("opening %q: %w", path, err))
123+
}
108124
continue
109125
}
110-
if f.Want[hdr.Name].Regex != "" {
126+
if f.Regex != "" {
111127
// We care about the contents, so read and buffer them and regexp.
112-
var buf bytes.Buffer
113-
if _, err := io.Copy(&buf, tr); err != nil {
114-
return err
128+
got, err := io.ReadAll(tf)
129+
if err != nil {
130+
errs = append(errs, fmt.Errorf("reading %q: %w", path, err))
131+
continue
115132
}
116-
if !regexp.MustCompile(f.Want[hdr.Name].Regex).Match(buf.Bytes()) {
117-
errs = append(errs, fmt.Errorf("file %q does not match regexp %q, got:\n%s", hdr.Name, f.Want[hdr.Name].Regex, buf.String()))
118-
}
119-
}
120-
// At least mark that we found this file we cared about.
121-
f.Want[hdr.Name] = File{
122-
Regex: f.Want[hdr.Name].Regex,
123-
ran: true,
124-
}
125133

126-
// If all the checks have run, we can stop early.
127-
// This might not be strictly correct, since tar files can have multiple
128-
// files with the same name, and the last one wins; in practice, this is
129-
// unlikely to be a problem, and the optimization is worth it.
130-
for _, f := range f.Want {
131-
if !f.ran {
132-
continue L
134+
if !regexp.MustCompile(f.Regex).Match(got) {
135+
errs = append(errs, fmt.Errorf("file %q does not match regexp %q, got:\n%s", path, f.Regex, got))
133136
}
134137
}
135-
break
136-
}
137-
for path, f := range f.Want {
138-
if !f.ran {
139-
errs = append(errs, fmt.Errorf("file %q not found", path))
140-
}
141138
}
139+
142140
return errors.Join(errs...)
143141
}

0 commit comments

Comments
 (0)