Skip to content

Commit 246a234

Browse files
prestistyasminvalimjmarreroc4rt0
committed
base/v0_6_exp: add parent directory sugar
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. resolves: coreos#380 Co-authored-by: Yasmin Valim <[email protected]> Co-authored-by: Joseph Corchado <[email protected]> Co-authored-by: Adam Piasecki <[email protected]>
1 parent d26d803 commit 246a234

12 files changed

+289
-5
lines changed

base/v0_6_exp/schema.go

+6
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ type File struct {
6767
Append []Resource `yaml:"append"`
6868
Contents Resource `yaml:"contents"`
6969
Mode *int `yaml:"mode"`
70+
Parent Parent `yaml:"parent"`
71+
}
72+
73+
type Parent struct {
74+
Path *string `yaml:"path,omitempty"`
75+
Mode *int `yaml:"mode,omitempty"`
7076
}
7177

7278
type Filesystem struct {

base/v0_6_exp/translate.go

+71-5
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ func (c Config) ToIgn3_5Unvalidated(options common.TranslateOptions) (types.Conf
8383

8484
tr := translate.NewTranslator("yaml", "json", options)
8585
tr.AddCustomTranslator(translateIgnition)
86-
tr.AddCustomTranslator(translateFile)
87-
tr.AddCustomTranslator(translateDirectory)
88-
tr.AddCustomTranslator(translateLink)
86+
tr.AddCustomTranslator(translateStorage)
8987
tr.AddCustomTranslator(translateResource)
9088
tr.AddCustomTranslator(translatePasswdUser)
9189
tr.AddCustomTranslator(translateUnit)
@@ -99,7 +97,6 @@ func (c Config) ToIgn3_5Unvalidated(options common.TranslateOptions) (types.Conf
9997
translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd)
10098

10199
c.addMountUnits(&ret, &tm)
102-
103100
tm2, r2 := c.processTrees(&ret, options)
104101
tm.Merge(tm2)
105102
r.Merge(r2)
@@ -121,6 +118,59 @@ func translateIgnition(from Ignition, options common.TranslateOptions) (to types
121118
return
122119
}
123120

121+
func translateStorage(from Storage, options common.TranslateOptions) (to types.Storage, tm translate.TranslationSet, r report.Report) {
122+
tr := translate.NewTranslator("yaml", "json", options)
123+
tr.AddCustomTranslator(translateFile)
124+
tr.AddCustomTranslator(translateDirectory)
125+
tr.AddCustomTranslator(translateLink)
126+
tr.AddCustomTranslator(translateLuks)
127+
tm, r = translate.Prefixed(tr, "directories", &from.Directories, &to.Directories)
128+
translate.MergeP(tr, tm, &r, "disks", &from.Disks, &to.Disks)
129+
translate.MergeP(tr, tm, &r, "files", &from.Files, &to.Files)
130+
translate.MergeP(tr, tm, &r, "filesystems", &from.Filesystems, &to.Filesystems)
131+
translate.MergeP(tr, tm, &r, "links", &from.Links, &to.Links)
132+
translate.MergeP(tr, tm, &r, "luks", &from.Luks, &to.Luks)
133+
translate.MergeP(tr, tm, &r, "raid", &from.Raid, &to.Raid)
134+
for i, file := range from.Files {
135+
if util.NotEmpty(file.Parent.Path) {
136+
yamlPath := path.New("yaml", "files", i, "parent")
137+
138+
if !strings.Contains(file.Path, *file.Parent.Path) {
139+
r.AddOnError(yamlPath, common.ErrInvalidParent)
140+
continue
141+
}
142+
143+
dir := filepath.Dir(file.Path)
144+
// make sure to clean the path to avoid consistency issues
145+
dir = filepath.Clean(dir)
146+
for dir != "" {
147+
renderedDir := types.Directory{
148+
Node: types.Node{
149+
Path: dir,
150+
Group: types.NodeGroup{ID: file.Group.ID, Name: file.Group.Name},
151+
User: types.NodeUser{ID: file.User.ID, Name: file.User.Name},
152+
},
153+
DirectoryEmbedded1: types.DirectoryEmbedded1{
154+
Mode: file.Parent.Mode,
155+
},
156+
}
157+
to.Directories = append(to.Directories, renderedDir)
158+
nextDir, _ := filepath.Split(dir)
159+
// make sure to clean the path to avoid consistency issues
160+
nextDir = filepath.Clean(nextDir)
161+
if dir == *file.Parent.Path || nextDir == dir {
162+
// we have reached the parent directory or the end of the path
163+
break
164+
}
165+
dir = nextDir
166+
}
167+
tm.AddFromCommonSource(yamlPath, path.New("json", "directories"), to.Directories)
168+
}
169+
170+
}
171+
return
172+
}
173+
124174
func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) {
125175
tr := translate.NewTranslator("yaml", "json", options)
126176
tr.AddCustomTranslator(translateResource)
@@ -134,6 +184,22 @@ func translateFile(from File, options common.TranslateOptions) (to types.File, t
134184
return
135185
}
136186

187+
func translateLuks(from Luks, options common.TranslateOptions) (to types.Luks, tm translate.TranslationSet, r report.Report) {
188+
tr := translate.NewTranslator("yaml", "json", options)
189+
tr.AddCustomTranslator(translateResource)
190+
tm, r = translate.Prefixed(tr, "clevis", &from.Clevis, &to.Clevis)
191+
translate.MergeP(tr, tm, &r, "device", &from.Device, &to.Device)
192+
translate.MergeP(tr, tm, &r, "discard", &from.Discard, &to.Discard)
193+
translate.MergeP2(tr, tm, &r, "key_file", &from.KeyFile, "keyFile", &to.KeyFile)
194+
translate.MergeP(tr, tm, &r, "label", &from.Label, &to.Label)
195+
translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name)
196+
translate.MergeP2(tr, tm, &r, "open_options", &from.OpenOptions, "openOptions", &to.OpenOptions)
197+
translate.MergeP(tr, tm, &r, "options", &from.Options, &to.Options)
198+
translate.MergeP(tr, tm, &r, "uuid", &from.UUID, &to.UUID)
199+
translate.MergeP2(tr, tm, &r, "wipe_volume", &from.WipeVolume, "wipeVolume", &to.WipeVolume)
200+
return
201+
}
202+
137203
func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) {
138204
tr := translate.NewTranslator("yaml", "json", options)
139205
tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification)
@@ -294,7 +360,7 @@ func (c Config) processTrees(ret *types.Config, options common.TranslateOptions)
294360
return ts, r
295361
}
296362
t := newNodeTracker(ret)
297-
363+
ts.AddTranslation(path.New("yaml", "storage"), path.New("json", "storage"))
298364
for i, tree := range c.Storage.Trees {
299365
yamlPath := path.New("yaml", "storage", "trees", i)
300366
if options.FilesDir == "" {

base/v0_6_exp/translate_test.go

+168
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,174 @@ func TestTranslateFile(t *testing.T) {
599599
})
600600
}
601601
}
602+
func TestTranslateStorage(t *testing.T) {
603+
if runtime.GOOS == "windows" {
604+
t.Skip("skipping test")
605+
}
606+
607+
tests := []struct {
608+
in Storage
609+
out types.Storage
610+
errPath path.ContextPath
611+
errors error
612+
}{
613+
// Basic parent file directory
614+
{
615+
Storage{
616+
Files: []File{
617+
{
618+
Path: "/foo/bar/txt.txt",
619+
Contents: Resource{},
620+
Mode: util.IntToPtr(420),
621+
Parent: Parent{
622+
Path: util.StrToPtr("/foo"),
623+
Mode: util.IntToPtr(420),
624+
},
625+
},
626+
},
627+
},
628+
types.Storage{
629+
Files: []types.File{
630+
{
631+
Node: types.Node{
632+
Path: "/foo/bar/txt.txt",
633+
},
634+
FileEmbedded1: types.FileEmbedded1{
635+
Mode: util.IntToPtr(420),
636+
Contents: types.Resource{},
637+
},
638+
},
639+
},
640+
Directories: []types.Directory{
641+
{
642+
Node: types.Node{
643+
Path: "/foo/bar",
644+
},
645+
DirectoryEmbedded1: types.DirectoryEmbedded1{
646+
Mode: util.IntToPtr(420),
647+
},
648+
},
649+
{
650+
Node: types.Node{
651+
Path: "/foo",
652+
},
653+
DirectoryEmbedded1: types.DirectoryEmbedded1{
654+
Mode: util.IntToPtr(420),
655+
},
656+
},
657+
},
658+
},
659+
path.ContextPath{},
660+
nil,
661+
},
662+
// Empty parent file directory
663+
{
664+
Storage{
665+
Files: []File{
666+
{
667+
Path: "/foo/bar/txt.txt",
668+
Contents: Resource{},
669+
Mode: util.IntToPtr(420),
670+
Parent: Parent{
671+
Path: util.StrToPtr(""),
672+
Mode: util.IntToPtr(420),
673+
},
674+
},
675+
},
676+
},
677+
types.Storage{
678+
Files: []types.File{
679+
{
680+
Node: types.Node{
681+
Path: "/foo/bar/txt.txt",
682+
},
683+
FileEmbedded1: types.FileEmbedded1{
684+
Mode: util.IntToPtr(420),
685+
Contents: types.Resource{},
686+
},
687+
},
688+
},
689+
},
690+
path.ContextPath{},
691+
nil,
692+
},
693+
// Parent not defined
694+
{
695+
Storage{
696+
Files: []File{
697+
{
698+
Path: "/foo/bar/txt.txt",
699+
Contents: Resource{},
700+
Mode: util.IntToPtr(420),
701+
},
702+
},
703+
},
704+
types.Storage{
705+
Files: []types.File{
706+
{
707+
Node: types.Node{
708+
Path: "/foo/bar/txt.txt",
709+
},
710+
FileEmbedded1: types.FileEmbedded1{
711+
Mode: util.IntToPtr(420),
712+
Contents: types.Resource{},
713+
},
714+
},
715+
},
716+
},
717+
path.ContextPath{},
718+
nil,
719+
},
720+
// Parent path is not related to file path
721+
{
722+
Storage{
723+
Files: []File{
724+
{
725+
Path: "/foo/bar/txt.txt",
726+
Contents: Resource{},
727+
Mode: util.IntToPtr(420),
728+
Parent: Parent{
729+
Path: util.StrToPtr("/godzilla"),
730+
Mode: util.IntToPtr(420),
731+
},
732+
},
733+
},
734+
},
735+
types.Storage{
736+
Files: []types.File{
737+
{
738+
Node: types.Node{
739+
Path: "/foo/bar/txt.txt",
740+
},
741+
FileEmbedded1: types.FileEmbedded1{
742+
Mode: util.IntToPtr(420),
743+
Contents: types.Resource{},
744+
},
745+
},
746+
},
747+
},
748+
path.New("yaml", "files", 0, "parent"),
749+
common.ErrInvalidParent,
750+
},
751+
}
752+
753+
for i, test := range tests {
754+
t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) {
755+
actual, translations, r := translateStorage(test.in, common.TranslateOptions{})
756+
r = confutil.TranslateReportPaths(r, translations)
757+
baseutil.VerifyReport(t, test.in, r)
758+
assert.Equal(t, test.out, actual, "translation mismatch")
759+
assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage")
760+
if test.errors != nil {
761+
expected := report.Report{}
762+
expected.AddOnError(test.errPath, test.errors)
763+
assert.Equal(t, expected, r, "bad report for test case %d", i)
764+
} else {
765+
assert.Equal(t, report.Report{}, r, "non-empty report")
766+
}
767+
})
768+
}
769+
}
602770

603771
// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires.
604772
func TestTranslateDirectory(t *testing.T) {

config/common/errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ var (
8989
ErrLinkSupport = errors.New("links are not supported in this spec version")
9090
ErrLuksSupport = errors.New("luks is not supported in this spec version")
9191
ErrRaidSupport = errors.New("raid is not supported in this spec version")
92+
ErrInvalidParent = errors.New("parent must be included in the file path")
9293

9394
// Grub
9495
ErrGrubUserNameNotSpecified = errors.New("field \"name\" is required")

docs/config-fcos-v1_6-exp.md

+3
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s
117117
* **_group_** (object): specifies the file's group.
118118
* **_id_** (integer): the group ID of the group.
119119
* **_name_** (string): the group name of the group.
120+
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination.
121+
* **_path_** (string): the path of the directory within the file's 'path'.
122+
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
120123
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
121124
* **path** (string): the absolute path to the directory.
122125
* **_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.

docs/config-fiot-v1_1-exp.md

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ The Fedora IoT configuration is a YAML document conforming to the following spec
8888
* **_group_** (object): specifies the file's group.
8989
* **_id_** (integer): the group ID of the group.
9090
* **_name_** (string): the group name of the group.
91+
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination.
92+
* **_path_** (string): the path of the directory within the file's 'path'.
93+
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
9194
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
9295
* **path** (string): the absolute path to the directory.
9396
* **_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.

docs/config-flatcar-v1_2-exp.md

+3
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ The Flatcar configuration is a YAML document conforming to the following specifi
117117
* **_group_** (object): specifies the file's group.
118118
* **_id_** (integer): the group ID of the group.
119119
* **_name_** (string): the group name of the group.
120+
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination.
121+
* **_path_** (string): the path of the directory within the file's 'path'.
122+
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
120123
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
121124
* **path** (string): the absolute path to the directory.
122125
* **_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.

docs/config-openshift-v4_17-exp.md

+3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ The OpenShift configuration is a YAML document conforming to the following speci
107107
* **_group_** (object): specifies the file's group.
108108
* **_id_** (integer): the group ID of the group.
109109
* **_name_** (string): the group name of the group.
110+
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination.
111+
* **_path_** (string): the path of the directory within the file's 'path'.
112+
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
110113
* **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`.
111114
* **name** (string): the name of the luks device.
112115
* **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks.

docs/config-r4e-v1_2-exp.md

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ The RHEL for Edge configuration is a YAML document conforming to the following s
8888
* **_group_** (object): specifies the file's group.
8989
* **_id_** (integer): the group ID of the group.
9090
* **_name_** (string): the group name of the group.
91+
* **_parent_** (object): the parent directory for the specified file, by declaring a parent the directories from the parent to the file's target destination.
92+
* **_path_** (string): the path of the directory within the file's 'path'.
93+
* **_mode_** (integer): directory modes are set to 0755 as a default if not specified and directory does not exist prior to the specified file.
9194
* **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`.
9295
* **path** (string): the absolute path to the directory.
9396
* **_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.

docs/examples.md

+17
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,23 @@ storage:
149149
mode: 0644
150150
```
151151

152+
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.
153+
154+
<!-- butane-config -->
155+
```yaml
156+
variant: fcos
157+
version: 1.6.0-experimental
158+
storage:
159+
files:
160+
- path: /opt/foo/bar/file.txt
161+
contents:
162+
inline: Hello, world!
163+
mode: 0644
164+
parent:
165+
path: /opt/foo
166+
mode: 0644
167+
```
168+
152169
### Directory trees
153170

154171
Consider a directory tree at `~/conf/tree` on the system running Butane:

docs/release-notes.md

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ key](https://getfedora.org/security/).
4747
- Stabilize OpenShift spec 4.15.0, targeting Ignition spec 3.4.0
4848
- Add OpenShift spec 4.16.0-experimental, targeting Ignition spec
4949
3.5.0-experimental
50+
- Support s390x layouts in `boot_device` section (fcos 1.6.0-exp, openshift 4.15.0-exp)
51+
- Add `parent` field to `files`, to reduce verbosity when configuring a deeply
52+
nested file. _(base 0.6.0-exp)_
5053

5154
### Bug fixes
5255

0 commit comments

Comments
 (0)