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:
parent
26e6983320
commit
c1991b3d51
3 changed files with 1098 additions and 0 deletions
|
|
@ -21,6 +21,8 @@ type Customizations struct {
|
|||
FDO *FDOCustomization `json:"fdo,omitempty" toml:"fdo,omitempty"`
|
||||
OpenSCAP *OpenSCAPCustomization `json:"openscap,omitempty" toml:"openscap,omitempty"`
|
||||
Ignition *IgnitionCustomization `json:"ignition,omitempty" toml:"ignition,omitempty"`
|
||||
Directories []DirectoryCustomization `json:"directories,omitempty" toml:"directories,omitempty"`
|
||||
Files []FileCustomization `json:"files,omitempty" toml:"files,omitempty"`
|
||||
}
|
||||
|
||||
type IgnitionCustomization struct {
|
||||
|
|
@ -341,3 +343,17 @@ func (c *Customizations) GetIgnition() *IgnitionCustomization {
|
|||
}
|
||||
return c.Ignition
|
||||
}
|
||||
|
||||
func (c *Customizations) GetDirectories() []DirectoryCustomization {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Directories
|
||||
}
|
||||
|
||||
func (c *Customizations) GetFiles() []FileCustomization {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Files
|
||||
}
|
||||
|
|
|
|||
285
internal/blueprint/fsnode_customizations.go
Normal file
285
internal/blueprint/fsnode_customizations.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
package blueprint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/osbuild/osbuild-composer/internal/common"
|
||||
"github.com/osbuild/osbuild-composer/internal/fsnode"
|
||||
)
|
||||
|
||||
// validateModeString checks that the given string is a valid mode octal number
|
||||
func validateModeString(mode string) error {
|
||||
// Check that the mode string matches the octal format regular expression.
|
||||
// The leading is optional.
|
||||
if regexp.MustCompile(`^[0]{0,1}[0-7]{3}$`).MatchString(mode) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid mode %s: must be an octal number", mode)
|
||||
}
|
||||
|
||||
// DirectoryCustomization represents a directory to be created in the image
|
||||
type DirectoryCustomization struct {
|
||||
// Absolute path to the directory
|
||||
Path string `json:"path" toml:"path"`
|
||||
// Owner of the directory specified as a string (user name), int64 (UID) or nil
|
||||
User interface{} `json:"user,omitempty" toml:"user,omitempty"`
|
||||
// Owner of the directory specified as a string (group name), int64 (UID) or nil
|
||||
Group interface{} `json:"group,omitempty" toml:"group,omitempty"`
|
||||
// Permissions of the directory specified as an octal number
|
||||
Mode string `json:"mode,omitempty" toml:"mode,omitempty"`
|
||||
// EnsureParents ensures that all parent directories of the directory exist
|
||||
EnsureParents bool `json:"ensure_parents,omitempty" toml:"ensure_parents,omitempty"`
|
||||
}
|
||||
|
||||
// Custom TOML unmarshalling for DirectoryCustomization with validation
|
||||
func (d *DirectoryCustomization) UnmarshalTOML(data interface{}) error {
|
||||
var dir DirectoryCustomization
|
||||
|
||||
dataMap, _ := data.(map[string]interface{})
|
||||
|
||||
switch path := dataMap["path"].(type) {
|
||||
case string:
|
||||
dir.Path = path
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: path must be a string")
|
||||
}
|
||||
|
||||
switch user := dataMap["user"].(type) {
|
||||
case string:
|
||||
dir.User = user
|
||||
case int64:
|
||||
dir.User = user
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: user must be a string or an integer, got %T", user)
|
||||
}
|
||||
|
||||
switch group := dataMap["group"].(type) {
|
||||
case string:
|
||||
dir.Group = group
|
||||
case int64:
|
||||
dir.Group = group
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: group must be a string or an integer")
|
||||
}
|
||||
|
||||
switch mode := dataMap["mode"].(type) {
|
||||
case string:
|
||||
dir.Mode = mode
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: mode must be a string")
|
||||
}
|
||||
|
||||
switch ensureParents := dataMap["ensure_parents"].(type) {
|
||||
case bool:
|
||||
dir.EnsureParents = ensureParents
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: ensure_parents must be a bool")
|
||||
}
|
||||
|
||||
// try converting to fsnode.Directory to validate all values
|
||||
_, err := dir.ToFsNodeDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = dir
|
||||
return nil
|
||||
}
|
||||
|
||||
// Custom JSON unmarshalling for DirectoryCustomization with validation
|
||||
func (d *DirectoryCustomization) UnmarshalJSON(data []byte) error {
|
||||
type directoryCustomization DirectoryCustomization
|
||||
|
||||
var dirPrivate directoryCustomization
|
||||
if err := json.Unmarshal(data, &dirPrivate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := DirectoryCustomization(dirPrivate)
|
||||
if uid, ok := dir.User.(float64); ok {
|
||||
// check if uid can be converted to int64
|
||||
if uid != float64(int64(uid)) {
|
||||
return fmt.Errorf("invalid user %f: must be an integer", uid)
|
||||
}
|
||||
dir.User = int64(uid)
|
||||
}
|
||||
if gid, ok := dir.Group.(float64); ok {
|
||||
// check if gid can be converted to int64
|
||||
if gid != float64(int64(gid)) {
|
||||
return fmt.Errorf("invalid group %f: must be an integer", gid)
|
||||
}
|
||||
dir.Group = int64(gid)
|
||||
}
|
||||
// try converting to fsnode.Directory to validate all values
|
||||
_, err := dir.ToFsNodeDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = dir
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToFsNodeDirectory converts the DirectoryCustomization to an fsnode.Directory
|
||||
func (d DirectoryCustomization) ToFsNodeDirectory() (*fsnode.Directory, error) {
|
||||
var mode *os.FileMode
|
||||
if d.Mode != "" {
|
||||
err := validateModeString(d.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modeNum, err := strconv.ParseUint(d.Mode, 8, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid mode %s: %v", d.Mode, err)
|
||||
}
|
||||
mode = common.ToPtr(os.FileMode(modeNum))
|
||||
}
|
||||
|
||||
return fsnode.NewDirectory(d.Path, mode, d.User, d.Group, d.EnsureParents)
|
||||
}
|
||||
|
||||
// FileCustomization represents a file to be created in the image
|
||||
type FileCustomization struct {
|
||||
// Absolute path to the file
|
||||
Path string `json:"path" toml:"path"`
|
||||
// Owner of the directory specified as a string (user name), int64 (UID) or nil
|
||||
User interface{} `json:"user,omitempty" toml:"user,omitempty"`
|
||||
// Owner of the directory specified as a string (group name), int64 (UID) or nil
|
||||
Group interface{} `json:"group,omitempty" toml:"group,omitempty"`
|
||||
// Permissions of the file specified as an octal number
|
||||
Mode string `json:"mode,omitempty" toml:"mode,omitempty"`
|
||||
// Data is the file content in plain text
|
||||
Data string `json:"data,omitempty" toml:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Custom TOML unmarshalling for FileCustomization with validation
|
||||
func (f *FileCustomization) UnmarshalTOML(data interface{}) error {
|
||||
var file FileCustomization
|
||||
|
||||
dataMap, _ := data.(map[string]interface{})
|
||||
|
||||
switch path := dataMap["path"].(type) {
|
||||
case string:
|
||||
file.Path = path
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: path must be a string")
|
||||
}
|
||||
|
||||
switch user := dataMap["user"].(type) {
|
||||
case string:
|
||||
file.User = user
|
||||
case int64:
|
||||
file.User = user
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: user must be a string or an integer")
|
||||
}
|
||||
|
||||
switch group := dataMap["group"].(type) {
|
||||
case string:
|
||||
file.Group = group
|
||||
case int64:
|
||||
file.Group = group
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: group must be a string or an integer")
|
||||
}
|
||||
|
||||
switch mode := dataMap["mode"].(type) {
|
||||
case string:
|
||||
file.Mode = mode
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: mode must be a string")
|
||||
}
|
||||
|
||||
switch data := dataMap["data"].(type) {
|
||||
case string:
|
||||
file.Data = data
|
||||
case nil:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("UnmarshalTOML: data must be a string")
|
||||
}
|
||||
|
||||
// try converting to fsnode.File to validate all values
|
||||
_, err := file.ToFsNodeFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*f = file
|
||||
return nil
|
||||
}
|
||||
|
||||
// Custom JSON unmarshalling for FileCustomization with validation
|
||||
func (f *FileCustomization) UnmarshalJSON(data []byte) error {
|
||||
type fileCustomization FileCustomization
|
||||
|
||||
var filePrivate fileCustomization
|
||||
if err := json.Unmarshal(data, &filePrivate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := FileCustomization(filePrivate)
|
||||
if uid, ok := file.User.(float64); ok {
|
||||
// check if uid can be converted to int64
|
||||
if uid != float64(int64(uid)) {
|
||||
return fmt.Errorf("invalid user %f: must be an integer", uid)
|
||||
}
|
||||
file.User = int64(uid)
|
||||
}
|
||||
if gid, ok := file.Group.(float64); ok {
|
||||
// check if gid can be converted to int64
|
||||
if gid != float64(int64(gid)) {
|
||||
return fmt.Errorf("invalid group %f: must be an integer", gid)
|
||||
}
|
||||
file.Group = int64(gid)
|
||||
}
|
||||
// try converting to fsnode.File to validate all values
|
||||
_, err := file.ToFsNodeFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*f = file
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToFsNodeFile converts the FileCustomization to an fsnode.File
|
||||
func (f FileCustomization) ToFsNodeFile() (*fsnode.File, error) {
|
||||
var data []byte
|
||||
if f.Data != "" {
|
||||
data = []byte(f.Data)
|
||||
}
|
||||
|
||||
var mode *os.FileMode
|
||||
if f.Mode != "" {
|
||||
err := validateModeString(f.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modeNum, err := strconv.ParseUint(f.Mode, 8, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid mode %s: %v", f.Mode, err)
|
||||
}
|
||||
mode = common.ToPtr(os.FileMode(modeNum))
|
||||
}
|
||||
|
||||
return fsnode.NewFile(f.Path, mode, f.User, f.Group, data)
|
||||
}
|
||||
797
internal/blueprint/fsnode_customizations_test.go
Normal file
797
internal/blueprint/fsnode_customizations_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue