Skip to content

Commit 0399712

Browse files
committed
config,storage: support populating directories from archives
Tarballs are ubiquitous as a binary release format, and without any builtin ability to fetch, validate, and extract them, we are left with half-baked hacks to do all of this from the confines of a oneshot systemd service, or worse, extracting and providing the entirety of the archive contents as files and directory entries in the ignition config, resulting in very large json documents. Let's not do this. Instead, this commit formalizes the use of archives via adding a new (optional) "contents" key under a directory entry. This new contents key is identical in function as its eponymous version in the "files" entries, except that it incorporates a new "archive" subkey to specify the archive format rather than guessing with heuristics. Today, only "archive": "tar" is supported, though this commit is structured to allow the addition of other archive types if needed.
1 parent 1613111 commit 0399712

File tree

12 files changed

+743
-9
lines changed

12 files changed

+743
-9
lines changed

config/shared/errors/errors.go

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ var (
8484
ErrInvalidProxy = errors.New("proxies must be http(s)")
8585
ErrInsecureProxy = errors.New("insecure plaintext HTTP proxy specified for HTTPS resources")
8686
ErrPathConflictsSystemd = errors.New("path conflicts with systemd unit or dropin")
87+
ErrUnsupportedArchiveType = errors.New("unsupported archive type")
88+
ErrArchiveTypeRequired = errors.New("archive type is required")
89+
ErrOverwriteMustBeTrue = errors.New("overwrite must be true when specifying directory contents")
8790

8891
// Systemd section errors
8992
ErrInvalidSystemdExt = errors.New("invalid systemd unit extension")

config/v3_4_experimental/schema/ignition.json

+18
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@
4040
}
4141
}
4242
},
43+
"archiveResource": {
44+
"allOf": [
45+
{
46+
"$ref": "#/definitions/resource"
47+
},
48+
{
49+
"type": "object",
50+
"properties": {
51+
"archive": {
52+
"type": ["string", "null"]
53+
}
54+
}
55+
}
56+
]
57+
},
4358
"verification": {
4459
"type": "object",
4560
"properties": {
@@ -394,6 +409,9 @@
394409
"properties": {
395410
"mode": {
396411
"type": ["integer", "null"]
412+
},
413+
"contents": {
414+
"$ref": "#/definitions/archiveResource"
397415
}
398416
}
399417
}

config/v3_4_experimental/types/directory.go

+15
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,27 @@
1515
package types
1616

1717
import (
18+
"github.com/coreos/ignition/v2/config/shared/errors"
19+
"github.com/coreos/ignition/v2/config/util"
20+
1821
"github.com/coreos/vcontext/path"
1922
"github.com/coreos/vcontext/report"
2023
)
2124

2225
func (d Directory) Validate(c path.ContextPath) (r report.Report) {
2326
r.Merge(d.Node.Validate(c))
2427
r.AddOnError(c.Append("mode"), validateMode(d.Mode))
28+
if !util.NilOrEmpty(d.Contents.Archive) && (d.Overwrite == nil || !*d.Overwrite) {
29+
r.AddOnError(c.Append("overwrite"), errors.ErrOverwriteMustBeTrue)
30+
}
2531
return
2632
}
33+
34+
func (d Directory) KeyPrefix() string {
35+
if util.NilOrEmpty(d.Contents.Archive) {
36+
return ""
37+
}
38+
// If a directory is populated by an archive, all other file/directory entries
39+
// in the config must conflict with any files under said directory.
40+
return d.Path
41+
}

config/v3_4_experimental/types/resource.go

+21
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,24 @@ func (res Resource) validateRequiredSource() error {
8989
}
9090
return validateURL(*res.Source)
9191
}
92+
93+
func (res ArchiveResource) Validate(c path.ContextPath) (r report.Report) {
94+
r.Merge(res.Resource.Validate(c))
95+
r.AddOnError(c.Append("archive"), res.validateArchive())
96+
return
97+
}
98+
99+
func (res ArchiveResource) validateArchive() error {
100+
if util.NilOrEmpty(res.Source) {
101+
// archive can be omitted iff the contents are omitted
102+
return nil
103+
}
104+
if util.NilOrEmpty(res.Archive) {
105+
return errors.ErrArchiveTypeRequired
106+
}
107+
switch *res.Archive {
108+
case "tar":
109+
return nil
110+
}
111+
return errors.ErrUnsupportedArchiveType
112+
}

config/v3_4_experimental/types/schema.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ package types
22

33
// 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
44

5+
type ArchiveResource struct {
6+
Resource
7+
ArchiveResourceEmbedded1
8+
}
9+
10+
type ArchiveResourceEmbedded1 struct {
11+
Archive *string `json:"archive,omitempty"`
12+
}
13+
514
type Clevis struct {
615
Custom ClevisCustom `json:"custom,omitempty"`
716
Tang []Tang `json:"tang,omitempty"`
@@ -31,7 +40,8 @@ type Directory struct {
3140
}
3241

3342
type DirectoryEmbedded1 struct {
34-
Mode *int `json:"mode,omitempty"`
43+
Contents ArchiveResource `json:"contents,omitempty"`
44+
Mode *int `json:"mode,omitempty"`
3545
}
3646

3747
type Disk struct {

docs/configuration-v3_4_experimental.md

+9
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ The Ignition configuration is a JSON document conforming to the following specif
111111
* **_group_** (object): specifies the directory's group.
112112
* **_id_** (integer): the group ID of the group.
113113
* **_name_** (string): the group name of the group.
114+
* **_contents_** (object): options related to the contents of the directory. If specified, `overwrite` must be `true`. Directories populated from an archive own all files under it. This means that specifying files, directories, and links under the path of this directory always result in a conflict error during config validation.
115+
* **archive** (string): format of the archive to extract into the directory. Must be `tar`. If `tar` is specified, the source must be a USTAR, PAX, or GNU tarball. Only regular files, directories, and links (both hard links and symlinks) are extracted, other file types are ignored and emit a warning. Note that for `tar` archives, sparse files are not supported and processing an archive with one will result in an error.
116+
* **_compression_** (string): the type of compression used on the archive (null or gzip). Compression cannot be used with S3.
117+
* **_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.
118+
* **_httpHeaders_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only.
119+
* **name** (string): the header name.
120+
* **_value_** (string): the header contents.
121+
* **_verification_** (object): options related to the verification of the archive file.
122+
* **_hash_** (string): the hash of the archive file, in the form `<type>-<value>` where type is either `sha512` or `sha256`.
114123
* **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`.
115124
* **path** (string): the absolute path to the link
116125
* **_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.

docs/release-notes.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ nav_order: 9
1212
### Features
1313

1414
- Ship aarch64 macOS ignition-validate binary in GitHub release artifacts
15+
- Add the ability to populate directory from tar archives.
1516

1617
### Changes
1718

internal/exec/stages/files/filesystemEntries.go

+29-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package files
1717
import (
1818
"encoding/json"
1919
"fmt"
20+
"io"
2021
"os"
2122
"os/exec"
2223
"path/filepath"
@@ -285,7 +286,7 @@ func (tmp fileEntry) create(l *log.Logger, u util.Util) error {
285286

286287
for _, op := range fetchOps {
287288
msg := "writing file %q"
288-
if op.Append {
289+
if op.Mode == util.FetchAppend {
289290
msg = "appending to file %q"
290291
}
291292
if err := l.LogOp(
@@ -323,6 +324,33 @@ func (tmp dirEntry) create(l *log.Logger, u util.Util) error {
323324
return fmt.Errorf("error creating directory %s: A non-directory already exists and overwrite is false", d.Path)
324325
}
325326

327+
if d.Contents.Archive != nil {
328+
dirf, err := os.Open(d.Path)
329+
if err != nil {
330+
return fmt.Errorf("open() failed on %s: %v", d.Path, err)
331+
}
332+
switch _, err := dirf.Readdirnames(1); {
333+
case err == nil:
334+
return fmt.Errorf("refusing to populate directory %s: directory is not empty and overwrite is false", d.Path)
335+
case err != io.EOF:
336+
return fmt.Errorf("readdirnames() failed on %s: %v", d.Path, err)
337+
}
338+
339+
fetch, err := util.MakeFetchOp(l, d.Node, d.Contents.Resource)
340+
if err != nil {
341+
return fmt.Errorf("failed to resolve directory %q: %v", d.Path, err)
342+
}
343+
fetch.Mode = util.FetchExtract
344+
fetch.ArchiveType = util.ArchiveType(*d.Contents.Archive)
345+
346+
op := func() error {
347+
return u.PerformFetch(fetch)
348+
}
349+
if err := l.LogOp(op, "populating directory %q", d.Path); err != nil {
350+
return fmt.Errorf("failed to populate directory %q: %v", d.Path, err)
351+
}
352+
}
353+
326354
if err := u.SetPermissions(d.Mode, d.Node); err != nil {
327355
return fmt.Errorf("error setting directory permissions for %s: %v", d.Path, err)
328356
}

0 commit comments

Comments
 (0)