From 61384e8323daea88bf7fc18d5a38c5bf393dc501 Mon Sep 17 00:00:00 2001 From: vic1707 <28602203+vic1707@users.noreply.github.com> Date: Sun, 16 Nov 2025 14:14:19 +0100 Subject: [PATCH] base/v0_7_exp: add ownership and mode support for trees Add support for setting user, group, file_mode, and dir_mode on trees to address the use case of deploying directory trees with specific ownership for rootless containers. Fixes: coreos/butane#544 --- base/v0_7_exp/schema.go | 8 +- base/v0_7_exp/translate.go | 77 +++++++++++--- base/v0_7_exp/translate_test.go | 157 ++++++++++++++++++++++++++++- docs/config-fcos-v1_7-exp.md | 10 +- docs/config-fiot-v1_1-exp.md | 10 +- docs/config-flatcar-v1_2-exp.md | 10 +- docs/config-openshift-v4_21-exp.md | 10 +- docs/config-r4e-v1_2-exp.md | 10 +- docs/release-notes.md | 2 + internal/doc/butane.yaml | 49 ++++++++- 10 files changed, 321 insertions(+), 22 deletions(-) diff --git a/base/v0_7_exp/schema.go b/base/v0_7_exp/schema.go index ecdeff1e..4f4e05ed 100644 --- a/base/v0_7_exp/schema.go +++ b/base/v0_7_exp/schema.go @@ -249,8 +249,12 @@ type Timeouts struct { } type Tree struct { - Local string `yaml:"local"` - Path *string `yaml:"path"` + Group NodeGroup `yaml:"group"` + Local string `yaml:"local"` + Path *string `yaml:"path"` + User NodeUser `yaml:"user"` + FileMode *int `yaml:"file_mode"` + DirMode *int `yaml:"dir_mode"` } type Unit struct { diff --git a/base/v0_7_exp/translate.go b/base/v0_7_exp/translate.go index fd95720d..20882675 100644 --- a/base/v0_7_exp/translate.go +++ b/base/v0_7_exp/translate.go @@ -333,29 +333,69 @@ func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) destBaseDir = *tree.Path } - walkTree(yamlPath, &ts, &r, t, srcBaseDir, destBaseDir, options) + walkTree(yamlPath, &ts, &r, t, treeWalkOptions{ + srcBaseDir: srcBaseDir, + destBaseDir: destBaseDir, + TranslateOptions: options, + user: tree.User, + group: tree.Group, + fileMode: tree.FileMode, + dirMode: tree.DirMode, + }) } return ts, r } -func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, srcBaseDir, destBaseDir string, options common.TranslateOptions) { +type treeWalkOptions struct { + srcBaseDir string + destBaseDir string + common.TranslateOptions + user NodeUser + group NodeGroup + fileMode *int + dirMode *int +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, options treeWalkOptions) { // The strategy for errors within WalkFunc is to add an error to // the report and return nil, so walking continues but translation // will fail afterward. - err := filepath.Walk(srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + err := filepath.Walk(options.srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { if err != nil { r.AddOnError(yamlPath, err) return nil } - relPath, err := filepath.Rel(srcBaseDir, srcPath) + relPath, err := filepath.Rel(options.srcBaseDir, srcPath) if err != nil { r.AddOnError(yamlPath, err) return nil } - destPath := slashpath.Join(destBaseDir, filepath.ToSlash(relPath)) + destPath := slashpath.Join(options.destBaseDir, filepath.ToSlash(relPath)) if info.Mode().IsDir() { - return nil + // If nothing custom is required we skip directories generation + if options.dirMode == nil && options.user == (NodeUser{}) && options.group == (NodeGroup{}) { + return nil + } + + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + mode := util.IntToPtr(0755) + if options.dirMode != nil { + mode = options.dirMode + } + i, dir := t.AddDir(types.Directory{ + Node: createNode(destPath, options.user, options.group), + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: mode, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "directories", i), dir) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "directories")) + } } else if info.Mode().IsRegular() { i, file := t.GetFile(destPath) if file != nil { @@ -369,9 +409,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report return nil } i, file = t.AddFile(types.File{ - Node: types.Node{ - Path: destPath, - }, + Node: createNode(destPath, options.user, options.group), }) ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) if i == 0 { @@ -400,6 +438,9 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report if info.Mode()&0111 != 0 { mode = 0755 } + if options.fileMode != nil { + mode = *options.fileMode + } file.Mode = &mode ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) } @@ -416,9 +457,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report return nil } i, link = t.AddLink(types.Link{ - Node: types.Node{ - Path: destPath, - }, + Node: createNode(destPath, options.user, options.group), }) ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) if i == 0 { @@ -441,6 +480,20 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report r.AddOnError(yamlPath, err) } +func createNode(destPath string, user NodeUser, group NodeGroup) types.Node { + return types.Node{ + Path: destPath, + User: types.NodeUser{ + ID: user.ID, + Name: user.Name, + }, + Group: types.NodeGroup{ + ID: group.ID, + Name: group.Name, + }, + } +} + func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { if len(c.Storage.Filesystems) == 0 { return diff --git a/base/v0_7_exp/translate_test.go b/base/v0_7_exp/translate_test.go index 0433fad3..05213632 100644 --- a/base/v0_7_exp/translate_test.go +++ b/base/v0_7_exp/translate_test.go @@ -1212,6 +1212,7 @@ func TestTranslateTree(t *testing.T) { inDirs []Directory inLinks []Link outFiles []types.File + outDirs []types.Directory outLinks []types.Link report string skip func(t *testing.T) @@ -1643,6 +1644,160 @@ func TestTranslateTree(t *testing.T) { report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", }, + // Permissions and ownership + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + FileMode: util.IntToPtr(0777), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + { + Local: "tree2", + DirMode: util.IntToPtr(0777), + Path: util.StrToPtr("/etc"), + }, + }, + outDirs: []types.Directory{ + { + Node: types.Node{ + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + Path: "/", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0755), + }, + }, + { + Node: types.Node{ + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + Path: "/subdir", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0755), + }, + }, + { + Node: types.Node{ + Path: "/etc", + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0777), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0777), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0777), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // Overwrite via tree ownership fails + { + dirFiles: map[string]os.FileMode{ + "tree/etc/file": 0600, + }, + inDirs: []Directory{ + {Path: "/etc"}, + }, + inTrees: []Tree{ + { + Local: "tree", + FileMode: util.IntToPtr(0777), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n", + }, } for i, test := range tests { @@ -1728,7 +1883,7 @@ func TestTranslateTree(t *testing.T) { assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") - assert.Equal(t, []types.Directory(nil), actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outDirs, actual.Storage.Directories, "directories mismatch") assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") }) } diff --git a/docs/config-fcos-v1_7-exp.md b/docs/config-fcos-v1_7-exp.md index de740ada..e5e45aff 100644 --- a/docs/config-fcos-v1_7-exp.md +++ b/docs/config-fcos-v1_7-exp.md @@ -170,9 +170,17 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s * **_needs_network_** (boolean): whether or not the device requires networking. * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. - * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid * **_systemd_** (object): describes the desired state of the systemd units. * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). diff --git a/docs/config-fiot-v1_1-exp.md b/docs/config-fiot-v1_1-exp.md index 3e8c33fb..32118e51 100644 --- a/docs/config-fiot-v1_1-exp.md +++ b/docs/config-fiot-v1_1-exp.md @@ -109,9 +109,17 @@ The Fedora IoT configuration is a YAML document conforming to the following spec * **_name_** (string): the group name of the group. * **target** (string): the target path of the link * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. - * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid * **_systemd_** (object): describes the desired state of the systemd units. * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). diff --git a/docs/config-flatcar-v1_2-exp.md b/docs/config-flatcar-v1_2-exp.md index 806556b4..c46b0558 100644 --- a/docs/config-flatcar-v1_2-exp.md +++ b/docs/config-flatcar-v1_2-exp.md @@ -168,9 +168,17 @@ The Flatcar configuration is a YAML document conforming to the following specifi * **pin** (string): the clevis pin. * **config** (string): the clevis configuration JSON. * **_needs_network_** (boolean): whether or not the device requires networking. - * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid * **_systemd_** (object): describes the desired state of the systemd units. * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). diff --git a/docs/config-openshift-v4_21-exp.md b/docs/config-openshift-v4_21-exp.md index 91439e8d..80667622 100644 --- a/docs/config-openshift-v4_21-exp.md +++ b/docs/config-openshift-v4_21-exp.md @@ -139,9 +139,17 @@ The OpenShift configuration is a YAML document conforming to the following speci * **_needs_network_** (boolean): whether or not the device requires networking. * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. - * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid * **_systemd_** (object): describes the desired state of the systemd units. * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). diff --git a/docs/config-r4e-v1_2-exp.md b/docs/config-r4e-v1_2-exp.md index 9f309af0..0d4b15ed 100644 --- a/docs/config-r4e-v1_2-exp.md +++ b/docs/config-r4e-v1_2-exp.md @@ -109,9 +109,17 @@ The RHEL for Edge configuration is a YAML document conforming to the following s * **_name_** (string): the group name of the group. * **target** (string): the target path of the link * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. - * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid * **_systemd_** (object): describes the desired state of the systemd units. * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). diff --git a/docs/release-notes.md b/docs/release-notes.md index de943831..681e36c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,8 @@ nav_order: 9 ### Features +- Add support for mode and ownership settings for trees. + ### Bug fixes - Warn for `boot_device.layout` to be specified when using `boot_device.mirror` _(fcos 1.3.0-1.6.0)_ diff --git a/internal/doc/butane.yaml b/internal/doc/butane.yaml index 4ef84128..cbdcf199 100644 --- a/internal/doc/butane.yaml +++ b/internal/doc/butane.yaml @@ -256,9 +256,9 @@ root: use: mode - name: trees after: $ - desc: a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + desc: a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. transforms: - - regex: Ownership is not preserved. + - regex: Ownership, replacement: Symlinks must not be present. $0 if: - variant: openshift @@ -274,11 +274,56 @@ root: replacement: $1. if: - variant: openshift + - regex: ", file modes \\(using `file_mode`\\) and directories modes \\(using `dir_mode`\\) can be specified for the tree. If not specified, ownership is not preserved and f" + replacement: " is not preserved. F" + if: + - variant: fcos + max: 1.6.0 + - variant: fiot + max: 1.0.0 + - variant: flatcar + max: 1.1.0 + - variant: openshift + max: 4.20.0 + - variant: r4e + max: 1.1.0 children: - name: local desc: the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. - name: path desc: the path of the tree within the target system. Defaults to `/`. + - name: file_mode + desc: Custom permissions to apply to files + - name: dir_mode + desc: Custom permissions to apply to directories + transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: fcos + max: 1.6.0 + - variant: fiot + max: 1.0.0 + - variant: flatcar + max: 1.1.0 + - variant: openshift + max: 4.20.0 + - variant: r4e + max: 1.1.0 + - name: user + desc: User owner of the tree + children: + - name: name + desc: username + - name: id + desc: uid + - name: group + desc: Group owner of the tree + children: + - name: name + desc: group name + - name: id + desc: gid - name: systemd children: - name: units