Skip to content

Commit

Permalink
base/v0_6_exp: add parent directory sugar
Browse files Browse the repository at this point in the history
Add a field called 'Parent' which is used to specify a file's parent
directory. When a parent is specified, all directories from the parent to
the file will be created, with the 'mode' supplied in the parent directory.

Co-authored-by: Yasmin Valim  <[email protected]>
Co-authored-by: Joseph Corchado <[email protected]>
Co-authored-by: Adam Piasecki <[email protected]>
  • Loading branch information
4 people committed Feb 20, 2024
1 parent 4f1b414 commit eb5115d
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 5 deletions.
6 changes: 6 additions & 0 deletions base/v0_6_exp/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ type File struct {
Append []Resource `yaml:"append"`
Contents Resource `yaml:"contents"`
Mode *int `yaml:"mode"`
Parent Parent `yaml:"parent"`
}

type Parent struct {
Path *string `yaml:"path"`
Mode *int `yaml:"mode"`
}

type Filesystem struct {
Expand Down
76 changes: 71 additions & 5 deletions base/v0_6_exp/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ func (c Config) ToIgn3_5Unvalidated(options common.TranslateOptions) (types.Conf

tr := translate.NewTranslator("yaml", "json", options)
tr.AddCustomTranslator(translateIgnition)
tr.AddCustomTranslator(translateFile)
tr.AddCustomTranslator(translateDirectory)
tr.AddCustomTranslator(translateLink)
tr.AddCustomTranslator(translateStorage)
tr.AddCustomTranslator(translateResource)
tr.AddCustomTranslator(translatePasswdUser)
tr.AddCustomTranslator(translateUnit)
Expand All @@ -99,7 +97,6 @@ func (c Config) ToIgn3_5Unvalidated(options common.TranslateOptions) (types.Conf
translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd)

c.addMountUnits(&ret, &tm)

tm2, r2 := c.processTrees(&ret, options)
tm.Merge(tm2)
r.Merge(r2)
Expand All @@ -121,6 +118,59 @@ func translateIgnition(from Ignition, options common.TranslateOptions) (to types
return
}

func translateStorage(from Storage, options common.TranslateOptions) (to types.Storage, tm translate.TranslationSet, r report.Report) {
tr := translate.NewTranslator("yaml", "json", options)
tr.AddCustomTranslator(translateFile)
tr.AddCustomTranslator(translateDirectory)
tr.AddCustomTranslator(translateLink)
tr.AddCustomTranslator(translateLuks)
tm, r = translate.Prefixed(tr, "directories", &from.Directories, &to.Directories)
translate.MergeP(tr, tm, &r, "disks", &from.Disks, &to.Disks)
translate.MergeP(tr, tm, &r, "files", &from.Files, &to.Files)
translate.MergeP(tr, tm, &r, "filesystems", &from.Filesystems, &to.Filesystems)
translate.MergeP(tr, tm, &r, "links", &from.Links, &to.Links)
translate.MergeP(tr, tm, &r, "luks", &from.Luks, &to.Luks)
translate.MergeP(tr, tm, &r, "raid", &from.Raid, &to.Raid)
for i, file := range from.Files {
if util.NotEmpty(file.Parent.Path) {
yamlPath := path.New("yaml", "files", i, "parent")

if !strings.Contains(file.Path, *file.Parent.Path) {
r.AddOnError(yamlPath, common.ErrInvalidParent)
continue
}

dir := filepath.Dir(file.Path)
// make sure to clean the path to avoid consistency issues
dir = filepath.Clean(dir)
for dir != "" {
renderedDir := types.Directory{
Node: types.Node{
Path: dir,
Group: types.NodeGroup{ID: file.Group.ID, Name: file.Group.Name},
User: types.NodeUser{ID: file.User.ID, Name: file.User.Name},
},
DirectoryEmbedded1: types.DirectoryEmbedded1{
Mode: file.Parent.Mode,
},
}
to.Directories = append(to.Directories, renderedDir)
nextDir, _ := filepath.Split(dir)
// make sure to clean the path to avoid consistency issues
nextDir = filepath.Clean(nextDir)
if dir == *file.Parent.Path || nextDir == dir {
// we have reached the parent directory or the end of the path
break
}
dir = nextDir
}
tm.AddFromCommonSource(yamlPath, path.New("json", "directories"), to.Directories)
}

}
return
}

func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) {
tr := translate.NewTranslator("yaml", "json", options)
tr.AddCustomTranslator(translateResource)
Expand All @@ -134,6 +184,22 @@ func translateFile(from File, options common.TranslateOptions) (to types.File, t
return
}

func translateLuks(from Luks, options common.TranslateOptions) (to types.Luks, tm translate.TranslationSet, r report.Report) {
tr := translate.NewTranslator("yaml", "json", options)
tr.AddCustomTranslator(translateResource)
tm, r = translate.Prefixed(tr, "clevis", &from.Clevis, &to.Clevis)
translate.MergeP(tr, tm, &r, "device", &from.Device, &to.Device)
translate.MergeP(tr, tm, &r, "discard", &from.Discard, &to.Discard)
translate.MergeP2(tr, tm, &r, "key_file", &from.KeyFile, "keyFile", &to.KeyFile)
translate.MergeP(tr, tm, &r, "label", &from.Label, &to.Label)
translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name)
translate.MergeP2(tr, tm, &r, "open_options", &from.OpenOptions, "openOptions", &to.OpenOptions)
translate.MergeP(tr, tm, &r, "options", &from.Options, &to.Options)
translate.MergeP(tr, tm, &r, "uuid", &from.UUID, &to.UUID)
translate.MergeP2(tr, tm, &r, "wipe_volume", &from.WipeVolume, "wipeVolume", &to.WipeVolume)
return
}

func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) {
tr := translate.NewTranslator("yaml", "json", options)
tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification)
Expand Down Expand Up @@ -294,7 +360,7 @@ func (c Config) processTrees(ret *types.Config, options common.TranslateOptions)
return ts, r
}
t := newNodeTracker(ret)

ts.AddTranslation(path.New("yaml", "storage"), path.New("json", "storage"))
for i, tree := range c.Storage.Trees {
yamlPath := path.New("yaml", "storage", "trees", i)
if options.FilesDir == "" {
Expand Down
165 changes: 165 additions & 0 deletions base/v0_6_exp/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,171 @@ func TestTranslateFile(t *testing.T) {
})
}
}
func TestTranslateStorage(t *testing.T) {
tests := []struct {
in Storage
out types.Storage
errPath path.ContextPath
errors error
}{
// Basic parent file directory
{
Storage{
Files: []File{
{
Path: "/foo/bar/txt.txt",
Contents: Resource{},
Mode: util.IntToPtr(420),
Parent: Parent{
Path: util.StrToPtr("/foo"),
Mode: util.IntToPtr(420),
},
},
},
},
types.Storage{
Files: []types.File{
{
Node: types.Node{
Path: "/foo/bar/txt.txt",
},
FileEmbedded1: types.FileEmbedded1{
Mode: util.IntToPtr(420),
Contents: types.Resource{},
},
},
},
Directories: []types.Directory{
{
Node: types.Node{
Path: "/foo/bar",
},
DirectoryEmbedded1: types.DirectoryEmbedded1{
Mode: util.IntToPtr(420),
},
},
{
Node: types.Node{
Path: "/foo",
},
DirectoryEmbedded1: types.DirectoryEmbedded1{
Mode: util.IntToPtr(420),
},
},
},
},
path.ContextPath{},
nil,
},
// Empty parent file directory
{
Storage{
Files: []File{
{
Path: "/foo/bar/txt.txt",
Contents: Resource{},
Mode: util.IntToPtr(420),
Parent: Parent{
Path: util.StrToPtr(""),
Mode: util.IntToPtr(420),
},
},
},
},
types.Storage{
Files: []types.File{
{
Node: types.Node{
Path: "/foo/bar/txt.txt",
},
FileEmbedded1: types.FileEmbedded1{
Mode: util.IntToPtr(420),
Contents: types.Resource{},
},
},
},
},
path.ContextPath{},
nil,
},
// Parent not defined
{
Storage{
Files: []File{
{
Path: "/foo/bar/txt.txt",
Contents: Resource{},
Mode: util.IntToPtr(420),
},
},
},
types.Storage{
Files: []types.File{
{
Node: types.Node{
Path: "/foo/bar/txt.txt",
},
FileEmbedded1: types.FileEmbedded1{
Mode: util.IntToPtr(420),
Contents: types.Resource{},
},
},
},
},
path.ContextPath{},
nil,
},

// Parent path is not related to file path
{
Storage{
Files: []File{
{
Path: "/foo/bar/txt.txt",
Contents: Resource{},
Mode: util.IntToPtr(420),
Parent: Parent{
Path: util.StrToPtr("/godzilla"),
Mode: util.IntToPtr(420),
},
},
},
},
types.Storage{
Files: []types.File{
{
Node: types.Node{
Path: "/foo/bar/txt.txt",
},
FileEmbedded1: types.FileEmbedded1{
Mode: util.IntToPtr(420),
Contents: types.Resource{},
},
},
},
},
path.New("yaml", "files", 0, "parent"),
common.ErrInvalidParent,
},
}

for i, test := range tests {
t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) {
actual, translations, r := translateStorage(test.in, common.TranslateOptions{})
r = confutil.TranslateReportPaths(r, translations)
baseutil.VerifyReport(t, test.in, r)
assert.Equal(t, test.out, actual, "translation mismatch")
assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage")
if test.errors != nil {
expected := report.Report{}
expected.AddOnError(test.errPath, test.errors)
assert.Equal(t, expected, r, "bad report for test case %d", i)
} else {
assert.Equal(t, report.Report{}, r, "non-empty report")
}
})
}
}

// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires.
func TestTranslateDirectory(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions config/common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ var (
ErrLinkSupport = errors.New("links are not supported in this spec version")
ErrLuksSupport = errors.New("luks is not supported in this spec version")
ErrRaidSupport = errors.New("raid is not supported in this spec version")
ErrInvalidParent = errors.New("parent must be included in the file path")

// Grub
ErrGrubUserNameNotSpecified = errors.New("field \"name\" is required")
Expand Down
3 changes: 3 additions & 0 deletions docs/config-fcos-v1_6-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s
* **_group_** (object): specifies the file's group.
* **_id_** (integer): the group ID of the group.
* **_name_** (string): the group name of the group.
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination with the permissions given.
* **_path_** (string): the path of the directory within the file's 'path'.
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
* **path** (string): the absolute path to the directory.
* **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false.
Expand Down
3 changes: 3 additions & 0 deletions docs/config-fiot-v1_1-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ The Fedora IoT configuration is a YAML document conforming to the following spec
* **_group_** (object): specifies the file's group.
* **_id_** (integer): the group ID of the group.
* **_name_** (string): the group name of the group.
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination with the permissions given.
* **_path_** (string): the path of the directory within the file's 'path'.
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
* **path** (string): the absolute path to the directory.
* **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false.
Expand Down
3 changes: 3 additions & 0 deletions docs/config-flatcar-v1_2-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ The Flatcar configuration is a YAML document conforming to the following specifi
* **_group_** (object): specifies the file's group.
* **_id_** (integer): the group ID of the group.
* **_name_** (string): the group name of the group.
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination with the permissions given.
* **_path_** (string): the path of the directory within the file's 'path'.
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
* **path** (string): the absolute path to the directory.
* **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false.
Expand Down
3 changes: 3 additions & 0 deletions docs/config-openshift-v4_15-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ The OpenShift configuration is a YAML document conforming to the following speci
* **_group_** (object): specifies the file's group.
* **_id_** (integer): the group ID of the group.
* **_name_** (string): the group name of the group.
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination with the permissions given.
* **_path_** (string): the path of the directory within the file's 'path'.
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
* **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`.
* **name** (string): the name of the luks device.
* **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks.
Expand Down
3 changes: 3 additions & 0 deletions docs/config-r4e-v1_2-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ The RHEL for Edge configuration is a YAML document conforming to the following s
* **_group_** (object): specifies the file's group.
* **_id_** (integer): the group ID of the group.
* **_name_** (string): the group name of the group.
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination with the permissions given.
* **_path_** (string): the path of the directory within the file's 'path'.
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
* **path** (string): the absolute path to the directory.
* **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false.
Expand Down
17 changes: 17 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,23 @@ storage:
mode: 0644
```

This example creates a file at `/opt/foo/bar/file.txt` with the contents `Hello, world!`, permissions 0644 (so readable and writable by the owner, and only readable by everyone else). Additionally is specifies a parent directory at `/opt/foo` with permissions 0644, which ensure that if the directories between `/opt/foo` and `/opt/foo/bar/file.txt` (i.e `/opt/foo`, `/opt/foo/bar`) do not exist, they will be created with the specified permissions. Use this to avoid having to create directories separately, with the correct permissions.

<!-- butane-config -->
```yaml
variant: fcos
version: 1.1.0
storage:
files:
- path: /opt/foo/bar/file.txt
contents:
inline: Hello, world!
mode: 0644
parent:
- path: /opt/foo
- mode: 0644
```

### Directory trees

Consider a directory tree at `~/conf/tree` on the system running Butane:
Expand Down
2 changes: 2 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ nav_order: 9
### Features

- Support s390x layouts in `boot_device` section (fcos 1.6.0-exp, openshift 4.15.0-exp)
- Add `parent` field to `files`, to reduce verbosity when configuring a deeply
nested file. _(base 0.6.0-exp)_

### Bug fixes

Expand Down
Loading

0 comments on commit eb5115d

Please sign in to comment.