Skip to content

Commit 7393034

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 e1edd80 commit 7393034

File tree

11 files changed

+687
-9
lines changed

11 files changed

+687
-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

+6
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,18 @@
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
}

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`.
115+
* **archive** (string): format of the archive to extract into the directory. must be `tar`.
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.

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)