diff --git a/config/shared/errors/errors.go b/config/shared/errors/errors.go index 9058e0b80b..e9142f9180 100644 --- a/config/shared/errors/errors.go +++ b/config/shared/errors/errors.go @@ -84,6 +84,9 @@ var ( ErrInvalidProxy = errors.New("proxies must be http(s)") ErrInsecureProxy = errors.New("insecure plaintext HTTP proxy specified for HTTPS resources") ErrPathConflictsSystemd = errors.New("path conflicts with systemd unit or dropin") + ErrUnsupportedArchiveType = errors.New("unsupported archive type") + ErrArchiveTypeRequired = errors.New("archive type is required") + ErrOverwriteMustBeTrue = errors.New("overwrite must be true when specifying directory contents") // Systemd section errors ErrInvalidSystemdExt = errors.New("invalid systemd unit extension") diff --git a/config/v3_4_experimental/schema/ignition.json b/config/v3_4_experimental/schema/ignition.json index d93ce6c2d0..7da9117804 100644 --- a/config/v3_4_experimental/schema/ignition.json +++ b/config/v3_4_experimental/schema/ignition.json @@ -40,6 +40,21 @@ } } }, + "archiveResource": { + "allOf": [ + { + "$ref": "#/definitions/resource" + }, + { + "type": "object", + "properties": { + "archive": { + "type": ["string", "null"] + } + } + } + ] + }, "verification": { "type": "object", "properties": { @@ -394,6 +409,9 @@ "properties": { "mode": { "type": ["integer", "null"] + }, + "contents": { + "$ref": "#/definitions/archiveResource" } } } diff --git a/config/v3_4_experimental/types/directory.go b/config/v3_4_experimental/types/directory.go index f6f0684557..f83eb39179 100644 --- a/config/v3_4_experimental/types/directory.go +++ b/config/v3_4_experimental/types/directory.go @@ -15,6 +15,9 @@ package types import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" "github.com/coreos/vcontext/report" ) @@ -22,5 +25,8 @@ import ( func (d Directory) Validate(c path.ContextPath) (r report.Report) { r.Merge(d.Node.Validate(c)) r.AddOnError(c.Append("mode"), validateMode(d.Mode)) + if !util.NilOrEmpty(d.Contents.Archive) && (d.Overwrite == nil || !*d.Overwrite) { + r.AddOnError(c.Append("overwrite"), errors.ErrOverwriteMustBeTrue) + } return } diff --git a/config/v3_4_experimental/types/resource.go b/config/v3_4_experimental/types/resource.go index 68da6c7b78..ee3aada6ce 100644 --- a/config/v3_4_experimental/types/resource.go +++ b/config/v3_4_experimental/types/resource.go @@ -89,3 +89,24 @@ func (res Resource) validateRequiredSource() error { } return validateURL(*res.Source) } + +func (res ArchiveResource) Validate(c path.ContextPath) (r report.Report) { + r.Merge(res.Resource.Validate(c)) + r.AddOnError(c.Append("archive"), res.validateArchive()) + return +} + +func (res ArchiveResource) validateArchive() error { + if util.NilOrEmpty(res.Source) { + // archive can be omitted iff the contents are omitted + return nil + } + if util.NilOrEmpty(res.Archive) { + return errors.ErrArchiveTypeRequired + } + switch *res.Archive { + case "tar": + return nil + } + return errors.ErrUnsupportedArchiveType +} diff --git a/config/v3_4_experimental/types/schema.go b/config/v3_4_experimental/types/schema.go index ca25b99ea7..d5b86e17da 100644 --- a/config/v3_4_experimental/types/schema.go +++ b/config/v3_4_experimental/types/schema.go @@ -2,6 +2,15 @@ package types // generated by "schematyper --package=types config/v3_4_experimental/schema/ignition.json -o config/v3_4_experimental/types/schema.go --root-type=Config" -- DO NOT EDIT +type ArchiveResource struct { + Resource + ArchiveResourceEmbedded1 +} + +type ArchiveResourceEmbedded1 struct { + Archive *string `json:"archive,omitempty"` +} + type Clevis struct { Custom ClevisCustom `json:"custom,omitempty"` Tang []Tang `json:"tang,omitempty"` @@ -31,7 +40,8 @@ type Directory struct { } type DirectoryEmbedded1 struct { - Mode *int `json:"mode,omitempty"` + Contents ArchiveResource `json:"contents,omitempty"` + Mode *int `json:"mode,omitempty"` } type Disk struct { diff --git a/docs/configuration-v3_4_experimental.md b/docs/configuration-v3_4_experimental.md index 01e835ce3e..cee7916268 100644 --- a/docs/configuration-v3_4_experimental.md +++ b/docs/configuration-v3_4_experimental.md @@ -111,6 +111,15 @@ The Ignition configuration is a JSON document conforming to the following specif * **_group_** (object): specifies the directory's group. * **_id_** (integer): the group ID of the group. * **_name_** (string): the group name of the group. + * **_contents_** (object): options related to the contents of the directory. If specified, `overwrite` must be `true`. + * **archive** (string): format of the archive to extract into the directory. must be `tar`. + * **_compression_** (string): the type of compression used on the archive (null or gzip). Compression cannot be used with S3. + * **_source_** (string): the URL of the archive to extract. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`][rfc2397]. When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a directory already exists at the path, Ignition will do nothing. If source is omitted and no directory exists, an empty directory will be created. + * **_httpHeaders_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the archive file. + * **_hash_** (string): the hash of the archive file, in the form `-` where type is either `sha512` or `sha256`. * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. * **path** (string): the absolute path to the link * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. diff --git a/internal/exec/stages/files/filesystemEntries.go b/internal/exec/stages/files/filesystemEntries.go index 18e00778ae..b1a1e2cf33 100644 --- a/internal/exec/stages/files/filesystemEntries.go +++ b/internal/exec/stages/files/filesystemEntries.go @@ -17,6 +17,7 @@ package files import ( "encoding/json" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -285,7 +286,7 @@ func (tmp fileEntry) create(l *log.Logger, u util.Util) error { for _, op := range fetchOps { msg := "writing file %q" - if op.Append { + if op.Mode == util.FetchAppend { msg = "appending to file %q" } if err := l.LogOp( @@ -323,6 +324,33 @@ func (tmp dirEntry) create(l *log.Logger, u util.Util) error { return fmt.Errorf("error creating directory %s: A non-directory already exists and overwrite is false", d.Path) } + if d.Contents.Archive != nil { + dirf, err := os.Open(d.Path) + if err != nil { + return fmt.Errorf("open() failed on %s: %v", d.Path, err) + } + switch _, err := dirf.Readdirnames(1); { + case err == nil: + return fmt.Errorf("refusing to populate directory %s: directory is not empty and overwrite is false", d.Path) + case err != io.EOF: + return fmt.Errorf("readdirnames() failed on %s: %v", d.Path, err) + } + + fetch, err := util.MakeFetchOp(l, d.Node, d.Contents.Resource) + if err != nil { + return fmt.Errorf("failed to resolve directory %q: %v", d.Path, err) + } + fetch.Mode = util.FetchExtract + fetch.ArchiveType = util.ArchiveType(*d.Contents.Archive) + + op := func() error { + return u.PerformFetch(fetch) + } + if err := l.LogOp(op, "populating directory %q", d.Path); err != nil { + return fmt.Errorf("failed to populate directory %q: %v", d.Path, err) + } + } + if err := u.SetPermissions(d.Mode, d.Node); err != nil { return fmt.Errorf("error setting directory permissions for %s: %v", d.Path, err) } diff --git a/internal/exec/util/archive.go b/internal/exec/util/archive.go new file mode 100644 index 0000000000..ab51ec76e6 --- /dev/null +++ b/internal/exec/util/archive.go @@ -0,0 +1,250 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/sys/unix" + + "github.com/coreos/ignition/v2/internal/log" +) + +const filemode = 07777 + +type archiveWalker interface { + Walk(l *log.Logger, path string, fn WalkFunc) error +} + +type FileInfo struct { + Name string + Linkname string + Mode int64 + Size int64 + UID int + GID int + Xattrs map[string]string +} + +type WalkFunc func(fi FileInfo, r io.Reader) error + +type tarWalker struct{} + +func (tarWalker) Walk(l *log.Logger, path string, fn WalkFunc) error { + ar, err := os.Open(path) + if err != nil { + return err + } + defer ar.Close() + + rd := tar.NewReader(ar) + for { + hdr, err := rd.Next() + switch { + case err == io.EOF: + return nil + case err != nil: + return err + } + + fi := FileInfo{ + Name: hdr.Name, + Mode: hdr.Mode & filemode, + Size: hdr.Size, + UID: hdr.Uid, + GID: hdr.Gid, + Xattrs: make(map[string]string), + } + + switch hdr.Typeflag { + case tar.TypeLink: + fi.Linkname = hdr.Linkname + fallthrough + case tar.TypeReg: + fi.Mode |= unix.S_IFREG + case tar.TypeSymlink: + fi.Linkname = hdr.Linkname + fi.Mode |= unix.S_IFLNK + case tar.TypeDir: + fi.Mode |= unix.S_IFDIR + default: + l.Warning("Unsupported TAR file type %q, skipping it.", hdr.Typeflag) + } + + // Prefer user/group names for portability. Most archives don't have + // users/groups other than 0, but a file owned by "bin" may have + // different IDs depending on the distribution. + if hdr.Uname != "" { + usr, err := user.Lookup(hdr.Uname) + if err == nil { + fi.UID, err = strconv.Atoi(usr.Uid) + } + if err != nil { + l.Warning("could not look up user %v, defaulting to UID %d: %v", hdr.Uname, fi.UID, err) + } + } + if hdr.Gname != "" { + grp, err := user.LookupGroup(hdr.Gname) + if err == nil { + fi.GID, err = strconv.Atoi(grp.Gid) + } + if err != nil { + l.Warning("could not look up group %v, defaulting to GID %d: %v", hdr.Gname, fi.GID, err) + } + } + + for k, v := range hdr.PAXRecords { + if !strings.HasPrefix(k, "SCHILY.xattr.") { + continue + } + fi.Xattrs[k[len("SCHILY.xattr."):]] = v + } + + if err := fn(fi, rd); err != nil { + return err + } + } +} + +func (u Util) extract(walker archiveWalker, from, to string) error { + destdir, err := unix.Open(to, unix.O_PATH|unix.O_DIRECTORY, 0) + if err != nil { + return &os.PathError{Op: "open", Path: to, Err: err} + } + defer unix.Close(destdir) + + return walker.Walk(u.Logger, from, func(fi FileInfo, r io.Reader) error { + + u.Debug("extracting %v", fi.Name) + + // Make sure we do not follow symlinks or magic links, and that we + // resolve paths relative to the destination. This is less expensive + // than chroot, and prevents any nonsense with regard to tarballs + // trying to write outside of the destination directory. + const resolve = unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_SYMLINKS + + dirfd, err := unix.Openat2(destdir, filepath.Dir(fi.Name), &unix.OpenHow{ + Flags: unix.O_PATH | unix.O_DIRECTORY | unix.O_NOFOLLOW, + Resolve: resolve, + }) + if err != nil { + return &os.PathError{Op: "open", Path: filepath.Dir(fi.Name), Err: err} + } + defer unix.Close(dirfd) + + name := filepath.Base(fi.Name) + + setattrs := func(fd int, name string) error { + if err := unix.Fchownat(fd, name, fi.UID, fi.GID, unix.AT_EMPTY_PATH); err != nil { + return &os.PathError{Op: "chown", Path: fi.Name, Err: err} + } + + if name == "" { + if err := unix.Fchmod(fd, uint32(fi.Mode&filemode)); err != nil { + return &os.PathError{Op: "chmod", Path: fi.Name, Err: err} + } + + for attr, val := range fi.Xattrs { + if err := unix.Fsetxattr(fd, attr, []byte(val), 0); err != nil { + return &os.PathError{Op: "setxattr", Path: fi.Name, Err: err} + } + } + } else { + if err := unix.Fchmodat(fd, name, uint32(fi.Mode&filemode), 0); err != nil { + return &os.PathError{Op: "chmod", Path: fi.Name, Err: err} + } + + // There is no fsetxattrat, but lsetxattr is good enough. + for attr, val := range fi.Xattrs { + if err := unix.Lsetxattr(fi.Name, attr, []byte(val), 0); err != nil { + return &os.PathError{Op: "setxattr", Path: fi.Name, Err: err} + } + } + } + return nil + } + + switch fi.Mode & unix.S_IFMT { + case unix.S_IFREG: + if fi.Linkname != "" { + target := fi.Linkname + if !filepath.IsAbs(target) { + target = filepath.Join(filepath.Dir(fi.Name), target) + } + + srcfd, err := unix.Openat2(destdir, target, &unix.OpenHow{ + Flags: unix.O_PATH | unix.O_NOFOLLOW, + Resolve: resolve, + }) + if err != nil { + return &os.PathError{Op: "open", Path: target, Err: err} + } + defer unix.Close(srcfd) + + if err := unix.Linkat(srcfd, "", dirfd, name, unix.AT_EMPTY_PATH); err != nil { + return &os.PathError{Op: "link", Path: fi.Name, Err: err} + } + } else { + fd, err := unix.Openat2(dirfd, name, &unix.OpenHow{ + Flags: unix.O_WRONLY | unix.O_CREAT | unix.O_TRUNC, + Resolve: resolve, + }) + if err != nil { + return &os.PathError{Op: "open", Path: fi.Name, Err: err} + } + f := os.NewFile(uintptr(fd), fi.Name) + defer f.Close() + + if _, err := io.Copy(f, r); err != nil { + return &os.PathError{Op: "extract", Path: fi.Name, Err: err} + } + + if err := setattrs(fd, ""); err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + } + case unix.S_IFDIR: + if err := unix.Mkdirat(dirfd, name, 0); err != nil && !errors.Is(err, unix.EEXIST) { + return &os.PathError{Op: "mkdir", Path: fi.Name, Err: err} + } + if err := setattrs(dirfd, name); err != nil { + return err + } + case unix.S_IFLNK: + if err := unix.Symlinkat(fi.Linkname, dirfd, name); err != nil { + return &os.PathError{Op: "symlink", Path: fi.Name, Err: err} + } + if err := setattrs(dirfd, name); err != nil { + return err + } + default: + return &os.PathError{Op: "walk", Path: fi.Name, Err: fmt.Errorf("Unsupported file type %v", fi.Mode&unix.S_IFMT)} + } + + return nil + }) +} diff --git a/internal/exec/util/file.go b/internal/exec/util/file.go index db666915b1..f996422a94 100644 --- a/internal/exec/util/file.go +++ b/internal/exec/util/file.go @@ -40,15 +40,30 @@ const ( DefaultFilePermissions os.FileMode = 0644 ) +type FetchMode int + +const ( + FetchReplace FetchMode = iota + FetchAppend + FetchExtract +) + +type ArchiveType string + +const ( + ArchiveTAR ArchiveType = "tar" +) + type FetchOp struct { Hash hash.Hash Url url.URL FetchOptions resource.FetchOptions - Append bool + Mode FetchMode + ArchiveType ArchiveType Node types.Node } -func newFetchOp(l *log.Logger, node types.Node, contents types.Resource) (FetchOp, error) { +func MakeFetchOp(l *log.Logger, node types.Node, contents types.Resource) (FetchOp, error) { var expectedSum []byte uri, err := url.Parse(*contents.Source) @@ -106,7 +121,7 @@ func (u Util) PrepareFetches(l *log.Logger, f types.File) ([]FetchOp, error) { ops := []FetchOp{} if f.Contents.Source != nil { - if base, err := newFetchOp(l, f.Node, f.Contents); err != nil { + if base, err := MakeFetchOp(l, f.Node, f.Contents); err != nil { return nil, err } else { ops = append(ops, base) @@ -114,10 +129,10 @@ func (u Util) PrepareFetches(l *log.Logger, f types.File) ([]FetchOp, error) { } for _, appendee := range f.Append { - if op, err := newFetchOp(l, f.Node, appendee); err != nil { + if op, err := MakeFetchOp(l, f.Node, appendee); err != nil { return nil, err } else { - op.Append = true + op.Mode = FetchAppend ops = append(ops, op) } } @@ -216,7 +231,8 @@ func (u Util) PerformFetch(f FetchOp) error { return err } - if f.Append { + switch f.Mode { + case FetchAppend: // Make sure that we're appending to a file finfo, err := os.Lstat(path) switch { @@ -244,10 +260,21 @@ func (u Util) PerformFetch(f FetchOp) error { if _, err = io.Copy(targetFile, tmp); err != nil { return err } - } else { + case FetchReplace: if err = os.Rename(tmp.Name(), path); err != nil { return err } + case FetchExtract: + var walker archiveWalker + switch f.ArchiveType { + case ArchiveTAR: + walker = tarWalker{} + default: + return fmt.Errorf("unsupported archive type %s", string(f.ArchiveType)) + } + if err := u.extract(walker, tmp.Name(), path); err != nil { + return err + } } return nil diff --git a/tests/negative/files/directory.go b/tests/negative/files/directory.go new file mode 100644 index 0000000000..f5aab3ccb6 --- /dev/null +++ b/tests/negative/files/directory.go @@ -0,0 +1,85 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package files + +import ( + "github.com/coreos/ignition/v2/tests/register" + "github.com/coreos/ignition/v2/tests/types" +) + +func init() { + register.Register(register.NegativeTest, DirectoryFromArchiveMustSetOverwrite()) + register.Register(register.NegativeTest, DirectoryFromArchiveMustSetArchive()) +} + +func DirectoryFromArchiveMustSetOverwrite() types.Test { + name := "directories.must-set-overwrite" + in := types.GetBaseDisk() + out := in + config := `{ + "ignition": { "version": "$version" }, + "storage": { + "directories": [ + { + "path": "/foo/bar", + "contents": { + "archive": "tar", + "source": "data:;base64," + } + } + ] + } + }` + configMinVersion := "3.4.0-experimental" + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigMinVersion: configMinVersion, + ConfigShouldBeBad: true, + } +} + +func DirectoryFromArchiveMustSetArchive() types.Test { + name := "directories.must-set-archive" + in := types.GetBaseDisk() + out := in + config := `{ + "ignition": { "version": "$version" }, + "storage": { + "directories": [ + { + "path": "/foo/bar", + "overwrite": true, + "contents": { + "source": "data:;base64," + } + } + ] + } + }` + configMinVersion := "3.4.0-experimental" + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigMinVersion: configMinVersion, + ConfigShouldBeBad: true, + } +} diff --git a/tests/positive/files/directory.go b/tests/positive/files/directory.go index 5bda64c136..a256d473bc 100644 --- a/tests/positive/files/directory.go +++ b/tests/positive/files/directory.go @@ -15,6 +15,19 @@ package files import ( + "archive/tar" + "compress/gzip" + "encoding/base64" + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/sys/unix" + "github.com/coreos/ignition/v2/tests/register" "github.com/coreos/ignition/v2/tests/types" ) @@ -26,6 +39,7 @@ func init() { register.Register(register.PositiveTest, DirCreationOverNonemptyDir()) register.Register(register.PositiveTest, CheckOrdering()) register.Register(register.PositiveTest, ApplyDefaultDirectoryPermissions()) + register.Register(register.PositiveTest, CreateDirectoryFromTAR()) } func CreateDirectoryOnRoot() types.Test { @@ -279,3 +293,210 @@ func ApplyDefaultDirectoryPermissions() types.Test { ConfigMinVersion: configMinVersion, } } + +func CreateDirectoryFromTAR() types.Test { + name := "directories.tar" + in := types.GetBaseDisk() + out := types.GetBaseDisk() + + handleErr := func(err error) { + if err != nil { + panic(fmt.Sprintf("error generating test archive: %v", err)) + } + } + + type filespec struct { + tar.Header + Contents string + } + + usr, err := user.Lookup("bin") + handleErr(err) + binUID, err := strconv.Atoi(usr.Uid) + handleErr(err) + + grp, err := user.Lookup("daemon") + handleErr(err) + daemonGID, err := strconv.Atoi(grp.Gid) + handleErr(err) + + files := []filespec{ + { + Header: tar.Header{ + Typeflag: tar.TypeReg, + Name: "reg", + }, + Contents: "Hello, world\n", + }, + { + Header: tar.Header{ + Typeflag: tar.TypeDir, + Name: "dir", + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeSymlink, + Name: "symlink", + Linkname: "reg", + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeSymlink, + Name: "dir/symlink", + Linkname: "../reg", + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeLink, + Name: "link", + Linkname: "/reg", + }, + }, + { + // Verify that hard link resolution is done relative to target + Header: tar.Header{ + Typeflag: tar.TypeLink, + Name: "dir/link", + Linkname: "../reg", + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeReg, + Name: "xattrs", + Xattrs: map[string]string{ + "security.capability": "\x00\x00\x00\x02\xc2\x10\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // some permitted-only capabilities + "user.foo": "bar", + }, + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeReg, + Name: "uidgid", + Uid: 1, + Gid: 2, + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeReg, + Name: "usergroup", + Uname: "bin", + Gname: "daemon", + Uid: binUID, + Gid: daemonGID, + }, + }, + { + Header: tar.Header{ + Typeflag: tar.TypeReg, + Name: "mode", + Mode: 01234, + }, + }, + } + + var archive strings.Builder + + enc := base64.NewEncoder(base64.StdEncoding, &archive) + gz := gzip.NewWriter(enc) + ar := tar.NewWriter(gz) + + for _, file := range files { + node := types.Node{ + Name: filepath.Base(file.Name), + Directory: filepath.Join("dir", filepath.Dir(file.Name)), + User: file.Uid, + Group: file.Gid, + } + + switch file.Typeflag { + case tar.TypeReg: + if file.Size == 0 { + file.Size = int64(len(file.Contents)) + } + + // types.File.Mode is actually an os.FileMode more than a file mode + // as defined by stat, so we have to convert between the upper bits + // and the portable Go definitions of setuid/setgid/sticky. + mode := os.FileMode(file.Mode & 0777) + if file.Mode&unix.S_ISUID != 0 { + mode |= os.ModeSetuid + } + if file.Mode&unix.S_ISGID != 0 { + mode |= os.ModeSetgid + } + if file.Mode&unix.S_ISVTX != 0 { + mode |= os.ModeSticky + } + + out[0].Partitions.AddFiles("ROOT", []types.File{{ + Node: node, + Contents: file.Contents, + Mode: int(mode), + }}) + case tar.TypeDir: + out[0].Partitions.AddDirectories("ROOT", []types.Directory{{ + Node: node, + }}) + case tar.TypeLink: + target := file.Linkname + if filepath.IsAbs(target) { + target = filepath.Join("dir", target) + } else { + target = filepath.Join(node.Directory, file.Linkname) + } + out[0].Partitions.AddLinks("ROOT", []types.Link{{ + Node: node, + Target: target, + Hard: true, + }}) + case tar.TypeSymlink: + out[0].Partitions.AddLinks("ROOT", []types.Link{{ + Node: node, + Target: file.Linkname, + }}) + } + + handleErr(ar.WriteHeader(&file.Header)) + if file.Typeflag == tar.TypeReg { + _, err := io.WriteString(ar, file.Contents) + handleErr(err) + } + } + + handleErr(ar.Close()) + handleErr(gz.Close()) + handleErr(enc.Close()) + + config := fmt.Sprintf(`{ + "ignition": { "version": "$version" }, + "storage": { + "directories": [ + { + "path": "/dir", + "overwrite": true, + "contents": { + "archive": "tar", + "source": "data:;base64,%v", + "compression": "gzip" + } + } + ] + } + }`, archive.String()) + + configMinVersion := "3.4.0-experimental" + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigMinVersion: configMinVersion, + } +}