blueprint: add representation of Directories and Files customization

Extend the Blueprint customizations with the representation for custom
Directories and Files specified by the user.

Implement custom Unmarshalers for TOML and JSON. These ensure that all
user-provided values are validated before use and also handle the fact
that user and group ownership for directories and files can be
specifies as a string or as an integer.

Implement helper functions for converting the Blueprint-specific types
for these customizations to their internal representation from `fsnode`
package.

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
This commit is contained in:
Tomáš Hozza 2023-01-31 21:53:24 +01:00 committed by Sanne Raymaekers
parent 26e6983320
commit c1991b3d51
3 changed files with 1098 additions and 0 deletions

View file

@ -0,0 +1,797 @@
package blueprint
import (
"encoding/json"
"os"
"testing"
"github.com/BurntSushi/toml"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/fsnode"
"github.com/stretchr/testify/assert"
)
func TestDirectoryCustomizationToFsNodeDirectory(t *testing.T) {
ensureDirCreation := func(dir *fsnode.Directory, err error) *fsnode.Directory {
t.Helper()
assert.NoError(t, err)
assert.NotNil(t, dir)
return dir
}
testCases := []struct {
Name string
Dir DirectoryCustomization
WantDir *fsnode.Directory
Error bool
}{
{
Name: "empty",
Dir: DirectoryCustomization{},
Error: true,
},
{
Name: "path-only",
Dir: DirectoryCustomization{
Path: "/etc/dir",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, nil, nil, false)),
},
{
Name: "path-invalid",
Dir: DirectoryCustomization{
Path: "etc/dir",
},
Error: true,
},
{
Name: "path-and-mode",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Mode: "0700",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", common.ToPtr(os.FileMode(0700)), nil, nil, false)),
},
{
Name: "path-and-mode-no-leading-zero",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Mode: "700",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", common.ToPtr(os.FileMode(0700)), nil, nil, false)),
},
{
Name: "path-and-mode-invalid",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Mode: "12345",
},
Error: true,
},
{
Name: "path-user-group-string",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: "root",
Group: "root",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, "root", "root", false)),
},
{
Name: "path-user-group-int64",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: int64(0),
Group: int64(0),
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, int64(0), int64(0), false)),
},
{
Name: "path-and-user-invalid-string",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: "r@@t",
},
Error: true,
},
{
Name: "path-and-user-invalid-int64",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: -1,
},
Error: true,
},
{
Name: "path-and-group-invalid-string",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Group: "r@@t",
},
Error: true,
},
{
Name: "path-and-group-invalid-int64",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Group: -1,
},
Error: true,
},
{
Name: "path-and-ensure-parent-dirs",
Dir: DirectoryCustomization{
Path: "/etc/dir",
EnsureParents: true,
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, nil, nil, true)),
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
dir, err := tc.Dir.ToFsNodeDirectory()
if tc.Error {
assert.Error(t, err)
assert.Nil(t, dir)
} else {
assert.NoError(t, err)
assert.EqualValues(t, tc.WantDir, dir)
}
})
}
}
func TestDirectoryCustomizationUnmarshalTOML(t *testing.T) {
testCases := []struct {
Name string
TOML string
Want []DirectoryCustomization
Error bool
}{
{
Name: "directory-with-path",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.directories]]
path = "/etc/dir"
`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir",
},
},
},
{
Name: "multiple-directories",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.directories]]
path = "/etc/dir1"
mode = "0700"
user = "root"
group = "root"
ensure_parents = true
[[customizations.directories]]
path = "/etc/dir2"
mode = "0755"
user = 0
group = 0
ensure_parents = true
[[customizations.directories]]
path = "/etc/dir3"
`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir1",
Mode: "0700",
User: "root",
Group: "root",
EnsureParents: true,
},
{
Path: "/etc/dir2",
Mode: "0755",
User: int64(0),
Group: int64(0),
EnsureParents: true,
},
{
Path: "/etc/dir3",
},
},
},
{
Name: "invalid-directories",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.directories]]
path = "/etc/../dir1"
[[customizations.directories]]
path = "/etc/dir2"
mode = "12345"
[[customizations.directories]]
path = "/etc/dir3"
user = "r@@t"
[[customizations.directories]]
path = "/etc/dir4"
group = "r@@t"
[[customizations.directories]]
path = "/etc/dir5"
user = -1
[[customizations.directories]]
path = "/etc/dir6"
group = -1
[[customizations.directories]]
`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := toml.Unmarshal([]byte(tc.TOML), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Directories, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.GetDirectories())
}
})
}
}
func TestDirectoryCustomizationUnmarshalJSON(t *testing.T) {
testCases := []struct {
Name string
JSON string
Want []DirectoryCustomization
Error bool
}{
{
Name: "directory-with-path",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"directories": [
{
"path": "/etc/dir"
}
]
}
}`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir",
},
},
},
{
Name: "multiple-directories",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"directories": [
{
"path": "/etc/dir1",
"mode": "0700",
"user": "root",
"group": "root",
"ensure_parents": true
},
{
"path": "/etc/dir2",
"mode": "0755",
"user": 0,
"group": 0,
"ensure_parents": true
},
{
"path": "/etc/dir3"
}
]
}
}`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir1",
Mode: "0700",
User: "root",
Group: "root",
EnsureParents: true,
},
{
Path: "/etc/dir2",
Mode: "0755",
User: int64(0),
Group: int64(0),
EnsureParents: true,
},
{
Path: "/etc/dir3",
},
},
},
{
Name: "invalid-directories",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"directories": [
{
"path": "/etc/../dir1"
},
{
"path": "/etc/dir2",
"mode": "12345"
},
{
"path": "/etc/dir3",
"user": "r@@t"
},
{
"path": "/etc/dir4",
"group": "r@@t"
},
{
"path": "/etc/dir5",
"user": -1
},
{
"path": "/etc/dir6",
"group": -1
}
{}
]
}
}`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := json.Unmarshal([]byte(tc.JSON), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Directories, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.GetDirectories())
}
})
}
}
func TestFileCustomizationToFsNodeFile(t *testing.T) {
ensureFileCreation := func(file *fsnode.File, err error) *fsnode.File {
t.Helper()
assert.NoError(t, err)
assert.NotNil(t, file)
return file
}
testCases := []struct {
Name string
File FileCustomization
Want *fsnode.File
Error bool
}{
{
Name: "empty",
File: FileCustomization{},
Error: true,
},
{
Name: "path-only",
File: FileCustomization{
Path: "/etc/file",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, nil, nil)),
},
{
Name: "path-invalid",
File: FileCustomization{
Path: "../etc/file",
},
Error: true,
},
{
Name: "path-and-mode",
File: FileCustomization{
Path: "/etc/file",
Mode: "0700",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", common.ToPtr(os.FileMode(0700)), nil, nil, nil)),
},
{
Name: "path-and-mode-no-leading-zero",
File: FileCustomization{
Path: "/etc/file",
Mode: "700",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", common.ToPtr(os.FileMode(0700)), nil, nil, nil)),
},
{
Name: "path-and-mode-invalid",
File: FileCustomization{
Path: "/etc/file",
Mode: "12345",
},
Error: true,
},
{
Name: "path-user-group-string",
File: FileCustomization{
Path: "/etc/file",
User: "root",
Group: "root",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, "root", "root", nil)),
},
{
Name: "path-user-group-int64",
File: FileCustomization{
Path: "/etc/file",
User: int64(0),
Group: int64(0),
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, int64(0), int64(0), nil)),
},
{
Name: "path-and-user-invalid-string",
File: FileCustomization{
Path: "/etc/file",
User: "r@@t",
},
Error: true,
},
{
Name: "path-and-user-invalid-int64",
File: FileCustomization{
Path: "/etc/file",
User: int64(-1),
},
Error: true,
},
{
Name: "path-and-group-string",
File: FileCustomization{
Path: "/etc/file",
Group: "root",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, "root", nil)),
},
{
Name: "path-and-group-int64",
File: FileCustomization{
Path: "/etc/file",
Group: int64(0),
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, int64(0), nil)),
},
{
Name: "path-and-group-invalid-string",
File: FileCustomization{
Path: "/etc/file",
Group: "r@@t",
},
Error: true,
},
{
Name: "path-and-group-invalid-int64",
File: FileCustomization{
Path: "/etc/file",
Group: int64(-1),
},
Error: true,
},
{
Name: "path-and-data",
File: FileCustomization{
Path: "/etc/file",
Data: "hello world",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, nil, []byte("hello world"))),
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
file, err := tc.File.ToFsNodeFile()
if tc.Error {
assert.Error(t, err)
assert.Nil(t, file)
} else {
assert.NoError(t, err)
assert.EqualValues(t, tc.Want, file)
}
})
}
}
func TestFileCustomizationUnmarshalTOML(t *testing.T) {
testCases := []struct {
Name string
TOML string
Want []FileCustomization
Error bool
}{
{
Name: "file-with-path",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.files]]
path = "/etc/file"
`,
Want: []FileCustomization{
{
Path: "/etc/file",
},
},
},
{
Name: "multiple-files",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.files]]
path = "/etc/file1"
mode = "0600"
user = "root"
group = "root"
data = "hello world"
[[customizations.files]]
path = "/etc/file2"
mode = "0644"
data = "hello world 2"
[[customizations.files]]
path = "/etc/file3"
user = 0
group = 0
data = "hello world 3"
`,
Want: []FileCustomization{
{
Path: "/etc/file1",
Mode: "0600",
User: "root",
Group: "root",
Data: "hello world",
},
{
Path: "/etc/file2",
Mode: "0644",
Data: "hello world 2",
},
{
Path: "/etc/file3",
User: int64(0),
Group: int64(0),
Data: "hello world 3",
},
},
},
{
Name: "invalid-files",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.files]]
path = "/etc/../file1"
[[customizations.files]]
path = "/etc/file2"
mode = "12345"
[[customizations.files]]
path = "/etc/file3"
user = "r@@t"
[[customizations.files]]
path = "/etc/file4"
group = "r@@t"
[[customizations.files]]
path = "/etc/file5"
user = -1
[[customizations.files]]
path = "/etc/file6"
group = -1
`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := toml.Unmarshal([]byte(tc.TOML), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Files, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.Files)
}
})
}
}
func TestFileCustomizationUnmarshalJSON(t *testing.T) {
testCases := []struct {
Name string
JSON string
Want []FileCustomization
Error bool
}{
{
Name: "file-with-path",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"files": [
{
"path": "/etc/file"
}
]
}
}`,
Want: []FileCustomization{
{
Path: "/etc/file",
},
},
},
{
Name: "multiple-files",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"files": [
{
"path": "/etc/file1",
"mode": "0600",
"user": "root",
"group": "root",
"data": "hello world"
},
{
"path": "/etc/file2",
"mode": "0644",
"data": "hello world 2"
},
{
"path": "/etc/file3",
"user": 0,
"group": 0,
"data": "hello world 3"
}
]
}
}`,
Want: []FileCustomization{
{
Path: "/etc/file1",
Mode: "0600",
User: "root",
Group: "root",
Data: "hello world",
},
{
Path: "/etc/file2",
Mode: "0644",
Data: "hello world 2",
},
{
Path: "/etc/file3",
User: int64(0),
Group: int64(0),
Data: "hello world 3",
},
},
},
{
Name: "invalid-files",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"files": [
{
"path": "/etc/../file1"
},
{
"path": "/etc/file2",
"mode": "12345"
},
{
"path": "/etc/file3",
"user": "r@@t"
},
{
"path": "/etc/file4",
"group": "r@@t"
},
{
"path": "/etc/file5",
"user": -1
},
{
"path": "/etc/file6",
"group": -1
}
]
}
}`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := json.Unmarshal([]byte(tc.JSON), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Files, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.Files)
}
})
}
}