Skip to content

Commit 9cbbb3e

Browse files
committed
feat(erofs): initial commit for erofs support
Fixes opencontainers/image-spec#1190 Signed-off-by: Ramkumar Chinchani <[email protected]>
1 parent adf0a37 commit 9cbbb3e

File tree

5 files changed

+883
-0
lines changed

5 files changed

+883
-0
lines changed

pkg/erofs/erofs.go

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package erofs
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"os"
7+
"os/exec"
8+
"path"
9+
"path/filepath"
10+
"strings"
11+
"sync"
12+
13+
"github.com/pkg/errors"
14+
)
15+
16+
var checkZstdSupported sync.Once
17+
var zstdIsSuspported bool
18+
19+
// ExcludePaths represents a list of paths to exclude in a erofs listing.
20+
// Users should do something like filepath.Walk() over the whole filesystem,
21+
// calling AddExclude() or AddInclude() based on whether they want to include
22+
// or exclude a particular file. Note that if e.g. /usr is excluded, then
23+
// everyting underneath is also implicitly excluded. The
24+
// AddExclude()/AddInclude() methods do the math to figure out what is the
25+
// correct set of things to exclude or include based on what paths have been
26+
// previously included or excluded.
27+
type ExcludePaths struct {
28+
exclude map[string]bool
29+
include []string
30+
}
31+
32+
func NewExcludePaths() *ExcludePaths {
33+
return &ExcludePaths{
34+
exclude: map[string]bool{},
35+
include: []string{},
36+
}
37+
}
38+
39+
func (eps *ExcludePaths) AddExclude(p string) {
40+
for _, inc := range eps.include {
41+
// If /usr/bin/ls has changed but /usr hasn't, we don't want to list
42+
// /usr in the include paths any more, so let's be sure to only
43+
// add things which aren't prefixes.
44+
if strings.HasPrefix(inc, p) {
45+
return
46+
}
47+
}
48+
eps.exclude[p] = true
49+
}
50+
51+
func (eps *ExcludePaths) AddInclude(orig string, isDir bool) {
52+
// First, remove this thing and all its parents from exclude.
53+
p := orig
54+
55+
// normalize to the first dir
56+
if !isDir {
57+
p = path.Dir(p)
58+
}
59+
for {
60+
// our paths are all absolute, so this is a base case
61+
if p == "/" {
62+
break
63+
}
64+
65+
delete(eps.exclude, p)
66+
p = filepath.Dir(p)
67+
}
68+
69+
// now add it to the list of includes, so we don't accidentally re-add
70+
// anything above.
71+
eps.include = append(eps.include, orig)
72+
}
73+
74+
func (eps *ExcludePaths) String() (string, error) {
75+
var buf bytes.Buffer
76+
for p := range eps.exclude {
77+
_, err := buf.WriteString(p)
78+
if err != nil {
79+
return "", err
80+
}
81+
_, err = buf.WriteString("\n")
82+
if err != nil {
83+
return "", err
84+
}
85+
}
86+
87+
_, err := buf.WriteString("\n")
88+
if err != nil {
89+
return "", err
90+
}
91+
92+
return buf.String(), nil
93+
}
94+
95+
func MakeErofs(tempdir string, rootfs string, eps *ExcludePaths, verity VerityMetadata) (io.ReadCloser, string, string, error) {
96+
var excludesFile string
97+
var err error
98+
var toExclude string
99+
var rootHash string
100+
101+
if eps != nil {
102+
toExclude, err = eps.String()
103+
if err != nil {
104+
return nil, "", rootHash, errors.Wrapf(err, "couldn't create exclude path list")
105+
}
106+
}
107+
108+
if len(toExclude) != 0 {
109+
excludes, err := os.CreateTemp(tempdir, "stacker-erofs-exclude-")
110+
if err != nil {
111+
return nil, "", rootHash, err
112+
}
113+
defer os.Remove(excludes.Name())
114+
115+
excludesFile = excludes.Name()
116+
_, err = excludes.WriteString(toExclude)
117+
excludes.Close()
118+
if err != nil {
119+
return nil, "", rootHash, err
120+
}
121+
}
122+
123+
tmpErofs, err := os.CreateTemp(tempdir, "stacker-erofs-img-")
124+
if err != nil {
125+
return nil, "", rootHash, err
126+
}
127+
tmpErofs.Close()
128+
os.Remove(tmpErofs.Name())
129+
defer os.Remove(tmpErofs.Name())
130+
args := []string{rootfs, tmpErofs.Name()}
131+
compression := GzipCompression
132+
if mkerofsSupportsZstd() {
133+
args = append(args, "-z", "zstd")
134+
compression = ZstdCompression
135+
}
136+
if len(toExclude) != 0 {
137+
args = append(args, "--exclude-path", excludesFile)
138+
}
139+
cmd := exec.Command("mkfs.erofs", args...)
140+
cmd.Stdout = os.Stdout
141+
cmd.Stderr = os.Stderr
142+
if err = cmd.Run(); err != nil {
143+
return nil, "", rootHash, errors.Wrap(err, "couldn't build erofs")
144+
}
145+
146+
if verity {
147+
rootHash, err = appendVerityData(tmpErofs.Name())
148+
if err != nil {
149+
return nil, "", rootHash, err
150+
}
151+
}
152+
153+
blob, err := os.Open(tmpErofs.Name())
154+
if err != nil {
155+
return nil, "", rootHash, errors.WithStack(err)
156+
}
157+
158+
return blob, GenerateErofsMediaType(compression, verity), rootHash, nil
159+
}
160+
161+
func mkerofsSupportsZstd() bool {
162+
checkZstdSupported.Do(func() {
163+
var stdoutBuffer strings.Builder
164+
var stderrBuffer strings.Builder
165+
166+
cmd := exec.Command("mkfs.erofs", "--help")
167+
cmd.Stdout = &stdoutBuffer
168+
cmd.Stderr = &stderrBuffer
169+
170+
// Ignore errs here as `mkerofs --help` exit status code is 1
171+
_ = cmd.Run()
172+
173+
if strings.Contains(stdoutBuffer.String(), "zstd") ||
174+
strings.Contains(stderrBuffer.String(), "zstd") {
175+
zstdIsSuspported = true
176+
}
177+
})
178+
179+
return zstdIsSuspported
180+
}

pkg/erofs/mediatype.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package erofs
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
type ErofsCompression string
9+
type VerityMetadata bool
10+
11+
const (
12+
BaseMediaTypeLayerErofs = "application/vnd.stacker.image.layer.erofs"
13+
14+
GzipCompression ErofsCompression = "gzip"
15+
ZstdCompression ErofsCompression = "zstd"
16+
17+
veritySuffix = "verity"
18+
19+
VerityMetadataPresent VerityMetadata = true
20+
VerityMetadataMissing VerityMetadata = false
21+
)
22+
23+
func IsErofsMediaType(mediaType string) bool {
24+
return strings.HasPrefix(mediaType, BaseMediaTypeLayerErofs)
25+
}
26+
27+
func GenerateErofsMediaType(comp ErofsCompression, verity VerityMetadata) string {
28+
verityString := ""
29+
if verity {
30+
verityString = fmt.Sprintf("+%s", veritySuffix)
31+
}
32+
return fmt.Sprintf("%s+%s%s", BaseMediaTypeLayerErofs, comp, verityString)
33+
}
34+
35+
func HasVerityMetadata(mediaType string) VerityMetadata {
36+
return VerityMetadata(strings.HasSuffix(mediaType, veritySuffix))
37+
}

0 commit comments

Comments
 (0)